本文档属于学习时总结,如有大佬发现问题请一定评论纠错改正
在Android的使用以及学习中,轻量化的持久存储是绝对必不可少的,同时ShardPreference也是很多人从刚开始用到现在的存储类,可以说是老少都用过的东西,那么这个东西真的就没有问题?
答案是否定的,因为谷歌甚至都想舍弃这个东西,从而在Jetpack全家桶里面发行了DataStore,当然我知道很多人都在使用MMKV,当然了,这里我也推荐使用MMKV或者DataStore,因为这两者是真的好用。
回到正题,要知道SP存在的问题,那么就要去阅读SP的源码,让我们从源码一步步分析
SharedPreferences preferences = getSharedPreferences("hello",MODE_PRIVATE);
上面的代码就是我们日常使用的SP最常用的方式了,首先我们不使用任何封装,那么这就是最简单的获取SP示例的方式,然后我们点击 Context.getSharedPreferences() 进一步了解源码。
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
我们发现是mBase调用的,那么这个mBase是一个什么东西呢,是一个context对象,因为我们看到的源码其实是ContextWrapper这个包装类,所以我们应该继续点击get方法,结果跑到了一个抽象方法,然后不知所措? 好了源码阅读到此结束 哈哈开个玩笑,我们知道SP其实是一个接口,那么接口肯定就有其实现类,由于接口以及实现类的命名规范我们不难找到 SharedPreferencesImpl (注意,新版SDK可能没有此方法了,查看源码的版本是SDK30,本类的所有方法被打上了灰名单),不过不影响我们看源码,实现貌似写进了ContextImpl里,阅读源码总是先看静态方法,没有则看我们调用的方法,那么问题就来咯,构造函数如下:
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
//此时调用了一下方法,通过名字我们不难看出,
//就是加载硬盘的文件,那么这个File其实就是我们的SP存的XML
}
private void startLoadFromDisk() {
synchronized (mLock) {//此时同步锁住了变量判断是否加载了
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
//下面就是加载的过程 其实很简单 就是把XML中数据都加载到Map里面 但是XML文件因为标签的原因
//所以有很多冗余的东西
private void loadFromDisk() {
...
Map<String, Object> map = null;
StructStat stat = null;
Throwable thrown = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
...
经过简单的源码分析我们得出,SP在初始化的时候就是去加载文件到一个Map对象中,并且是异步的,理论上是没有什么问题的,那么接下来的操作如果使用不当,则会出问题。
如果我们要应用一个场景,那就是在Splash去判断,如果用户是第一次打开,则展示隐私条款,如果不是第一次打开则展示广告,那么代码可能是这样的
SharedPreferences preferences = getSharedPreferences("hello",MODE_PRIVATE);
boolean isFirst = preferences.getBoolean("isFirst",false);
if(isFirst){}else{}
代码没问题,单独这么写也没问题,那么我们点击进去看一下源码,你会发现:
@Override
public boolean getBoolean(String key, boolean defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Boolean v = (Boolean)mMap.get(key);
return v != null ? v : defValue;
}
}
我们可以看到,有一个等待方法,如果Map初始化没有完成,那么将阻塞所有流程
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
那么,你可能会说,哎呦不就是一个几字节的文件哪,加载很快的啦?
这个确实如此,但是很多人非常喜欢自己封装一个SPManager等相关的操作类,如果不区分存下的xml文件的话,那么一个xml文件的量级根本是不可控的,记得之前有人在SP里面存Base64图片,导致程序Splash直接阻塞一两秒,并且对于Splash这种最优先展示给用户的界面,这一两秒可能是致命的,所以如果只想使用SP的话,建议开机load文件要控制住!
SharedPreferences preferences = getSharedPreferences("hello",MODE_PRIVATE);
preferences.edit().putBoolean("isFirst",true).apply();
用过的人都知道,edit()方法会直接new一个对象
@Override
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
代码如上,其实问题更大了,如果我写在一个Manager里面的话,直接搞一个静态变量不就得了,那么会直接导致上面的加载时间长,甚至ANR,当然你也可以区分xml之类的,这里就不多说了,如果随用随创建,那么使用不当可能会频繁gc,内存抖动,然后内存碎片导致OOM。
设置数据这一块没有什么可讲的,就是set进一个map等待下面的提交。
首先结论:无论是commit或者是apply,都有可能造成卡顿甚至ANR。
都知道Commit是同步代码提交,那么它会返回一个成功或者失败的Boolean变量,内部逻辑呢,就是取出map然后比较两个map的差异然后写入,这里就不多介绍,既然是同步那就一定会有一定的延迟,可能是忽略不计的,同步大家都懂,同时代码逻辑也简单一些。
那么异步为什么也会这样呢,接下来我们看apply的方法源码:
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
通过代码我们清楚的看到,apply方法只是帮我们封装了一下,变成了一个runnable任务,然后交给QueuedWork去提交,代码如下
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
那么熟悉AMS的小伙伴可能就发现问题了,在我们Android的生命周期里面有onPause()这个方法,那么在ActivityThread里面,我们可以找到调用它的方法,下面是代码:
@Override
public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,
int configChanges, PendingTransactionActions pendingActions, String reason) {
ActivityClientRecord r = mActivities.get(token);
if (r != null) {
if (userLeaving) {
performUserLeavingActivity(r);
}
r.activity.mConfigChangeFlags |= configChanges;
performPauseActivity(r, finished, reason, pendingActions);
if (r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
mSomeActivitiesChanged = true;
}
}
哇哦,相信大家一眼就定位到了问题所在,那就是 QueuedWork.waitToFinish();,那么我们经过一个小案例来演示一下
假如你需要将用户头像缓存起来,只用SP的话,你在用户选择完头像,在你的处理Activity里调用apply的话,然后马上跳到另一个activity
那么此时onPause()会阻塞(这里夸张了一下,大家知道什么意思就行),导致你的activity跳转可能会出现一定的延迟,并且我们知道SP的更新是全量文件覆盖,那么如果xml文件本来就很臃肿,那么会出大问题甚至ANR。
本文章虽然是时间比较新,但是我发现SDK31以后ShardPreferenceImpl确实消失了,并且内容开始出现了DataStore,估计是谷歌要更换掉SP的底层架构,在原有API调用不变的方式下换个内脏,也有可能直接在getShardPreference()方法打上废弃标签,大家拭目以待。
在这里还是推荐大家使用MMKV或者DataStore。
接下来会发一下MMKV的源码分析以及SDK31以后的SP底层到底发生了什么变化,喜欢可以关注一下哦,大家一起进步~!