之前都是使用 SharedPreference 来做一些基本的保存工作,因为都是在同一进程下使用,所以也没有遇到过什么问题,这次偶然间需要在多进程下使用,结果发现在读取时会存在读取不到的问题,因此去看看了源码,找到了问题原因和解决方式,也对 SharedPreference 有了更深的理解,特此记录一下~
获取 SharedPreference
通常我们都是通过 Context.getSharedPreferences()
来获取 SharedPreference 对象,这个 Context 无论是 Application、 Service 或是 Activity,都是继承自 ContextWrapper,通过查看 ContextWrapper 源码可以看发现内部都是调用了 mBase 的相关方法,而这个 mBase 就是 ContextImpl。getSharedPreferences()
在 ContextImpl 有两个重载方法
1 | public SharedPreferences getSharedPreferences(String name, int mode) { |
这个是我们常用的,其中 name 为文件名称,也就是生成后保存在 data 目录下的 xml 文件名称,mode 为操作模式,通常我们传入的都是 Context.MODE_PRIVATE
,这里只是去获取 file 文件,然后调用 getSharedPreferences(file, mode)
方法
1 | public SharedPreferences getSharedPreferences(File file, int mode) { |
可以看到,在获取 SharedPreference 时,系统会其做一个缓存,因此不会每次都去 新建一个出来,减少了不必要的开销,这里要特别注意最后一段,当 mode 为Context.MODE_MULTI_PROCESS
或是 Android 版本低于 Android 3.0时,会去执行 startReloadIfChangedUnexpectedly()
方法,这个地方就是在多进程下可以使用的原因,后续再说。
get
调用 SharedPreference 的各种 get 方法,其实是从内存中去拿数据,这里以 getString()
为例
1 | public String getString(String key, @Nullable String defValue) { |
其中 awaitLoadedLocked()
的作用保证数据已经从文件中加载到内存中,mLoaded
在 loadFromDisk()
中加载完成后,即 Map 被赋值后被置为 true
1 | private void awaitLoadedLocked() { |
commit 和 apply
在获取到 SharedPreference 后,要想保存数据,必须要调用 edit()
方法来获取一个 Editor 对象,Editor 是 SharedPreference 内的一个接口,提供了所有的 put、提交以及清除的方法,它的实现是 EditorImpl。commit()
方法带有一个布尔的返回值,用来返回是否成功将提交写入文件中,而且是同步写入,因此如果要写入的数据过大,会造成线程阻塞,apply()
方法没有返回值,用异步方式写入,也是比较推荐的一种用法。
1 | public boolean commit() { |
1 | public void apply() { |
可以看到两个方法中都是先通过调用 commitToMemory()
获取到了一个 MemoryCommitResult 对象,commitToMemory()
主要是将 Editor 中的更改添加到 SharedPreference 的缓存 Map 中去,如果调用了 Clear()
,则会去对 Map 做清空操作,接着会遍历更改写入到 Map 中,最后返回一个 MemoryCommitResult 对象
1 | return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,mapToWriteToDisk); |
其中 memoryStateGeneration 是一个长整型,用来记录当前内存的状态,会在每次修改后加一,keysModified 是所有更改的 key 值,listeners 是通过registerOnSharedPreferenceChangeListener()
注册的 Listener 集合,mapToWriteToDisk是修改后需要写入磁盘的 Map。在获取到 MemoryCommitResult 后,会将其传入 SharedPreference 的 enqueueDiskWrite()
方法中
1 | private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) { |
这里要注意的是 mDiskWritesInFlight 是在 commitToMemory()
做加一操作,因此,如果传入的 postWriteRunnable 为空,则 wasEmpty 肯定为true,因此 commit()
方法会同步写入,否则会将 postWriteRunnable 传入到 QueuedWork.queue()
中去
1 | LinkedList<Runnable> sWork = new LinkedList<>(); |
在 queue ()
中会通过 getHandler 来获取到一个 Handler,然后通过这个Handler发送一条消息,其实这里就是 apply()
是异步写入的关键,通过查看 getHandler()
代码,发现里面就是通过 HandlerThread 来获取这个 Handler,因此也完成了线程的切换
1 | private static Handler getHandler() { |
Mode
Context.MODE_PRIVATE
表明只能被当前应用读写或是分享同一 user ID 的所有应用读写Context.MODE_MULTI_PROCESS
已被标记为弃用,随时可能会被移除,如果在多进程下官方推荐使用ContentProvider
来进行数据共享
多进程中使用
首先来分析下为什么会出现获取数据为空的情况,之前在看 Context.getSharedPreferences()
时,可以看到 Context 会对 SharedPreference 做一个缓存,即只会在第一次获取时才会新创建对象,因此,对应的 SharedPreference 构造函数中的 startLoadFromDisk()
也只有在第一次才会调用,那么,问题就来了,当你在主进程中添加或修改了数据,而在进程2中已经获取过对应的SharedPreference,这时在进程2中去调用 get 方法,因为进程2内存中保存的 Map 中数据并未更改,所以返回空数据或旧数据。
而当我们把 mode 改为 Context.MODE_MULTI_PROCESS
时为什么就可以获取到正确的数据了呢?主要原因就在 getSharedPreferences()
中,当 mode Context.MODE_MULTI_PROCESS 时,会调用下面这个方法来重新从文件中读取数据
1 | void startReloadIfChangedUnexpectedly() { |
因此,这里主要是强制在每次调用 getSharedPreferences()
时都去从文件中重新加载一边,保证此时内存中的数据是最新的。
总结
在源码中可以发现官方已经将 Context.MODE_MULTI_PROCESS
标记为弃用,而且极力推荐使用 ContentProvider
来进行进程间的数据共享,因此使用这个 mode 来在进程下使用 SharedPreference 是不安全的,但有时我们只是需要存储一些简单的数据,用 ContentProvider
好像又有点过于繁琐了,所以我觉得,也需要视情况而定,如果你能保证自己的数据量不大,且使用不是很频繁,那么使用这个 mode 也不失为一个办法。另外,在多进程下想安全的像使用 SharedPreference 来保存和读取数据,不妨试试腾讯开源的 MMKV。