SharedPreference解析:

SharedPreference作为一个我们在应用开发中非常常用的数据存储手段,之前都是想都不想的就直接使用,并没有对其实现原理做过探究,刚好昨天面试的时候问到了这个,于是就参看了一些博客和源码,学习一下。

首先是SharedPreference一些基础相关的东西。

SharedPreference是Android中的轻量级的数据存储方式,能够保存一些简单的数据类型,不适合保存大对象(想起大一的时候曾经用SP来保存过图片文件就很尴尬)。

其内部使用的是key-value的方式进行存储,以XML格式的结构保存在/data/data/packageName/shared_prefs文件夹下面。

使用方式可以概括为一下几个步骤:

获取SP,获取editor,putValue,apply/commit。

如果是get操作则直接获取到SP然后get即可。

获取SP:

SharedPreferences testSp = getSharedPreferences("test_sp", Context.MODE_PRIVATE);

开始的开始都是从getSP开始的,这个方法最终调用到的是ContextImpl的getSP方法。

sp1

这个方法返回的是SharedPreferenceImpl对象,实际上SP本身只是一个接口,其中定义了get方法和一个Editor内部接口(定义put方法)。

从379来看,首先是从cache中获取sp,如果cache中没有(381),则会去创建一个SPImpl对象(391)。

那么cache又是什么,还是一个Map,保存了整个系统中所有的package下所有的保存SP的map(这里可能有点绕,后面还会再说)。

sp2

这个方法返回的是当前包下的存放所有SP的Map。

注意其中的sSharedPrefsCache,这个Map的定义如下:

系统会在内存中保存所有的packagePrefs对象,更进一步的说,整个系统的SP都会读取到内存中并一直占用内存。

 private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

在391行,如果SP是空的,那么就会去调用SPImpl的构造方法创建一个SPImpl对象。

在SPImpl的构造方法中,调用到了SPImpl内部的一个startLoadFromDisk方法。其内部开启了一个子线程来调用同类中的loadFromDisk方法。

sp3

132行中的if表示,如果备份文件存在,就表示上一次的保存没有完成或者失败,然后直接利用备份文件进行操作即可。

143行中的Map就是一个SP文件本身的map结构,在151到153中,通过XmlUtils这个工具类来将对应的XML文件解析成map。

然后在177中将map赋值给mMap。

而mMap就是Map<String, Object> mMap。

在188行finally中,唤醒了所有在等待获取SP的线程。

于是至此,就完成了将SP文件从磁盘加载到内存中的操作,下面就先来看下SP的get操作。

Sp.getXXX:

以getString为例:

sp4

所有的get方法都加锁了。而awaitLoadedLocked方法则是等待从磁盘中加载数据完成。

Sp.putXXX:

在put之前先要通过edit()方法获取一个Editor对象,实际上EditorImpl。

以putInt为例(put方法的实现都在SPImpl的EditorImpl中):

sp5

同样也是同步方法。

调用到了mModified的put方法,它是一个HashMap,所以put方法并不能保存数据到磁盘中,需要调用apply或者commit才可以。

SP.commit:

该方法是同步提交方法。有返回值,表示提交是否成功

sp6

这里面涉及到了一个MCR对象,它保存的是当前要提交的值(就是历次put到mModified中的)等信息(commit的returnValue也在其中),而在commitToMemory中,完成了对mModified的遍历,将其中的kv取出保存到一个变量mapToWriteToDisk中,最后在该方法的return中调用MCR的构造方法,将所有要提交的kv保存到MCR对象中。

在commitToMemory方法中,我们需要注意下面这一点:

第503行mDiskWritesInFlight可以理解为它表示的是当前有多少次写入磁盘的申请,这个值大于0可以理解为已经有在其它地方调用到了commitToMemory,只是还没有将这个mMap的值写入到磁盘中。

因为apply是个异步写入磁盘的过程,如果已经调用过一次commitToMemory,但还没真正写入磁盘,再调用commitToMemory时,mDiskWritesInFlight等于1,需要再拷贝一份mMap,这样前后两次要准备写入磁盘的mapToWriteToDisk是两个不同的内存对象,后一次调用commitToMemory时,在更新mMap中的值时不会影响前一次的mapToWriteToDisk的写入磁盘。以此来保证异步操作的

