Android 浅析 SharedPreferences (二) 原理

前言

Linus Benedict Torvalds : RTFSC – Read The Funning Source Code

概括

分析SharedPreferences的获取生成原理和修改原理。并且会看到为什么对于多进程是不安全的。

创建原理

SharedPreferences的获取原理从getSharedPreferences函数开始。所以我们从getSharedPreferences函数为起点开始分析。

ContextImpl

我们知道Context的主要实现从ContextImpl开始,如何传进来的过程忽略。

getSharedPreferences函数返回的是一个SharedPreferences的对象并且是以单例实现的。因为此函数比较重要我们一步步来看。

Step 1.getSharedPreferences()

Part 1.初始化映射对象

ContextImpl 包含了一个比较复杂的内部全局私有变量sSharedPrefs,它的类型是ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>,比较难读,简单点就是从包名映射到preference名字到缓存preferences。
一开始我们先新建一个映射对象,然后获取包名赋予它初值。

Part 2.创建SharedPrefs文件

接下来我们通过一开始传进来的文件名去创建一个SharedPrefs文件,通过getSharedPrefsFile()函数返回一个”.xml”的文件对象。然后就会调用new SharedPreferencesImpl(prefsFile, mode)来创建一个SharedPreferencesImpl对象。

SharedPreferencesImpl

SharedPreferences的其中一个重点在于它的创建,构造函数就是获取它对象的地方。

Step 2.SharedPreferencesImpl()

1
2
3
4
mFile = file;
mBackupFile = makeBackupFile(file);
...
startLoadFromDisk();

构造函数功能是初始化一些变量和调用一些初始函数。
这里有两个变量值得注意,一个是mFile,保存preference的文件对象,另一个是mBackupFile,保存preference的备份文件对象。

首先来看makeBackupFile()函数

Step 3.makeBackupFile()

return new File(prefsFile.getPath() + ".bak");
makeBackupFile从字面意思也很好理解,就是生成一个备份的文件,主要是生成一个prefsFile的备份文件。

初始化的最后是调用startLoadFromDisk()函数进行操作。

Step 4.startLoadFromDisk()

loadFromDiskLocked()
startLoadFromDisk() 的函数功能也很简单,开启一个线程单步调用loadFromDiskLocked()

Step 5.loadFromDiskLocked()

Part 1.查看PrefsFile

loadFromDiskLocked()函数的第一步是查看备份文件是否已经存在,存在则讲原文件删除用备份文件替换。

Part 2.加载PrefsFile

在确认PrefsFile文件可以被读取后会将PrefsFile文件内容加载到一个map里面,确认map不为空后会将它存到全局的mMap里,然后通过notifyAll发出一个通知,通知所有等待初始化完成的线程可以开始运作。

ContextImpl

Step 6.getSharedPreferences()

当创建完一个SharedPreferencesImpl后会将SharedPrefs对象保存到最初的数组里面。最后,如果这不是第一次加载,那么SharedPrefs对象不需要创建,但是会重新调用一次startLoadFromDisk(),让文件保持最新状态。

总结

SharedPreference的创建原理就到此为止了,到此用户就得到了SharedPreference的对象,方便未来的操作。

获取原理

获取的操作都是要在创建SharedPreference之后才可以操作的。

获取的过程我们以获取String为例。

SharedPreferencesImpl

getString()

首先通过SharedPreference对象调用getString方法。此方法有两个参数,一个是key值,也就是我们windows ini配置文件的key值一样,另一个就是默认参数。

首先进来会调用awaitLoadedLocked()来查看加载配置文件是否初始化完成。接着就很简单了,调用全局变量mMap来查找想要的信息。

修改原理

修改的操作都是要在创建SharedPreference之后才可以操作的。

修改的过程我们以修改一个String值为例。

Activity

在我们调用的地方,我们首先通过preference.edit()函数获取SharedPreference的Editor类的对象。

SharedPreferencesImpl

Step 1.edit()

edit()函数返回的是一个新建EditorImpl类的对象。
SharedPreferencesImpl.EditorImpl类里面有一个关键的变量mModified,这是一个Map,它会将以后要修改的值都放到里面去

Activity

Step 2.editor.putString()

这一步很简单,将我们要保存的值存入EditorImpl类里的变量mModified里。

Step 3.editor.commit()

这一步就是调用editor的提交方法了。比较关键。

SharedPreferencesImpl

Step 4.commit()

首先会调用commitToMemory函数返回一个MemoryCommitResult对象。所以我们首先来看下commitToMemory。

Step 5.commitToMemory()

MemoryCommitResult类是一个封装了commitmemory结果的一个类,里面有许多信息。
commitToMemory首先创建一个MemoryCommitResult对象。接着会克隆一个mMap对象。如果有设置监听消息会在这里设置一个值到MemoryCommitResult。接下来我们就会对mMap里面的值进行修改,在修改完成后会把之前我们所进行修改临时保存的全局变量mModified进行清空处理。然后返回出去MemoryCommitResult对象。

在commitToMemory返回一个结果类后会将它当作参数传入enqueueDiskWrite()函数里。

Step 6.enqueueDiskWrite()

enqueueDiskWrite()函数里面执行的是一个异步操作,在外部commit()函数会做一个await操作等待异步的完成。
在这个函数里会开启一个writeToDiskRunnable的线程,该线程做的事情是将传进来的MemoryCommitResult 里的数据写入到文件里。

Step 7.writeToFile()

此函数首先会判断mFile文件是否存在,如果存在就再判断备份文件是否存在,备份文件不在的话就创建一个备份文件,然后删除原文件。
接着创建一个mFile的文件,将数据写入,再添加权限,做完后将备份文件删除。

做完后commit函数的职责就算完了,然后再广播监听者修改完成的消息。

总结

对于修改的原理,在多进程情况下如果两个修改的时机接近那么就很容易导致一方写不进去的问题,是因为会被另一个进程将写入的数据覆盖。这个问题目前的解决方案是不同的进程尽量使用不同的SharedPreference进行存储。