【Android】SharedPreferences 源码解析


本文是独立解析源码的第二篇,SharedPreference 是一个 Android 开发自带的适合保存轻量级数据的 K-V 存储库,它使用了 XML 的方式来存储数据,比如我就经常用它保存一些如用户登录信息等轻量级数据。那么今天就让我们来分析一下它的源码,研究一下其内部实现。

获取SharedPreferences

我们在使用 SharedPreferences 时首先是需要获取到这个 SharedPreferences 的,因此我们首先从 SharedPreferences 的获取入手,来分析其源码。

根据名称获取 SP

不论是在 Activity 中调用 getPreferences() 方法还是调用 Context 的 getSharedPreferences 方法,最终都是调用到了 ContextImpl 的 getSharedPreferences(String name, int mode) 方法。我们先看看它的代码:

可以看到,它首先对 Android 4.4 以下的设备做了特殊处理,之后将对 mSharedPrefsPaths 的操作加了锁。mSharedPrefsPaths 的声明如下:

private ArrayMap<String, File> mSharedPrefsPaths;

可以看到它是一个以 name 为 key,name 对应的 File 为 value 的 HashMap。首先调用了 getSharedPreferencesPath 方法构建出了 name 对应的 File,将其放入 map 后再调用了 getSharedPreferences(File file, int mode) 方法。

获取 SP 名称对应的 File 对象

我们先看看是如何构建出 name 对应的 File 的。

可以看到,调用了 makeFilename 方法来创建一个名为 name.xml 的 File。makeFilename 中仅仅是做了一些判断,之后 new 出了这个 File 对象并返回。

可以看到,SharedPreference 确实是使用 xml 来保存其中的 K-V 数据的,而具体存储的路径我们这里就不再关心了,有兴趣的可以点进去看看。

根据创建的 File 对象获取 SP

我们接着看到获取到 File 并放入 Map 后调用的 getSharedPreferences(file, mode) 方法:

首先可以看到注释 1 处,这里调用了 getSharedPreferencesCacheLocked 来获取到了一个 ArrayMap<File, SharedPreferencesImpl>,之后再从这个 Map 中尝试获取到对应的 SharedPreferencesImpl 实现类(简称 SPI)。

之后看到注释 2 处,当获取不到对应 SPI 时,再创建一个对应的 SPI,并将其加入这个 ArrayMap 中。

这里很明显是一个缓存机制的实现,以加快之后获取 SP 的速度,同时可以发现,SP 其实只是一个接口,而 SPI 才是其具体的实现类。

缓存机制

那么我们先来看看其缓存机制,进入 getSharedPreferencesCacheLocked 方法:

显然,这里有个全局的 ArrayMap:sSharedPrefsCache。

它是一个 ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> 类型的 Map,而从代码中可以看出,它是根据 PackageName 来保存不同的 SP 缓存 Map 的,通过这样的方式,就保证了不同 PackageName 中相同 name 的 SP 从缓存中0拿到的数据是不同的。

SharedPreferencesImpl

那么终于到了我们 SPI 的创建了,在 cache 中找不到对应的 SPI 时,就会 new 出一个 SPI,看看它的构造函数:

可以看到注释 1 处它调用了 makeBackupFile 来进行备份文件的创建。

之后在注释 2 处则调用了 startLoadFromDisk 来开始从 Disk 载入信息。

首先我们看看 makeBackupFile 方法:

很简单,返回了一个同目录下的后缀名为 .bak 的同名文件对象。

从 Disk 加载数据

接着,我们看看 startLoadFromDisk 方法:

可以看到,首先在加锁的情况下对 mLoaded 进行了修改,之后则开了个名为「SharedPreferencesImpl-load」的线程来加载数据。

我们看到 loadFromDisk 方法:

代码比较长,我们慢慢分析

首先在注释 1 处,如果已经加载过,则不再进行加载,之后又开始判断是否存在备份文件,若存在则直接将备份文件直接修改为数据文件 ${name}.xml。

之后在注释 2 处,通过 XmlUtils 将 xml 中的数据读取为一个 Map。由于本文主要是对 SP 的大致流程的解读,因此关于 XML 文件的具体读取部分,有兴趣的读者可以自己进入源码研究。

之后在注释 3 处,进行了一些收尾处理,将 mLoaded 置为 true,并对 mMap 进行了判空处理,以保证在 xml 没有数据的情况下其仍不为 null,最后释放了这个读取的锁,表示读取成功。

编辑 SharedPreferences