sp10

sp7

回到commit的580行,继续看enqueueDiskWrite方法:

sp8

注意doc,数据写入磁盘的顺序是一次一个,而commit在调用enqueue方法的时候传入的Runnable是null的,在调用writeToFile的时候,不会走647的if。

其内部的writeToDiskWrite子线程需要658行的wasEmpty为真,。这个值会在写入完成后的地645行--,这就是为什么commit会阻塞线程,因为如果当前mDiskWritesInFlight不为1,则会进入到666行,使得当前提交操作进入等待状态(即commit阻塞线程)。

写入文件的操作就不再深入往下了,也是基于XmlUtils这个工具类来完成的。

SP.apply:

sp9

apply首先也是调用到了commitToMemory获取到将要写入到磁盘中的值,然后调用enqueueDiskWrite方法,同时传入了一个postWriteRunnable子线程对象,

在enqueueDiskWrite方法中apply执行到了第666行,后面是在线程池中开启一个线程执行writeToDiskRunnable。在writeToDiskRunnable的最后还会执行postWriteRunnable。

SP的错误使用:

1.存放超大体积的Value:

SP只适用于存放简单的体积小的值,不应该存放大体积的Value。

首当其冲的影响是,如果存放了大体积的Value,当初次从一个SP中获取某一值的时候,我们需要先将对应的SP从磁盘加载到内存中,这个过程就会消耗大量的时间,在getXXX方法中,都调用到了awaitLoadedLocked方法,该方法实现如下:

sp10

即如果某一SP从磁盘加载到内存和get方法中间时间间隔很短仍然是会阻塞主线程的(即便getSP是开启了一个子线程,我们的get也必须等到子线程完了才行),因为getXX必须等到getSP完全执行完了才能继续,

除了这一点,还会引起的,大对象操作使得我们的程序产生了大量的临时大对象,这些会造成频繁的GC,也会造成应用卡顿。

从前面我们也可以看到,其实SP中的Key和Value是始终在内存中的(当它们加载完成后),这一点可以看图片2中的代码,我们不应该让这些超大Value占用着内存不放。

2.存放Json和HTML这种格式的数据:

不是不可以,只是在因为这些格式的数据在存放到SP中的时候,我们可能要进行大量的转义,会引起大量的字符操作开销以及产生大量的转义符,而SP在存放这些符号的时候又要进行特殊的处理。整个处理的过程会有大量的函数开销,完全没必要。

3.多次edit多次apply:

除了edit会创建editor对象外,apply虽然是在子线程中完成提交操作的,但是多次提交也会造成卡顿,首先是writeToDisk是在单线程执行的,其它的apply必须等待到上一次调用writeToDisk执行完了才可以执行。

其次是在apply方法的第473行有这样一处代码:

QueuedWork.addFinisher(awaitCommit);

这个带有await的子线程(等待提交)被加入到了一个QueueWork中,而在Activity调用onStop的时候(最终调用到了ActivityThread的handleStopActivity方法),该方法里面有这样一处代码:

if (!r.isPreHoneycomb()) {
    QueuedWork.waitToFinish();
}

在waitToFinish中,有如下过程:

sp12

这个循环要执行完成所有的sFinishers中的Runnable才能结束,而我们最初的addFinisher就往里面add了那个awaitCommit的Runnable。

如果说在Activity进行Stop的时候,写入数据到SP已经完成了,不会有任何问题,但是如果写入数据没有完成,我们的Activity必须等待finishers全都完成任务,才能继续执行onStop。

commit的情况就不必要分析了,毕竟这个操作是直接堵塞主线程的。

4.跨进程:

最后一点就是用SP做跨进程通信,SP本身就已经说明了不支持,就不要勉强了。

Note: This class does not support use across multiple processes.

sp13

具体原因就看注释吧,这个API只在小于11上面适用,而且,能做的事情,也就是如果sp已经被读取进内存,再次获取这个sp的时候,如果有这个flag,会重新读一遍文件,仅此而已!


参考:

https://blog.csdn.net/offbye/...

https://blog.csdn.net/yueqian...

https://www.jianshu.com/p/f72...


一天八升水
4 声望1 粉丝

计算机本科在读