参考:《第一行代码》
Android系统中主要提供了三种方式用于简单地实现数据持久化功能, 即文件存储、SharedPreference存储以及数据库存储。
当然,除了这三种方式之外,你还可以将数据保存在手机的 SD卡中,不过使用文件、SharedPreference或数据库来保存数据会相对更简单一些,而且比起将数据保存在 SD卡中会更加的安全。
文件存储
openFileOutput()方法,可以用于将数据 存储 到指定的文件中。第一个参数是文件名,注意这里指定的文件名不可以包含路径,因为所有的文件都是默认存储到/data/data/<package name>/files/目 录下 的 。第二个参数是文件的操作模式,主要有两种模式可选,MODE_PRIVATE和 MODE_APPEND。其中 MODE_PRIVATE是默认的操作模式,表示当指定同样文件名的时候,所写入的内容将会覆盖原文件中的内容,而 MODE_APPEND则表示 如果该文件已存在就往文件里面追加内容,不存在就创建新文件。
openFileInput()方法,用于从文件中 读取 数据。它只接收一个参数,即要读取的文件名,然后系统会自动到/data/data/<packagename>/files/目录下去加载这个文件,并返回一个 FileInputStream对象,得到了这个对象之后再通过 Java流的方式就可以将数据读取出来了。
SharedPreferences存储
Android 中主要提供了三种方法用于得到 SharedPreferences对象。
1. Context类中的 getSharedPreferences()方法
此方法接收两个参数,第一个参数用于指定 SharedPreferences文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences 文件都是存放在/data/data/<package name>/shared_prefs/目录下的。第二个参数用于指定操作模式,主要有两种模式可以选择,MODE_PRIVATE和 MODE_MULTI_PROCESS。MODE_PRIVATE仍然是默认的操作模式,和直接传入0效果是相同的,表示只有当前的应用程序才可以对这个 SharedPreferences文件进行读写。MODE_MULTI_PROCESS则一般是用于会有多个进程中对同一个SharedPreferences文件进行读写的情况。
2. Activity类中的 getPreferences()方法
这个方法和 Context中的 getSharedPreferences()方法很相似,不过它只接收一个操作模式参数,因为使用这个方法时会自动将当前活动的类名作为SharedPreferences的文件名
3. PreferenceManager类中的 getDefaultSharedPreferences()方法
这是一个静态方法,它接收一个 Context参数,并自动使用当前应用程序的包名作为前缀来命名 SharedPreferences文件。
得到了SharedPreferences对象之后,就可以开始向SharedPreferences文件中存储数据了, 主要可以分为三步实现。
1. 调用 SharedPreferences对象的 edit() 方法来获取一个 SharedPreferences.Editor对象。
2. 向 SharedPreferences.Editor 对象中添加数据,比如添加一个布尔型数据就使用 putBoolean方法,添加一个字符串则使用 putString()方法,以此类推。
3. 调用 commit()方法将添加的数据提交,从而完成数据存储操作。
从 SharedPreferences中读取数据:
SharedPreference在使用过程中有什么注意点?commit()和apply()的区别
返回值
apply()没有返回值,而commit()返回boolean表明修改是否提交成功。
apply()是将修改数据提交到内存, 而后异步真正提交到硬件磁盘。
而commit()其过程也是先把数据更新到内存,然后在当前线程中写文件操作(同步的提交到硬件磁盘),提交完成返回提交状态。
如果对提交的结果不关心的话,建议使用apply(),如果需要确保提交成功且有后续操作的话,还是需要用commit()。
一句话:在多进程中,如果要交换数据,不要使用SharedPreference,因为在不同版本表现不稳定,推荐使用ContentProvider替代。
· ContextImpl中有一个静态的ArrayMap变量sSharedPrefs,无论有多少个ContextImpl对象实例,系统都共享这一个sSharedPrefs的Map,应用启动以后首次使用SharePreference时创建,系统结束时才可能会被垃圾回收器回收,所以如果我们一个App中频繁的使用不同文件名的SharedPreferences很多时这个Map就会很大,也即会占用移动设备宝贵的内存空间。所以我们应用中应该尽可能少的使用不同文件名的SharedPreferences,取而代之的是合并他们,减小内存使用
· SharedPreferences在实例化时首先会从sdcard异步读文件,然后缓存在内存中;接下来的读操作都是内存缓存操作而不是文件操作。
· 在SharedPreferences的Editor中如果用commit()方法提交数据,其过程是先把数据更新到内存,然后在当前线程中写文件操作,提交完成返回提交状态;如果用的是apply()方法提交数据,首先也是写到内存,接着在一个新线程中异步写文件,然后没有返回值。
· 在写操作commit时有三级锁操作,效率很低,所以当我们一次有多个修改写操作时等都批量put完了再一次提交确认,这样可以提高效率。
SharedPreferences内部工作原理:
1、调用getSharedPreferences();创建一个SharedPreferences对象,其中会先判断是否存在对应xml文件,如果发现存在则会有一个预加载操作,这个操作是把xml文件的内容通过I/O操作和XmlUitl解析后存入一个map对象中,所以我们调用SharedPreferences::getString();等get操作实际上是不会对文件做I/O操作,而是直接访问刚刚的map集合的内容,这提高了效率,如果对应的xml不存在则重新创建一个对应的xml文件。
部分实现如下:
2、put写操作:写操作也有两步,一是把数据先写入内存中,即map集合,二是把数据写入硬盘文件中。这样才能保证数据的完整性,写操作有两个提交的方式:
commit():线程安全,性能慢,一般来说在当前线程完成写文件操作
apply():线程不安全,性能高,异步处理IO操作,一定会把这个写文件操作放入一个SingleThreadExecutor线程池中处理
3、SharedPreferences在第一次创建后会一直维持一个Singleton,每次调用getSharedPreferences()都返回唯一的一个实例
SQLite数据库存储
Android为了让我们能够更加方便地管理数据库,专门提供了一个 SQLiteOpenHelper帮助类,借助这个类就可以非常简单地对数据库进行创建 和 升级。SQLiteOpenHelper中还有两个非常重要的实例方法,getReadableDatabase()和getWritableDatabase()。这两个方法都可以创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。不同的是,当数据库不可写入的时候(如磁盘空间已满)getReadableDatabase()方法返回的对象将以只读的方式去打开数据库,而getWritableDatabase()方法则将出现异常。
SQLiteOpenHelper中有两个构造方法可供重写,一般使用参数少一点的那个构造方法即可。这个构造方法中接收四个参数,第一个参数 是 Context,这个没什么好说的,必须要有它才能对数据库进行操作。第二个参数 是数据库名,创建数据库时使用的就是这里指定的名称。第三个参数 允许我们在查询数据的时候返回一个自定义的 Cursor,一般都是传入null。第四个参数 表示当前数据库的版本号,可用于对数据库进行升级操作。构建出 SQLiteOpenHelper的实例之后,再调用它的 getReadableDatabase()或 getWritableDatabase()方法就能够创建数据库了,数据库文件会存放在/data/data/<package name>/databases/目录下。
此时,重写的onCreate() 方法也会得到执行,所以通常会在这里去处理一些创建表 的逻辑。
onUpgrade() 方法是用于对数据库进行升级的,它在整个数据库的管理工作当中起着非常重要的作用。(这一部分直接参考第一行代码)。
数据库的增删查改:
其中 C 代表添加 (Create),R代表查询(Retrieve),U代表更新(Update),D代表删除(Delete)。
增:SQLiteDatabase中提供了一个 insert() 方法,这个方法就是专门用于添加数据的。它接收三个参数,第一个参数是表名,我们希望向哪张表里添加数据,这里就传入该表的名字。第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值 NULL,一般我们用不到这个功能,直接传入 null即可。第三个参数是一个 ContentValues对象,它提供了一系列的put()方法重载,用于向ContentValues中添加数据,只需要将表中的每个列名以及相应的待添加数据传入即可。
改:update()方法用于对数据进行更新,这个方法接收四个参数,第一个参数是表名,第二个参数是 ContentValues对象,要把更新数据在这里组装进去。第三、第四个参数用于去约束更新某一行或某几行中的数据,不指定的话默认就是更新所有行。这里在更新数据按钮的点击事件里面构建了一个 ContentValues对象,并且只给它指定了一组数据,说明我们只是想把价格这一列的数据更新成10.99。然后调用了SQLiteDatabase 的update()方法去执行具体的更新操作,可以看到,这里使用了第三、第四个参数来指定具体更新哪几行。第三个参数对应的是SQL语句的 where部分,表示去更新所有 name等于 ? 的行,而 ? 是一个占位符,可以通过第四个参数提供的一个字符串数组为第三个参数中的每个占位符指定相应的内容。因此上述代码想表达的意图就是,将名字是 The DaVinci Code 的这本书的价格改成 10.99。
<span style="font-size:12px;">SQLiteDatabase db = dbHelper.getWritableDatabase(); ContentValues values = new ContentValues(); values.put("price", 10.99); db.update("Book", values, "name = ?", new String[] { "The DaVinci Code" });</span>
删:delete() 方法专门用于删除数据,这个方法接收三个参数,第一个参数仍然是表名,这个已经没什么好说的了,第二、第三个参数又是用于去约束删除某一行或某几行的数据,不指定的话默认就是删除所有行。可以看到,我们在删除按钮的点击事件里指明去删除 Book表中的数据,并且通过第二、 第三个参数来指定仅删除那些页数超过 500页的书籍。
<span style="font-size:12px;">SQLiteDatabase db = dbHelper.getWritableDatabase(); db.delete("Book", "pages > ?", new String[] { "500" });</span>
查:query() 方法用于对数据进行查询。这个方法的参数非常复杂,最短的一个方法重载也需要传入七个参数。第一个参数是表名,表示我们希望从哪张表中查询数据。第二个参数用于指定去查询哪几列,如果不指定则默认查询所有列。第三、第四个参数用于去约束查询某一行或某几行的数据,不指定则默认是查询所有行的数据。第五个参数用于指定需要去 group by的列,不指定则表示不对查询结果进行 groupby操作。第六个参数用于对 group by之后的数据进行进一步的过滤,不指定则表示不进行过滤。第七个参数用于指定查询结果的排序方式,不指定则表示使用默认的排序方式。
数据库的升级:
每一个数据库版本都会对应一个版本号,当指定的数据库版本号大于当前数据库版本号的时候,就会进入到 onUpgrade() 方法中去执行更新操作。这里需要为每一个版本号赋予它各自改变的内容,然后在 onUpgrade()方法中对当前数据库的版本号进行判断,再执行相应的改变就可以了。
(具体查看第一行代码P264)