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方法。
这个方法返回的是SharedPreferenceImpl对象,实际上SP本身只是一个接口,其中定义了get方法和一个Editor内部接口(定义put方法)。
从379来看,首先是从cache中获取sp,如果cache中没有(381),则会去创建一个SPImpl对象(391)。
那么cache又是什么,还是一个Map,保存了整个系统中所有的package下所有的保存SP的map(这里可能有点绕,后面还会再说)。
这个方法返回的是当前包下的存放所有SP的Map。
注意其中的sSharedPrefsCache,这个Map的定义如下:
系统会在内存中保存所有的packagePrefs对象,更进一步的说,整个系统的SP都会读取到内存中并一直占用内存。
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
在391行,如果SP是空的,那么就会去调用SPImpl的构造方法创建一个SPImpl对象。
在SPImpl的构造方法中,调用到了SPImpl内部的一个startLoadFromDisk方法。其内部开启了一个子线程来调用同类中的loadFromDisk方法。
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为例:
所有的get方法都加锁了。而awaitLoadedLocked方法则是等待从磁盘中加载数据完成。
Sp.putXXX:
在put之前先要通过edit()方法获取一个Editor对象,实际上EditorImpl。
以putInt为例(put方法的实现都在SPImpl的EditorImpl中):
同样也是同步方法。
调用到了mModified的put方法,它是一个HashMap,所以put方法并不能保存数据到磁盘中,需要调用apply或者commit才可以。
SP.commit:
该方法是同步提交方法。有返回值,表示提交是否成功
这里面涉及到了一个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的写入磁盘。以此来保证异步操作的
回到commit的580行,继续看enqueueDiskWrite方法:
注意doc,数据写入磁盘的顺序是一次一个,而commit在调用enqueue方法的时候传入的Runnable是null的,在调用writeToFile的时候,不会走647的if。
其内部的writeToDiskWrite子线程需要658行的wasEmpty为真,。这个值会在写入完成后的地645行--,这就是为什么commit会阻塞线程,因为如果当前mDiskWritesInFlight不为1,则会进入到666行,使得当前提交操作进入等待状态(即commit阻塞线程)。
写入文件的操作就不再深入往下了,也是基于XmlUtils这个工具类来完成的。
SP.apply:
apply首先也是调用到了commitToMemory获取到将要写入到磁盘中的值,然后调用enqueueDiskWrite方法,同时传入了一个postWriteRunnable子线程对象,
在enqueueDiskWrite方法中apply执行到了第666行,后面是在线程池中开启一个线程执行writeToDiskRunnable。在writeToDiskRunnable的最后还会执行postWriteRunnable。
SP的错误使用:
1.存放超大体积的Value:
SP只适用于存放简单的体积小的值,不应该存放大体积的Value。
首当其冲的影响是,如果存放了大体积的Value,当初次从一个SP中获取某一值的时候,我们需要先将对应的SP从磁盘加载到内存中,这个过程就会消耗大量的时间,在getXXX方法中,都调用到了awaitLoadedLocked方法,该方法实现如下:
即如果某一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中,有如下过程:
这个循环要执行完成所有的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.
具体原因就看注释吧,这个API只在小于11上面适用,而且,能做的事情,也就是如果sp已经被读取进内存,再次获取这个sp的时候,如果有这个flag,会重新读一遍文件,仅此而已!
参考:
https://blog.csdn.net/offbye/...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。