概述
Android SharedPreferences 提供了下面的模式来支持跨进程读写数据问题。
1 | @Deprecated |
这种模式已经被官方标记为 Deprecated
,关于废弃的原因,官方有解释:
1 | * @deprecated MODE_MULTI_PROCESS does not work reliably in |
Google认为多个进程读同一个文件都是不安全的,不建议这么做。Android 不保证该模式总是能正确的工作,建议使用 ContentProvider 替代多进程之间文件的共享。
如果在某些条件下必须使用,则要注意下面介绍的几个坑。
测试案例
在测试的工程中创建一个 Activity
,指定在另外一个进程和 Task 中启动:
1 | android:process=":second" |
然后在 MainActivity
中启动 SecondActivity
把当前时间写入到 SharedPreferences
:
1 | public void startSecondActivity(View v) { |
在 SecondActivity
中添加一个按钮,点击可以跨进程获取 SharedPreferences 中 time 的值。
先看第一种实现方案:
1 | public class SecondActivity extends Activity { |
这种情况下 mSharedPreferences
实例只在 onCreate
中被初始化一次。
测试方法,启动 SecondActivity
后获取一次数据,然后将 SecondActivity
切回到后台,再次写入数据然后打开 SecondActivity
再次读取数据,你会发现,每次读取到的数据都是第一次的数据,虽然此时已经写入了新的数据,造成了写入和读取数据不同步的问题。
下面来测试一下下面的情况:
1 | public class SecondActivity extends Activity { |
这种情况下 mSharedPreferences
的初始化放在了每次获取数据的时候。这样读取数据是正常的。
为什么会是这样的?我们从源码里面找答案。
源码分析
获取 SharedPreferences 文件
1 | private File getPreferencesDir() { |
在 ContextImpl.getPreferencesDir
方法中,会固定从 /data/data/<包名>/shared_prefs目录下获取对应名称的xml文件,如果想改变目录路径,则需要通过反射,在构造 SharedPreferencesImpl
时传入File参数来实现。
1 | Class spiClass = Class.forName("android.app.SharedPreferencesImpl"); |
获取 SharedPreferences 实例
1 | @Override |
通过 ContextImpl.getSharedPreferences
方法可以看到,获取的 SharedPreferences
是 SharedPreferencesImpl
实例。这个实例保存在静态变量 SharedPreferencesImpl
中,因此,无论该包名的应用中有多少个 ContextImpl
,他们共同使用同一个 SharedPreferencesImpl
实例。
在 MODE_MULTI_PROCESS
模式下,会调用 startReloadIfChangedUnexpectedly()
方法,区别也就在这里。
我们先来看一下 SharedPreferencesImpl
类。
该类就是一个简单的二级缓存,在启动时会将文件里的数据全部都加载到内存里。
先来看一下构造方法:
1 | SharedPreferencesImpl(File file, int mode) { |
在构造 SharedPreferencesImpl
实例时,会从xml文件中通过 loadFromDisk()
把所有数据读取到 Map 中。
数据读取
以 getString
为例子介绍:
先来看数据的读取:
1 | @Nullable |
可以看到,数据的读取直接从内存的 Map 中获取,没有涉及到 xml 文件的读取。
这也就不难理解,跨进程读取和写入的时候,为什么会造成数据的不同步了,如果写入是在另外一个进程,写入后,如果读取进程不重新从加载xml文件到内存,那么读取进程的Map是不会更新的,就读取不到另外进程新写入的数据了。
get 方法使用了对象的同步锁,说明这个方法是线程安全的。
数据写入
再来看一下数据写入的情况,以 putString
为例子介绍::
1 | public Editor edit() { |
写入内存:
1 | private MemoryCommitResult commitToMemory() { |
整个方法用到了 SharedPreferencesImpl
类锁来同步。
写入磁盘:
1 | private void enqueueDiskWrite(final MemoryCommitResult mcr, |
startReloadIfChangedUnexpectedly
1 | void startReloadIfChangedUnexpectedly() { |
这个方法主要是从新从磁盘上把数据加到到内存中,保存在前面提到的Map中。
1 | private boolean hasFileChangedUnexpectedly() { |
apply 和 commit
前面我们也分析了这两个方法的源码,下面来看一下这两个方法的差异点:
- apply 是没有返回值的,commit 有返回值
- apply 写入文件的操作是异步的,而commit 的写入文件的操作是在当前线程同步执行的
综合性能考虑,如果在主线程操作且不需要返回值的情况下,优先使用 apply 来提交修改。