之前一直没有读源码的习惯,直到今年年初才开始慢慢养成多看源码的习惯,不过之前在使用 SharedPreferences 时总觉得为啥要通过 Editor 去写,而在获取写入的值的时候却用 SharedPreferences 直接读出来,这次看过源码之后才知道是为什么。
说到 SharedPreferences,我们肯定想到的是它为我们提供简单数据的存储功能,因为 Android 本身支持多种类型的数据的持久化,比如文件、数据库、SharedPreferences 等等,而如果只是存出一些简单的数据类型,那么 SharedPreferences 是不错的选择,它的存储形式是基于 xml 的,通过键值对的方式保存数据,而且数据可以是私有的。
当然 SharedPreferences 的使用很简单,我们先获取 SharedPreferences 实例,然后通过 Editor 保存和提交数据,而读取的时候可以直接通过 SharedPreferences 的各种 getxxx 的方式读取。
SharedPreferences 本身是接口,里面还有一个 Editor 的接口,它的实现类是 SharedPreferencesImpl,里面实现了 SharedPreferences 各种方法,还是实现了 Editor 接口。
先来说说 SharedPreferences 的获取,有两种方式:
1.通过 PreferenceManager 获取。
2.通过 Context 中的 getSharedPreferences() 获取。
其实第一种也只是封装了一下 Context 中的 getSharedPreferences()。
这里有一点需要注意,我们应该减少 SharedPreferences 的大小,因为它本质是以 xml 存在本地,如果 SharedPreferences 数据量过大,那么初始化 SharedPreferences 时,会减慢读取速度。
这次的源码阅读只阅读了部分,帮我搞清了一些方法,但并没有通读 SharedPreferences 源码,下面就来分享一下我的收获。
先来说说 SharedPreferences 的存储形式,我一开始只是认为 SharedPreferences 将值存入磁盘中,而其实它分为两部分,首先它会先存在内存中一次,然后再提交到磁盘中。
截图中的 mMap 的作用有两个,一个是初始化的时候把磁盘中读到的数据赋值给它,然后我们再次提交的新提交的部分时,也会直接存到这里,这也就是先存到内存中一次。
在 SharedPreferences 构造中,会调用 startLoadFromDisk(),这个方法会新起一个线程,执行 loadFromDisk(),这个方法就是读取磁盘中 xml 文件,然后将读取到的值赋给 mMap。
那么接下来来看看我们在 Editor 中提交的时候,代码都做了什么。
SharedPreferencesImpl 有一个内部类 EditorImpl,它实现了 SharedPreferences 中的定义的 Editor 接口,然后当我们获取 Editor 时,就是得到了一个 EditorImpl 实例。
EditorImpl 中的 mModified 是用来存储本次想要提交的内容,各种 putxxx 这里就不在多说了,看看两个重要的提交方式,一个是 commit(),另一个是 apply()。
官方文档中也明确的说明了这两种方式的明确区别,除了返回值,还有提交到磁盘的方式,commit() 是同步,而 apply() 是异步。
但无论提交到磁盘的方式如何,它们都会首先将本次想要提交的内容通过 commitToMemory() 存到内存中,也就是存到 SharedPreferencesImpl 的 mMap 中。通过 MemoryCommitResult 拿到提交内容的内存的结果,然后再将拿到的结果写入磁盘。
final MemoryCommitResult mcr = commitToMemory();
写到磁盘时,都是通过调用 SharedPreferencesImpl 的 enqueueDiskWrite() 来完成的,apply() 调用 enqueueDiskWrite() 时会传入一个实例化的 Runnable,而 commit() 则传 null 值过去,enqueueDiskWrite() 通过传递过来 Runnable 是否为 null,来判断是否以什么方式提交。
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
final boolean isFromSyncCommit = (postWriteRunnable == null);
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
这就是 enqueueDiskWrite() 的源码,如果传递过来的 postWriteRunnable 为 null 则说明同步写入到磁盘中,而非 null 时利用单线程的线程池来将结果写入到磁盘中。
至此一个基本上写入的流程就完成了。
那些读的时候就直接调用 SharedPreferencesImpl 中的 getxxx 就可以了,而这些方法会线程安全的从 mMap 中拿到我们需要的值。
SharedPreferences 源码基本上就读到了这里,基本的一些操作也都梳理清楚,接下来说几点使用时需要注意的地方。
1.单个 SharedPreferences 文件不宜过大,所以最好不使用默认的存储目录,而是根据需求,自己定义存储的文件名,如果单个 xml 文件过大,那么初始化时会影响读取速度。
2.SharedPreferences 是进程内单例,当然它也可以读到别人进程写入的内容,但是会有些小问题,所以跨进程使用 SharedPreferences 还有很多需要注意的地方。
3.提交时如果不需要返回结果(是否提交成功),那么直接调用 commit()。
4.单次提交内容不易过大过多,那么同步可能会阻塞,虽然也可以异步,但是异步其实也只是单线程。
5.它会同时存在磁盘中,也会存在内存中。
所以是否会存在这种问题?内容不足时,回收了 mMap,下次使用时之前的存过的值会返回 null 或者默认值了呢?
如果真的会的话就要重新再去实例化一个 SharedPreferences 来使用了。
以及内容就是本次阅读心得,仅供参考,如果问题,可以留言沟通。