我们都知道,真正对 SP 的操作其实都是在 Editor 中的,它其实是一个接口,具体的实现类为 EditorImpl。让我们先看看 Editor 的获取:

获取 Editor

看到 SharedPreferencesImpl 的 edit 方法:

可以看到,这里首先先调用了 awaitLoadedLocked() 方法来等待读取的完成,当读取完成后才会真正创建并返回 EditorImpl 对象。

等待读取机制

由于读取过程是一个异步的过程,很有可能导致读取还没结束,我们就开始编辑,因此这里用到了一个 awaitLoadedLocked 方法来阻塞线程,直到读取过程完成,下面我们可以先看看 awaitLoadedLocked 方法:

可以看到,这里会阻塞直到 mLoaded 为 true,这样就保证了该方法后的方法都会在读取操作进行后执行。

EditorImpl

前面我们提到了 Edit 的真正实现类是 EditorImpl,它其实是 SPI 的一个内部类。它内部维护了一个Map<String, Object>: mModified,通过 mModified 来存放对 SP 进行的操作,此时还不会提交到 SPI 中的 mMap,我们做的操作都是在改变 mModified。

下面列出一些 EditorImpl 对外提供的修改接口,其实都是在对 mModified 这个 Map 进行修改,具体代码就不再讲解,比较简单:

提交 SharedPreferences

提交本来可以放到编辑中的,但因为它才是重头戏,因此我们单独拎出来讲一下。

我们都知道 SP 的提交有两种方式——apply 和 commit。下面我们来分别分析:

apply

首先看到注释 1 处,调用了 commitToMemory 方法,它内部就是将原先读取进来的 mMap 与刚刚修改过的 mModified 进行合并,并存储于返回的 MemoryCommitResult mcr中

而在注释 2 处,调用了 enqueueDiskWrite 方法,传入了之前构造的 Runnable 对象,这里的目的是进行一个异步的写操作。

前面提到的两个方法,我们放到后面分析。

也就是说,apply 方法会将数据先提交到内存,再开启一个异步过程来将数据写入硬盘

commit

看到注释 1 处,可以发现,同样调用了 commitToMemory 方法进行了合并。

之后看到 2 处,同样调用了 enqueueDiskWrite 方法,不过传入的第二个不再是 Runnable 方法。这里提一下,如果 enqueueDiskWrite 方法传入的第二个参数为 null,则会在当前线程执行写入操作。

也就是说,commit 方法会将数据先提交到内存,但之后则是一个同步的过程写入硬盘

同步数据至内存

下面我们来看看两个方法中都调用了的 commitToMemory 的具体实现:

可以看到,具体的代码就如同我们之前所说的一样,将 mMap 的数据与 mModified 的数据进行了整合,之后将 mModified 重新清空。最后将合并的数据放入了 MemoryCommitResult 中。

写入数据至硬盘

我们同样看到 apply 和 commit 都调用了的方法 enqueueDiskWrite:

可以看到 1 处,若第二个 Runnable 为 null 的话,则会将 isFromSyncCommit 置为 true,也就是写入会是一个同步的过程。之后在注释 2 处便进行了同步写入。否则会构造一个 Runnable 来提供给 QueueWork 进行异步写入。

QueueWork 类内部维护了一个 single 线程池,这样可以达到我们异步写入的目的。

而 writeToFile 方法中其实就是又调用了之前的 XmlUtils 来进行 XML 的写入。

总结

SharedPreferences 其实就是一个用使用 XML 进行保存的 K-V 存储库。

在获取 SP 时会进行数据的加载,将 name 对应的 xml 文件以 Map 的形式读入到内存。

而 SP 的编辑操作其实都是在 Editor 内实现,它内部维护了一个新的 Map,所有的编辑操作其实都是在操作这个 Map,只有提交时才会与之前读取的数据进行合并。

其提交分为两种,apply 和 commit,它们的特性如下

  • apply
    • 会将数据先提交到内存,再开启一个异步过程来将数据写入硬盘
    • 返回值时可能写入操作还没有结束
    • 写入失败时不会有任何提示
  • commit
    • 会将数据先提交到内存,但之后则是一个同步的过程写入硬盘

    • 写入操作结束后才会返回值

    • 写入失败会有提示

因此,当我们对写入的结果不那么关心的情况下,可以使用 apply 进行异步写入,而当我们对写入结果十分关心且提交后有后续操作的话最好使用 commit 来进行同步写入。


Android Developer in GDUT