任何一个应用程序,其实说白了就是在不停地和数据打交道,我们聊QQ、看新闻、刷微博,所关心的都是里面的数据,没有数据的应用程序就变成了一个空壳子,对用户来说没有任何实际用途。呢么这些数据都是从哪里来的啦?现在多数的数据都是由用户产生的,比如你发微博,评论新闻,其实都是在产生数据。
而我们前面章节所编写的众多例子中也有用到各种各样中的数据,例如3章最佳时间部分在登录界面输入账号和密码。这些数据都有一个共同点,及他们都是属于瞬间数据。那么什么是瞬间数据啦?就是指那么数据存储在内存当中,有可能会因为程序关闭或其他原因导致内存被回收而丢失的数据。这对于一些关键性数据是绝对不能容忍的,谁都不希望自己刚发出去的微博,刷新一下就没了吧!那么怎样才能保证一些关键性数据不会丢失啦?这就需要用到数据持久化了。
6.1 持久化技术简介
数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或电脑关机的情况下,这些数据仍然不会丢失。保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的,持久化技术则提供了一种机制可以让数据在瞬时状态和持久状态之间进行转化。
持久化技术被广泛应用于各种程序设计的领域当中,而这本书重要探讨的自然是Android中数据持久化技术。Android系统中主要提供了3种方式用于简单的实现数据持久化功能,及文件存储、ShaerdPreference存储以及数据库存储。当然,出了这3中方式之外,你还可以将数据库保存在SD卡中,不过使用文件、SharedPreference或数据库会相对更简单一些,而且不起将数据保存在SD卡中会更加地安全。
那么下面我就讲对这3中数据持久化的方式一一进行详细的解释。
6.2 文件存储
文件存储是Android中最基本的一种数据存储方式,他不对存储的内容进行任何的格式处理,所有的数据都是原封不动的保存到文件中,因而他比较适合存储一些简单的文本数据或二进制数据。如果你想使用文件存储的方式来保存一些较为复杂的文本数据,就炫耀定义一套自己的格式规范,这样可以方便之后数据从文件中重新解析出来。
那么首先我们就来看一看,Android中是如何通过文件保存数据的。
6.2.1 将数据存储到文件中
Context类中提供了一个openFileOutput()方法,可以用于将数据存储到指定文件中。这个方法接收两个参数,第一个参数是文件名,在文件创建的时候使用的就是一个名称注意这里指定的文件名不可以包含路径,因为所有的文件都是默认存储到/data/data/<package name>/files/目录下的。第二个参数时文件的操作模式,主要有两种模式可选,MODE_PRIVATE和MODE_APPEND。其中MODE_PRIVATE是默认的操作模式,表示当指定同样文件名的时候,所写入的内容将会覆盖源文件中的内容,而MODE_APPEND则表示如果该文件已经存在,就往文件里面追加内容,不存在就创建文件。其实文件的操作模式本来还有另外两种:MODE_WORLD_READABLEhe MODE_MODE_WRITEABLE,这两种模式表示允许其他的应用程序对我们程序中的文件进行读写操作,不过由于这两种模式过于危险,很容易引起应用的安全性漏洞,已在Android 4.2 版本中被废弃。
openFileOutput()方法返回的是一个FileOutputStream对象,得到了这个独享之后就可以使用Java留的方式讲述写入到文件中了。以下是一段简单的代码示例,展示了如何将一段文本内容保存到文件中:
这里只是在布局里面加入了一个EditText,用于输入文本内容。其实现在你就可以运行以下程序了,界面上肯定会有一个文本输入框。然后在文本输入框中随意输入点什么内容,再按下Back键,这是输入的内容坑定已经丢失了,因为他只是瞬时数据,在活动被销毁后就会被回收,而这里我们要做的,就是在数据被回收之前,将它存储到文件当中。修改MainActivity中的代码,如下所示:
可以看到,首先我们在onCreat()方法中获取了EditText的实例,然后重写了onDestry()方法,这样就可以保证在活动销毁之前一定会调用这个方法。在onDestry方法中我们获取了EditText()方法中的代码和之前的是基本相同,这里就不做解释了。现在重新运行以下程序,并在EditText中输入一些内容,如图6.1所示。
然后按下Back键关闭程序,这时我们输入的内容就已经保存到文件中了。那么如何才能证明使数据确实已经成功了啦?我们可以借助Android Device Monitor工具来查看一下。点击as导航栏中的Tools--》Android,会看到如图6.2所示的工具列表。
点击android Device Monitor就可以打开Android Device Monitor工具了,然后进入File Explorer标签页,在这里找到/data/data/com.example.filepersistencetest/files/目录,可以看到生成了一个data文件,如图6.3所示:
然后点击图6.4中左边的按钮可以将这个文件导出到电脑上
使用记事本打开这个文件,里面的内容如图6.5所示。
这样就证明了,在Edite中输入的内容确实应景陈宫保存到了文件中了。不过只是成功讲述保存下来还不够,我们好需要想办法在下次启动程序的时候让这些数据能够还原到EditText中,因此接下来我们就要学习意下如何从文件中读取数据。
6.2.2 从文件中读取数据
类似于将数据存储到文件中,Context类中还提供了一个openFileInput()方法,用于从文件中读取数据。这个方法要比openFileOutput简单一些,他只接受一个参数,纪要读取的文件名,然后系统会自动到data/data/<package name>/files/目录下去加载这个文件,并然会一个FileInputStream对象,得到了这个对象之后再通过java流的方式就可以将数据读取出来了。
以下是一段简单的代码示例,展示了如何从文件中读取文本数据:
在这段代码中,首先通过openFileInput()方法获取到了一个FileInputStream对象,然后借助它又构建出了InputStreamReader对象,接着在使用InputStreamReader构建出一个BufferedReader对象,这样我们就可以通过Bufferreader进行一行行地读取,把文件中所有的文本内容全部读取出来,并存放在一个StringBuilder对象中,最后将读取到的内容返回就可以了。
了解了从文件中读取数据的方法,那么我们就来继续完善上一小节中的例子,是的重新启动程序是EditText中鞥狗保留我们上次输入的内容。修改MainActivity中的代码,如下所示:
可以看到,这里的思路非常简单,在onCreat()方法中调用load()方法来读取文件中存储的文本内容,如果读到的内容不为null,就调用EditText的setText()方法将内容天聪到EditText里,并调用setSelection()方法将输入光标移动到文本的末尾位置以便于继续输入,然后弹出一句还原成功的提示。load()方法中的细节我们在前面已经讲过,这里就不在追溯了。
注意,上述代码在对字符串非空判断的时候使用了TextUtils.isEmpty()方法,这是一个非常好用的方法,他可以一次性进行两种空值判断。当传入的字符串等于null或者等于空字符串的时候,这个方法都会返回true,从而使得我们不需要先单独判断这两种空值在使用逻辑运算符连接起来了。
现在重新运行一下程序,刚才保存的Context字符串肯定会被填充到EditText中,然后编写一点其他的内容,比如在Edite中输入Hello,接着按下Back键退出程序,再重新启动程序,这是刚才输入的内容不会丢失,而是花园道了EditText中,如图6.6所示。
这样我们就已经把文件存储方面的知识学习完了,其实所用到的核心技术就是Context类中提供的openFileInput()和openFileOutput()方法,之后就是利用java的各种流进行读写操作。
不过正如我前面所说,文件存储的方式并适合用于保存一些较为复杂的文本数据,因此,下面我么就来学习一下Android中另一种数据持久化的方式,他比文件存储更加简单易用,而且可以很方便的对某一指定的数据进行读写操作。
6.3 SharePreferences 存储
不同于文件的存储方式,SharedPreferences是使用键值对的方式来存储数据的。也就是说,当保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可以通过这个键把相应的值取出来。而且SharedPreferences还支持多种不同的数据类型存储,如果存储的数据类型是整形,那么读取出来的数据也是整形的;如果存储的数据是一个字符串,那么读取出来的数据仍然是字符串。
6.3.1 将数据存储到SharedPreferences 中
要想使用SharedPreferences来存储数据,首先需要将获取到SharedPreferences对象。Android中主要提供了3种方法用于得到SharedPreferences对象。
1.Context类中的getSharedPrefer()方法
此方法接收两个参数,第一个参数用于指定SharedPreferences文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences文件都是存在/data/data/<package name>/shared_prefs/目录下的。第二个参数用于指定操作模式,目前只有MODE_PRIAVE这一种模式可以选,它是默认的操作模式,和直接传入0的效果是一样的,表示只有当前应用程序才可以对这个SharedPreferences文件进行读写。其他几种操作模式均已废弃.
2.Activity类中的getPreference()方法
这个方法和Context中的getSharedPreference()方法很相似,,不过他只接收一个操作模式参数,因为使用这个方法时会自动将当前活动的类名作为SharedPreferences的文件名。
3.PreferenceManager类中的getDefaultSharedPreferences()方法
这是一个静态方法,他接收一个Context参数,并自动使用当前应用程序的包名作为前缀来命名SharedPreferences文件。得到了下SharedPreferences对象之后,就可以开始向SharedPreferences文件中存储数据了,主要可以分为3步实现。
(1)调用SharedPreferences对象的edit()方法来获取一个SharedPreferences.editor对象。
(2)向SharedPreferences.Editor对象中添加数据,比如添加一个boolean数据就是使用putBoolean()方法,添加一个字符串则使用putString()方法,以此类推。
(3)调用apply()方法将添加的数据提交,从而完成数据存储操作。
不知不觉中已经将理论知识介绍的挺多了,那我们就赶快通过一个例子来体验一下SharedPreferences存储的用法吧。新建一个SharedPreferencesTest项目,然后修改activity_main.xml中的代码,如下所示:
可以看到,这里首先给按钮注册了一个点击事件,然后在点击事件中通过getShredPreferences()指定文件名为data,并得到了SharedPreferences.Editor对象。接着向这个对象中添加了三条不同类型的数据,最后调用apply()方法进行提交,从而完成了数据存储的操作。
很简单吧?现在就可以运行一下程序了,进入程序的主界面后,点击一下Save_data按钮。这时的数据应该已经保存成功了,不过为了证实一下,我们还是要借助File Explorer来进行查看。打开Android Device Monitor,并点击File Explorer标签页,然后进入到/data/data/com.comple.sharedpreferencestest/shaared_prefs/目录下,可以看到生成了一个data.xml文件,如图6.7所示。
接下来,同样十点几到处按钮将这个文件到处到电脑上,并用记事本进行查看,里面的内容如图6.8所示。
可以看到,我们刚刚在按钮的点击事件中添加的所有数据都已经成功保存下来了,并且SharedPreferences文件是使用XML格式来对数据进行管理的。
那么接下来我们自然要看一看,如何从SharedPreferences文件中读取这些数据了。
6.3.2 从SharedPreferences 中读取数据
你应该已经感觉到了,使用SharedPreferences来存储数据是非常简单的不过下面有更好的消息,其实从SharedPreferences文件中读取数据会更加的简单。SharedPreferences对象中提供了一系列的get方法,用于对存储的数据进行读取,每种get方法都对应了SharedPreferences.editor中的一种put方法,比如读取一个Boolean数据就使用getBoolean()方法,读取一个字符串就使用getString()方法。这些get方法都接收两个参数,第一个参数是键,传入存储数据时使用的键就可以得到相应的值了;第二个参数是默认值,即表示当前传入的键找不到对应的值时会以什么样的默认值进行返回。
我们还是通过例子来实际体验一下吧,仍然是在SharedPreferencesTest项目的基础上继续开发,修改activity_main.xml中的代码,如下所示:
可以看到,我们在还原数据按钮的点击事件中首先通过getSharedPreferences()方法得到了SharedPreferences对象,然后分别调用了它的getString(),getInt(),getBoolean()方法,取货去前面所存储的姓名,年龄和是否已婚,如果没有找到相应的值,就会使用方法中传入的默认值来代替,最后通过Log将这些值打印出来。
现在重新运行一下程序,并点击界面上Restore data按钮,然后查看logcat中的打印信息,如图6.9所示。
所有值钱存储的数据都成功读取出来了!,通过这例子,我们把SharedPreferences存储的知识学写完了。相比之下SharedPreferences存储确实要比文本存储简单方便了许多,应用场景也多了不少,比如很多应用程序中的偏好设置功能其实都是用了SharedPreferences技术,那么下面我们就来编写一个记住密码的功能,相信通过这个例子能够加深你对SharedPreferences的理解。
6.3.3 实践记住密码功能
6.4 SQlite数据库存储
在刚开始接触Android的时候,我甚至不敢相信,Android系统竟然是内置了数据库的!好吧!
是我太孤陋寡闻了。SQlite是一款轻量级的关系关系型数据库,它的运算数度非常快,占用资源很少,通常只需要几百KB的内存就足够了,因而特别适合在移动设备上使用。SQlite不仅支持标准的SQL语法,还遵循了数据库的ACID事务,所以只要你以前使用国其他的关系型数据库,就可以很快上手SQlite。而SQlite又比一般的数据库简单很多,他甚至不用设置用户名和密码就可以使用。Android正是把这个功能极为强大的数据库嵌入到了系统当中,使用本地持久化的功能有了一次质的飞跃。
前面我们所学的文件存储和SharedPreferences存储毕竟只适用于保存一些简单的数据和建值对,当需要存储大量复杂的关系型数据的时候,你就会发现以上两种存储方式很难应付得了。比如我们手机的短信程序中可能会有很多个会话,每个会话中有包含了很多条信息内容,并且大部分会话还可能各自对应电话博中的某个联系人。很难想象如何用文本或者SharedPreferences来存储这些数据量大、结构性复杂的数据吧?但是使用数据库就可以做得到。那么我们就赶快来看一看,Android中的SQlite数据库到底是如何使用的。
6.4.1 创建数据库
Android为了让我们能够钢架方便的管理数据库,专门提供了一个SQLiteOpenHelper帮助类,借助这个类可以非常简单地对数据库进行创建和升级。既然有好东西可以直接使用,那我们自然要尝试一下了,下面我就对SQLiteOPenHelper的基本用法进行介绍。
首先你要知道SQLiteOpenHelper是一个抽象类,这意味着如果我们要想使用它的话,就需要创建一个自己的帮助类去继承他。SQLiteOpenHelper中有两个抽象方法,分别是onCreat()和onUpgrade(),我们必须在自己的帮助类里面重写这两个方法,然后分别在这两个方法中区实现创建、升级数据库的逻辑。
SQLiteOpenHelper中还有两个非常重要的实例方法: getReadableDatabase()getWriteableDatabase()。这两个方法都可以创建或打开一个现有的数据库(如果数据库已经存在则直接打开,否则创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。不同的是,当数据库不可写入的时候(如此磁盘空间已满),getReadableDatabase()方法返回的对象将以只读的方式去打开数据库,而getWriteableDatabase()方法则将出现异常。
SQLiteOpenHelper中有两个构造方法可提供重写,一般使用参数少的一个构造方法即可,这个构造方法中接收4个参数,第一个参数是Context,这个没什么好说的,必须要有它才能对数据库进行操作。第二个参数是数据库名,创建数据库时使用的就是这里指定的名称。第三个参数允许我们在查询数据的时候返回一个自定义Curse,一般出入null。第四个参数表示当前数据库的版本,可用于对数据库进行升级操作。构建出SQLiteOpenHelper的实例之后,再调用它的getReadableDatabase()或getWriteableDatabase()方法就能够创建数据库了,数据库文件会存放在/data/data/<package name>/database/目录下。此时,重写的onCreat()方法特会得到执行,所以通常会在这里去处理一些创建表的逻辑。
接下来我们希望创建一个名为BookStore,db的数据库,然后在这个数据库中新建一张Book表,表中有id(主键)、作者、价格、页数、和书名等列。创建数据库表当然还是需要用建表语句的,
creat table Book (
id integer primary key autoincrement,
author text
price real
pages integer
name text
)
只要你对SQL方法面的知识稍微有一些了解,上面的建表语句对你来说应该不难吧。SQLite不像其他的数据库拥有众多繁杂的数据类型,它的数据类型很贱,integer 表示整形,real 表示浮点型,text 表示文本类型,bolb 表示二进制类型。另外,上述建表语句中我们还使用了primary key将id列设为主键,并用autoincrement关键字表示id列是自增长的。
然后需要在代码中去执行这条SQL语句,才能完成创建表的操作。新建MyDatabaseHelper类继承自SQLiteOpenHelper,代码如下所示:
可以看到,我们建表语句定义成了一个字符串常量,然后在onCreat()方法中有调用了
SQLiteDatabase
的
execSQL
()方法执行这条建表语句,并弹出一个Toas提示创建成功,这样就可以保证在
数据库创建完成的同时还能成功创建Book表:
布局文件很简单,就是加入了一个按钮,用于创建数据库.最后修改MainActivty中的代码,如下所示:
这里我们在onCreat()方法中构建了一个MyDatabaseHelper对象,并且通过构造函数的参数将数据库名指定为BookStrore.db,版本号指定为1,然后在Creat database按钮的点击事件里调用getWriteableDatabase()方法。这样当第一次点击Creat dataBase按钮时,就会检测到当前程序中并没有BookStrore.db这个数据库,于是会创建该数据库并调用MyDatabaseHelper中的onCreat()方法,这样BOOK表也就得到了创建,然后就会弹出一个Toast提示数据库创建成功。再次点Creat database按钮时,会发现此时已经存在BookStore.db数据库了,因此不会再创建一次
现在就可以运行一下代码了,在程序主界面点击Creat database按钮,结果如图6.12所示:
此时BookStore.db数据库和Book表应该都已经创建成功了,因为当你再次点击Create database按钮时,不会再有Toast弹出。可以又回到了之前的那个老问题,怎样才能证实它们的确创建成功了?如果还是使用File Explorer,那么最多你只能看到database目录下出现一个BookStore.db文件,Book表示无法通过File Explorer看到的。因此这次我们准备换一种查看方式,使用adb shell来对数据库和表的创建情况进行检查。
adb 是Android SDK中自带的一个调试工具,使用这个工具可以直接对连接在电脑上手机或模拟器进行调试操作。他存在sdk的platform-tools目录下,如果想要在命令中使用这个工具,就需要先把他的路径配置到环境里。
如果你是用的是Windows系统,可以点击计算机---->属性------>高级系统设置-------->环境变量,然后在系统变量里找到Path并点击编辑,将platform-tools目录配置进去,如图6.13所示。