下面的源码都是基于Android api 31
1、SharedPreference使用
val sharePref = getPreferences(Context.MODE_PRIVATE)
with(sharePref.edit())
{
putBoolean("isLogin", true)putInt("age", 18)apply()
}
val isLogin = sharePref.getBoolean("isLogin", false)
val age = sharePref.getInt("age", -1)
Log.d(TAG, "isLogin $isLogin age $age")
1、获取SharedPreferencesImpl
获取sharedpreferences实现类是ContextImpl,下面是ContextImpl中获取sharedpreferencesImpl的源码
public SharedPreferences getSharedPreferences(String name, int mode) {......File file;synchronized (ContextImpl.class) {//如果mSharedPrefsPaths为null,先创建一个mSharedPrefsPaths的Map。sharePreferences存放的地址if (mSharedPrefsPaths == null) {mSharedPrefsPaths = new ArrayMap<>();}//获取sharedPrefs的filefile = mSharedPrefsPaths.get(name);if (file == null) {//如果file为null,创建filefile = getSharedPreferencesPath(name);mSharedPrefsPaths.put(name, file);}}//通过file获取shareprefreturn getSharedPreferences(file, mode);
}//通过名称来获取file
public File getSharedPreferencesPath(String name) {return makeFilename(getPreferencesDir(), name + ".xml");
}
在contextImpl中是先根据sharedpref的名称来获取对应的File文件,然后通过file来获取sharedprefImpl
public SharedPreferences getSharedPreferences(File file, int mode) {SharedPreferencesImpl sp;synchronized (ContextImpl.class) {//Map中存放file-spImplfinal ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();//先从cache中获取,sp = cache.get(file);if (sp == null) {//创建一个spImplsp = new SharedPreferencesImpl(file, mode);//放入到cache中cache.put(file, sp);return sp;}}if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {//多进程,会执行reloadsp.startReloadIfChangedUnexpectedly();}return sp;
}
由上面代码可知获取sharedPreferencesImpl实例分为两步:
1、通过sp的name来获取存放sp的File。
2、通过file来获取sharedPreferencesImpl实例。
name和file,file和sharedPreferencesImpl在创建过一次后会以key-value的形式保存在map中,方便后面再次获取。所以并不是每次获取spImpl都会去new。
2、SharedPrefrence存储
sp在磁盘中是以xml文件的形式存放的,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<defaultMap><entry><key>key</key><value>true</value></entry> <entry><key>key</key><value>Hello World!</value></entry><entry><key>key</key><value>5</value></entry>
</defaultMap>
3、SharedPreference源码
SharedPreference本身是一个接口实现类是SharedPreferencesImpl。SharedPreferencesImpl构造方法如下:
SharedPreferencesImpl(File file, int mode) {mFile = file;mBackupFile = makeBackupFile(file);mMode = mode;mLoaded = false;mMap = null;mThrowable = null;startLoadFromDisk();
}
SharedPreferencesImpl的构建方法中会先通过传参中的file,创建一个mBackupFile,并且开始从磁盘中获取数据。
private void startLoadFromDisk() {synchronized (mLock) {mLoaded = false;}new Thread("SharedPreferencesImpl-load") {public void run() {loadFromDisk();}}.start();
}
startLoadFromDisk()中会创建一个名称为SharedPreferencesImpl-load的线程来加载磁盘中的数据。并将加载后的数据以key-value的形式放在mMap中。
写入数据
public final class EditorImpl implements Editor {private final Object mEditorLock = new Object();@GuardedBy("mEditorLock")private final Map<String, Object> mModified = new HashMap<>();@GuardedBy("mEditorLock")private boolean mClear = false;@Overridepublic Editor putString(String key, @Nullable String value) {synchronized (mEditorLock) {mModified.put(key, value);return this;}}
}
写入数据时并不是直接往磁盘中写,如上的putString,会先将数据方法到mModified的map中,然后在commit或者apply的时候再进行写磁盘操作。
1、commit()方法
public boolean commit() {long startTime = 0;if (DEBUG) {startTime = System.currentTimeMillis();}MemoryCommitResult mcr = commitToMemory();SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);try {mcr.writtenToDiskLatch.await();} catch (InterruptedException e) {return false;} finally {if (DEBUG) {Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration+ " committed after " + (System.currentTimeMillis() - startTime)+ " ms");}}notifyListeners(mcr);return mcr.writeToDiskResult;
}
在commit方法中会创建一个mcr对象,然后执行enqueueDiskWrite,最后返回mcr.writeToDiskResult。
2、commitToMemory方法
private MemoryCommitResult commitToMemory() {long memoryStateGeneration;boolean keysCleared = false;List<String> keysModified = null;Set<OnSharedPreferenceChangeListener> listeners = null;Map<String, Object> mapToWriteToDisk;synchronized (SharedPreferencesImpl.this.mLock) {if (mDiskWritesInFlight > 0) {//先判断是不是有多个写磁盘的操作,如果有多个写磁盘的操作,先copy mMap的对象。mMap = new HashMap<String, Object>(mMap);}//把map的对象赋值给mapToWriteDisk,mapToWriteToDisk = mMap;mDiskWritesInFlight++;//mDiskWritesInFlight加1synchronized (mEditorLock) {boolean changesMade = false;//判断是否清除sp中的数据if (mClear) {if (!mapToWriteToDisk.isEmpty()) {changesMade = true;mapToWriteToDisk.clear();//如果清除sp中的数据将mapToWirteToDisk的数据清空。}keysCleared = true;mClear = false;}//遍历mModified mapfor (Map.Entry<String, Object> e : mModified.entrySet()) {String k = e.getKey();Object v = e.getValue();////当v==this,或者v为null时,也就是执行了remove()方法的if (v == this || v == null) {if (!mapToWriteToDisk.containsKey(k)) {//如果mapToWriteToDisk中不包含这个k,继续循环continue;}//否则从mapToWriteToDisk中移除这个k-vmapToWriteToDisk.remove(k);} else {if (mapToWriteToDisk.containsKey(k)) {Object existingValue = mapToWriteToDisk.get(k);if (existingValue != null && existingValue.equals(v)) {continue;}//如果mapTopWriteToDisk中的key-value和mModified中的相同不操作}//把k-v方法哦mapToWriteToDisk中mapToWriteToDisk.put(k, v);}changesMade = true;}//清除mModified中的数据mModified.clear();if (changesMade) {//Generation加1mCurrentMemoryStateGeneration++;}memoryStateGeneration = mCurrentMemoryStateGeneration;}return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,listeners, mapToWriteToDisk);}
对应的流程图如下
3、enqueueDiskWrite方法
private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {final boolean isFromSyncCommit = (postWriteRunnable == null);final Runnable writeToDiskRunnable = new Runnable() {@Overridepublic void run() {synchronized (mWritingToDiskLock) {//往磁盘中写writeToFile(mcr, isFromSyncCommit);}synchronized (mLock) {//减1mDiskWritesInFlight--;}if (postWriteRunnable != null) {//运行postWriteRunnablepostWriteRunnable.run();}}};if (isFromSyncCommit) {//如果通过commit提交的直接执行writeToDiskRunnableboolean wasEmpty = false;synchronized (mLock) {wasEmpty = mDiskWritesInFlight == 1;}if (wasEmpty) {writeToDiskRunnable.run();return;}}//先进入队列QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
4、queue代码
由上面可知mDiskWritesInFlight>1,或者是通过sp.apply的方法调用的,会执行到queue方法中
public static void queue(Runnable work, boolean shouldDelay) {Handler handler = getHandler();synchronized (sLock) {sWork.add(work);//添加到work中//shouldDelay和sCanDelay为true时Handler发一个Delayed消息,否则发送一个非延迟消息if (shouldDelay && sCanDelay) {handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);} else {handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);}}
}
上面的handler是通过HandlerThread创建的sHandler,源码如下
private static Handler getHandler() {synchronized (sLock) {if (sHandler == null) {HandlerThread handlerThread = new HandlerThread("queued-work-looper",Process.THREAD_PRIORITY_FOREGROUND);handlerThread.start();sHandler = new QueuedWorkHandler(handlerThread.getLooper());}return sHandler;}
}
由上面流程可以知道,只有执行commit操作并且mDiskWritesInFlight == 1时才会在主线程中执行,卡住主线程,当mDiskWritesInFlight > 1或者apply的时候不是在主线程中的。
private static class QueuedWorkHandler extends Handler {static final int MSG_RUN = 1;QueuedWorkHandler(Looper looper) {super(looper);}public void handleMessage(Message msg) {if (msg.what == MSG_RUN) {processPendingWork();}}
}
5、apply()方法
public void apply() {final long startTime = System.currentTimeMillis();//创建mcr对象final MemoryCommitResult mcr = commitToMemory();final Runnable awaitCommit = new Runnable() {@Overridepublic void run() {try {///创建awaitCommit,用于等待写磁盘完成后调用mcr.writtenToDiskLatch.await();} catch (InterruptedException ignored) {}}};//添加awaitCommitQueuedWork.addFinisher(awaitCommit);Runnable postWriteRunnable = new Runnable() {@Overridepublic void run() {//执行awaitCommitawaitCommit.run();///移除awaitCommitQueuedWork.removeFinisher(awaitCommit);}};//进入队列SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);notifyListeners(mcr);
}
apply方法,首先创建了一个 awaitCommit 的 Runnable,然后加入到 QueuedWork 的 sPendingWorkFinishers 队列中,awaitCommit 中包含了一个等待锁,需要在真正对 SP 进行持久化的时候进行释放。
6、apply方法中为什么要QueuedWork.addFinisher(awaitCommit)?
先要了解QueuedWork.waitToFinish() 方法,QueuedWork.waitToFinish() 会等待所有的addFinisher的runnable执行完成后才会进行执行。在ActivityThread中下面的方法都会执行QueuedWork.waitToFinish()方法。
- handleServiceArgs -> Service.onStartCommand
- handleStopService -> Service.onDestroy
- handlePauseActivity -> Activity.onPause
- handleStopActivity -> Activity.onStop
上面的问题转换成了为什么ActivityThread中上面的方法为什么都要等待sharePref的apply方法执行完写磁盘后才能被调用。
应用可能被系统回收、被用户杀死,会Crash,这些都是不确定的。apply 提交的任务,是先放到队列中去依次执行,而不是立即执行,意味着可能该任务还没被执行,app就被杀死。所以为了尽可能保证数据能被持久化,就要找到一些重要的时机去卡住,来保证完成写入。
7、获取数据
public String getString(String key, @Nullable String defValue) {synchronized (mLock) {awaitLoadedLocked();String v = (String)mMap.get(key);return v != null ? v : defValue;}
}
获取数据时先要等数据从磁盘中加载完成,所以会先执行awaitLoadedLocked();方法,这里面的会通过mLoaded判断数据是否已经从磁盘中加载完成,如果没有加载完成则会等待。
private void awaitLoadedLocked() {while (!mLoaded) {try {mLock.wait();} catch (InterruptedException unused) {}}if (mThrowable != null) {throw new IllegalStateException(mThrowable);}
}
4、疑问
1、执行put不执行apply或者commit,再获取这个值能获取到吗?
val sharePref = getPreferences(Context.MODE_PRIVATE)
with(sharePref.edit())
{
putBoolean("isLogin", true)putInt("age", 18)val age = sharePref.getInt("age", -1)Log.d(TAG, "----before apply--- age $age")apply()val age1 = sharePref.getInt("age", -1)Log.d(TAG, "----after apply--- age $age1")
}
打印的log如下,在执行apply之前获取到的age是默认值-1,执行apply后获取的值是set的值18.
----before apply--- age -1----after apply--- age 18
5、SharedPreferences跨进程
在使用SharedPreference 时,有如下一些模式: MODE_PRIVATE
私有模式,这是最常见的模式,一般情况下都使用该模式。 MODE_WORLD_READABLE
,MODE_WORLD_WRITEABLE
,文件开放读写权限,不安全,已经被废弃了,google建议使用FileProvider
共享文件。 MODE_MULTI_PROCESS
,跨进程模式,如果项目有多个进程使用同一个Preference,需要使用该模式,但是也已经废弃了。使用contentProvider可以实现sp的跨进程,
参考:https://www.jianshu.com/p/875d13458538
6、SharedPreferences ANR
使用sp的项目中经常会出现下面的ANR
"main" prio=5 tid=1 Waiting| group="main" sCount=1 dsCount=0 obj=0x73f50268 self=0xefc05400| sysTid=13726 nice=0 cgrp=default sched=0/0 handle=0xf30cf534| state=S schedstat=( 1450815451 10991027618 7548 ) utm=40 stm=105 core=0 HZ=100| stack=0xff1f4000-0xff1f6000 stackSize=8MB| held mutexes=at java.lang.Object.wait!(Object.java)- waiting on <0x04386712> (a java.lang.Object)at java.lang.Thread.parkFor$(Thread.java:2127)- locked <0x04386712> (a java.lang.Object)at sun.misc.Unsafe.park(Unsafe.java:325)at java.util.concurrent.locks.LockSupport.park(LockSupport.java:161)at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:840)at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:994)at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1303)at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:203)at android.app.SharedPreferencesImpl$EditorImpl$ 1. run(SharedPreferencesImpl.java: 366 ) at android.app.QueuedWork.waitToFinish(QueuedWork.java:88)at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:3400)at android.app.ActivityThread.-wrap21(ActivityThread.java:-1)at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1632)at android.os.Handler.dispatchMessage(Handler.java:110)at android.os.Looper.loop(Looper.java:203)at android.app.ActivityThread.main(ActivityThread.java:6251)at java.lang.reflect.Method.invoke!(Method.java)at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1063)at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:924)
1、sp引起ANR的原因
由上面日志可以看出main线程在等待锁导致的ANR,具体原因是ActivityThread.handleServiceArgs,时执行QueueWork.waitToFinish。ActivityThread中handleServiceArgs的代码如下:
private void handleServiceArgs(ServiceArgsData data) {Service s = mServices.get(data.token);if (s != null) {try {if (data.args != null) {data.args.setExtrasClassLoader(s.getClassLoader());data.args.prepareToEnterProcess();}int res;if (!data.taskRemoved) {res = s.onStartCommand(data.args, data.flags, data.startId);} else {s.onTaskRemoved(data.args);res = Service.START_TASK_REMOVED_COMPLETE;}//QueuedWork.waitToFinishQueuedWork.waitToFinish();try {ActivityManager.getService().serviceDoneExecuting(data.token, SERVICE_DONE_EXECUTING_START, data.startId, res);} catch (RemoteException e) {throw e.rethrowFromSystemServer();}} catch (Exception e) {......}}
}
如上代码在handleServiceArgs中执行了QueueWork.waitFinish(),waitFinish()的源码如下:
public static void waitToFinish() {......try {//循环获取sFinishers队列中的任务,当任务为null时退出while (true) {Runnable finisher;synchronized (sLock) {finisher = sFinishers.poll();}if (finisher == null) {break;}//执行任务finisher.run();}} finally {sCanDelay = true;}......
}
在handleServiceArgs时执行QueueWork.waitFinish(),会等待QueueWork中的任务执行完成。在SharedPreferencesImpl的apply方法中如下:
public void apply() {final MemoryCommitResult mcr = commitToMemory();final Runnable awaitCommit = new Runnable() {@Overridepublic void run() {try {mcr.writtenToDiskLatch.await();} catch (InterruptedException ignored) {}}};//添加awaitQueuedWork.addFinisher(awaitCommit);//写磁盘后再调用Runnable postWriteRunnable = new Runnable() {@Overridepublic void run() {awaitCommit.run();QueuedWork.removeFinisher(awaitCommit);}};//添加到队列SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);notifyListeners(mcr);
}
postWriteRunnable执行时会调用awaitCommit.run();这里会执行mcr.writtenToDiskLatch.await();等待写磁盘完成,再调用,所以QueuedWork.removeFinisher(awaitCommit);是在写磁盘完成后移除的。
ActivityThread 中,下面的方法中都调用了QueuedWork.waitToFinish() :
- handleServiceArgs -> Service.onStartCommand
- handleStopService -> Service.onDestroy
- handlePauseActivity -> Activity.onPause
- handleStopActivity -> Activity.onStop
以上四处都存在着 SP 的等待锁问题所导致的 ANR 风险。
2、解决方法
1、8.0及以下版本
由于sPendingWorkFinishers是QueuedWork的静态集合对象,而且ConcurrentLinkedQueue这个集合类是可以继承的,所以可以直接重新定义一个集合类继承自ConcurrentLinkedQueue,覆盖ConcurrentLinkedQueue集合在QueuedWork中暴露出来的接口,将poll接口的返回值改为固定返回null,用这个自定义的集合动态代理之前的集合,这个时候ActivityThread的H在处理消息的时候就再也不用执行等待行为了。
如下方式hook住QueueWork替换sPendingWorkFinishers为ConcurrentLinkedQueueProxy
public static void replaceQueueWorkPendingWorkFinishers() {Log.d(TAG, "start hook, time stamp = " + System.currentTimeMillis());ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers = null;Field field = null;try {Class<?> atClass = Class.forName("android.app.QueuedWork");field = atClass.getDeclaredField("sPendingWorkFinishers");field.setAccessible(true);sPendingWorkFinishers = (ConcurrentLinkedQueue<Runnable>) field.get(null);if (sPendingWorkFinishers != null) {field.set(null, new ConcurrentLinkedQueueProxy<Runnable>(sPendingWorkFinishers));Log.d(TAG, "Below android 0,replaceQueueWorkPendingWorkFinishers success.");}} catch (Exception e) {// 出现异常try {if (sPendingWorkFinishers != null && field != null) {field.set(null, sPendingWorkFinishers);}} catch (Exception ex) {//ignore}Log.e(TAG, "Below android 0,,hook sPendingWorkFinishers fail.", e);}Log.d(TAG, "end hook, time stamp = " + System.currentTimeMillis());
}
7、SharedPreferences替代方案
MMKV
|len|key|len|value||len|key|len|value||len|key|len|value|...
文件中key value紧密排布。另有一个crc文件。
MMKV写策略是,增量kv对象序列化后,直接append到内存末尾。这样同一个key会有新旧若干份数据,最新的数据在最后。
不断 append 的话,文件大小会增长得不可控。例如同一个 key 不断更新的话,是可能耗尽几百 M 甚至上 G 空间,而事实上整个 kv 文件就这一个 key,不到 1k 空间就存得下。这明显是不可取的。因此MMKV在性能和空间上做了折中:以内存 pagesize 为单位申请空间,在空间用尽之前都是 append 模式;当 append 到文件末尾时,进行文件重整、key 排重,尝试序列化保存排重结果;排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。
而这个重写策略,就是直接将内存中的hash表取出kv逐个写入文件,直接丢弃原先的文件内容。
存在问题:
- 原先一直没有改变的值也需要跟着一起重新写入。
- 由于写入的是同一个文件路径,在这期间如果重写中断或者失败,就会导致内容丢失。
- append内容的时候如果被中断,会导致脏数据。
- 多进程情况下,一旦有进程更新文件,另一进程在访问KV的时候就必须要校验crc,如不一致则需重新载入kv来构建新的hash表。
- 无类型信息。一方面,无法根据已有文件来追踪所存内容。另一方面,这将导致已经迁移至MMKV的数据后续无法再迁移到其他方案,因为无法取出每个值的完整类型信息。
- 每新开一个kv仓库,都需要新增一个fd,如果该仓库要支持多进程,需要再增加一个fd,从而容易导致fd超限问题。
链接:https://github.com/Tencent/MMKV
参考
1、https://stackoverflow.com/questions/45358761/how-to-save-variable-value-even-if-app-is-destroyed