1. Android 中提供的数据持久化方式有(不考虑存储到手机 SD 卡的情况)
文件存储
- Android 中最基本的一种数据存储方式,它不对存储的内容进行任何的格式化处理,所有文件都是原封不动保存到文件中的,适合存储一些简单的文本数据或二进制数据。如果使用文件存储的方式存储一些较为复杂的文本文本数据,需要定义一套自己的格式规范,方便之后将数据从文件中重新解析出来(不常用)
- 核心技术是
Context
类中提供的openFileInput()
方法和openFileOutput()
方法,加上 Java 的各种流操作 - 默认存储到
/data/data/<package name>/files/
目录下,可使用 AS 的 Android Device Monitor 工具的 File Explorer 标签页查看
SharedPreferences
存储- 使用键值对的方式存储数据,支持
int
、float
、long
、boolean
4 个数据类型和字符串String
的存储 - 核心技术是
SharedPreferences
类,SharedPreferences
是一个单例 - 默认存放在
/data/data/<package name>/shared_prefs/
目录下,同样可以借助 AS 的 ADM 工具查看
- 使用键值对的方式存储数据,支持
SQLite 数据库存储
- SQLite 数据库是内置在 Android 系统中的一款轻量级的关系型数据库,运算速度快,占用资源少,通常只需要几百 K 的内存就足够了,特别适合在移动设备上使用。 SQLite 支持标准的 SQL 语法,遵循数据库的 ACID 事务,简单易用,甚至不用设置用户名和密码就可以
- 核心技术是
SQLiteOpenHelper
抽象类,需要创建它的实现类,实现onCreate()
方法和onUpgrade()
方法 - 默认存放在
/data/data/<package name>/databases/
目录下,同样可以借助 AS 的 ADM 工具查看
2. 写一个使用文件存储技术实现数据持久化的 Demo
1 | public class MainActivity extends AppCompatActivity { |
3. Android 中获取 SharedPreferences
对象的方法有
Context
类中的getSharedPreferences()
方法- 此方法接收两个参数,第一个参数用于指定
SharedPreferences
文件的名称,如果指定的文件不存在则会创建一个 - 第二个参数用于指定操作模式,目前只有
MODE_PRIVATE
这一种模式可选,它是默认的操作模式,和直接传入 0 的效果相同,表示只有当前的应用程序才可以对这个 SP 文件进行读写 - 其他几种模式都已被废弃,
MODE_WORLD_READABLE
和MODE_WORLD_WRITEABLE
这两种模式是在 Android 4.2 版本中被废弃的;MODE_MULTI_PROCESS
模式是在 Andorid 6.0 版本中被废弃的,即禁止在多进程之间使用SharedPreferences
共享数据
- 此方法接收两个参数,第一个参数用于指定
Activity
类中的getPreferences()
方法:这个方法只接收一个操作模式参数,因为使用这个方法时会自动将当前Activity
的类名作为 SP 的文件名PreferenceManager
类中的getDefaultSharedPreferences()
方法:这是一个静态方法,它接收一个Context
参数,并自动使用当前应用程序的包名作为前缀来命名 SP 文件
4. 写一个使用 SharedPreferences
存储技术实现数据持久化的 Demo
1 | public class MainActivity extends AppCompatActivity { |
5. 使用 SharedPreferences
提交数据时,apply()
方法和 commit()
方法的区别是?各自使用的场景是
结论:使用 SP 提交数据时,尽量使用
Editor#apply()
方法,而不是Editor#commit()
方法(IDE 也提示使用apply()
方法)。一般来讲,仅当需要确定提交结果,并据此有后续操作时,才使用Editor#commit()
方法说明
- 使用
apply()
方法进行提交会先写入内存,然后异步写入磁盘;commit()
方法是直接写入磁盘。因为 IO 操作一般比较费时,所以如果操作频繁的话,apply()
的性能会优于commit()
的性能 apply()
会将最后修改内容写入磁盘,但是如果希望立刻获取存取的结果,并据此做相应的其他操作,应当使用commit()
apply()
方法可能会执行失败,如果失败不会受到错误回调
- 使用
举例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 正例
public void updateSettingsAsync() {
SharedPreferences sp = getSharedPreferences("settings", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString("id", "foo");
editor.apply();
}
public void updateSettings() {
SharedPreferences sp = getSharedPreferences("settings", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString("id", "foo");
if(!editor.commit()) {
Log.e(LOG_TAG, "Failed to commit settings changes");
}
}
// 反例
editor.putLoong("key_name", "long value");
editor.commit();
6. 滥用 SharedPreferences
的场景有哪些?后果是?解决方案?(性能优化相关)
场景1:存储超大的 value
- 后果:会占用内存,引起界面卡顿、频繁 GC
- 方案:Don’t do this
场景2:存储 JSON/HTML 等特殊符号很多的 value
- 后果:会导致读取速度急剧下降
- 方案:应直接使用 JSON
场景3:多次
edit()
、多次apply()/commit()
- 后果:占用内存,界面卡顿(尤其是
commit()
方法) - 方案:应该尽量批量修改一次提交,避免产生过多
Editor
对象
- 后果:占用内存,界面卡顿(尤其是
场景4:使用
MODE_MULTI_PROCESS
跨进程- 后果: Androd 6.0 版本已废弃这个 flag
- 方案:Don’t do this,应使用类似
ContentProvider
这些
7. SharedPreferences
的缺点
- 跨进程不安全。由于没有使用跨进程的锁,即使使用 MODE_MULTI_PROCESS,SP 在跨进程频繁读写有可能导致数据全部丢失。根据线上统计,SP 大约有 1/10000 的损坏率。Android 系统不把 SP 设计成跨进程安全的原因之一是因为官方更希望开发者在这个场景使用 ContentProvider
- 加载缓慢。SP 文件的加载使用了异步线程,而且加载线程并没有设置线程优先级,如果此时主线程读取数据就需要等待文件加载线程的结束。这就导致出现主线程等待低优先级线程锁的问题,比如一个 100KB 的 SP 文件读取等待时间大约需要 50~100 ms,建议提前用异步线程预加载启动过程用到的 SP 文件
- 全量写入。无论是调用
commit()
还是apply()
,即使只改动其中的一个条目,都会把整个内容全部写到文件。而且即使我们多次写入同一个文件,SP 也没有将多次修改合并为一次,即没有使用缓存 buffer 的策略,这也是性能差的重要原因之一 - 卡顿。由于提供了异步落盘的 apply 机制,在崩溃或其他一些异常情况可能会导致数据丢失。所以当应用受到系统广播或者被调用
onPause()
等一些时机,系统会强制把所有的 SP 对象数据落地到磁盘。如果没有落地完成,此时主线程会被一直阻塞。这样非常容易造成卡顿,甚至是 ANR,从线上数据来看 SP 卡顿占比一般会超过 5%