“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情”
前言
最近看到社区里好几篇文章都是关于<K:V>存储的,在安卓上主要体现在讨论SharedPreferences、MMKV、DataSource。各种横向竖向比较SharedPreferences,把SharedPreferences按在地上摩擦摩擦再摩擦。
为什么这么说呢?是因为SharedPreferences有“很严重的性能问题”,这里当然是要打引号的,因为从安卓系统发展至今,这个api也没见被标记成Deprecated吧,Google这么大一公司难道就没有意识到问题?其实应该是意识到了,在android2.3的时代,SharedPreferences接口是只有commit()的,apply()正是官方意识到这个问题才推出的
那推出了apply(),性能问题得到了解决吗?
答案是解决了部分,但是对比于MMKV,还是有差距,毕竟MMKV写数据等同于往内存中写,能不快吗?
那你不禁会问:那我还用它做甚?用它做甚?用它做甚?
我全部切换成MMKV
我梭哈,毕竟年底Kpi看得看它。
那解决问题了吗?
我的答案是
部分解决、等将来的某天,新人看着这堆代码,心里默念x遍xxx之后,开启了重构。
为什么这么说呢?
让我们先从SharedPreferences带来的一系列问题入手
SharedPreferences的使用姿势问题
可以说,我们今日所承受的SharedPreferences的苦果,可能大部分都是我们自己种下的因,在日常开发中,经常能碰到各种奇葩的逻辑实现方式,可能框架的作者都想不到还能这样用?这个后续我看有没有必要整理成一篇文章,我们先看下面的代码
val sharedPreferences = activity?.getSharedPreferences("army", Context.MODE_PRIVATE)
sharedPreferences?.edit()?.putString("test2",new Gson().toJson(xx)?.commit()
看起来也没啥问题?
NO!!!
问题很大~,
1、缓存问题
第一个就是getSharedPreferences实例没有缓存,虽然官方有做这个逻辑,如在ContextImpl中
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
。。。
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
但是它的作用域只是Activity级别的
2、同步提交问题
commit()是同步提交呀!这是
假设我们项目的SharedPreferences的操作都是集中在一个工具类里,如KvUtil,恰巧里面的put方法都是用的commit(),那我们每次KvUtil.putString("xx","xx")都相当于去写文件,可以说后果是灾难性的。
放弃commit()吧!不用用它,
要用它,请把它丢到子线程去
3、存储键值问题
SharedPreferences是一个轻量型数据存储方案,别把鸟枪当大炮用呀,一个key,存一个json是怎么回事?我都有看到数据最后序列化成json大于1MB的。
存储这种大对象,就老老实实规划一个数据库吧,做成异步的数据获取流
别老是在主线程像下面这样写 达咩!
val sharedPreferences = activity?.getSharedPreferences("army", Context.MODE_PRIVATE)
val result = sharedPreferences?.getString("test", null)
val entity = Gson().fromJson(result, Any::class.java)
都像这样写,App早晚都得给玩死,Gson的fromJson可也是一个耗时方法呀
SharedPreferences如何造成ANR
你问我它是如何造成ANR的,那我可能会回答你,它丫浑身都是病!!
概括起来就是它有三点会产生ANR
- 初始化时
- commit时
- apply后,页面生命周期回调时
我们先从源码去探索看看,为啥这么说,首先我们从入口方法getSharedPreferences开始
对源码不感兴趣的可以直接跳过这一节
1、getSharedPreferences
我们先从上面getSharedPreferences()入手,默认最后会调到ContextImpl中的getSharedPreferences,这里面就有前面根据name缓存File的逻辑,不过这个是Activity作用域的,如果第一次就会直接new SharedPreferencesImpl()
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
...
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
return sp;
}
这里存储的SharedPreferencesImpl是全局作用域的,但是这里的Key值File是Activity作用域,会在页面销毁时去回收,间接会去回收SharedPreferencesImpl实例,所以我们是完全可以自己去做一层缓存的
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
private ArrayMap<String, File> mSharedPrefsPaths;
2、SharedPreferencesImpl
接着我们来看下SharedPreferencesImpl的构造方法
SharedPreferencesImpl(File file, int mode) {
...
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
loadFromDiskLocked();
}
}
}.start();
}
通过传入的file,其实就是我们前面通过getSharedPreferences传入的name创建的对应的File,去开启了一个新线程执行loadFromDiskLocked(),这里就是去从文件加载我们之前存储的内容到内存,这是一个耗时操作,官方已经将它放在了异步线程。
3、loadFromDiskLocked
private void loadFromDiskLocked() {
if (mLoaded) {
return;
}
//文件容错,回退逻辑
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
。。。
if (mFile.canRead()) {
BufferedInputStream str = null;
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);
}
mLoaded = true;
notifyAll();
}
我们看到loadFromDiskLocked(),主要是去做了一个文件的容错逻辑,即SharedPreferences每次写文件的时候,会先创建一个副本文件,然后去执行源文件的写入,如果系统中断重启了,会再次初始化,这里先判断副本文件是否存在,如果存在则会先恢复,防止因为读取了损坏的文件造成不必要的后果,这样只会丢失系统最后一次执行写入时的数据。那这个方法就执行完了,看起来是不是没啥问题?接着往下看:
我们一般的使用方法都是,先getSharedPreferences,然后调用edit(),写入值,或者直接getString、getInt获取值等,那我们先来看看这两个api
4、edit、getString
发现它们都调用了一个 awaitLoadedLocked();这一看就是一个等待锁释放的方法,同时外部还加了一把锁
public Editor edit() {
synchronized (this) {
awaitLoadedLocked();
}
return new EditorImpl();
}
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
其实SharedPreferences的方法都有加锁,而且都调用了awaitLoadedLocked(),那这个方法具体是做什么的呢,我们看看源码
private void awaitLoadedLocked() {
if (!mLoaded) {//严格模式
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
wait();
} catch (InterruptedException unused) {
}
}
}
会发现就是根据mLoaded的值,开启了一个死循环,当mLoaded==true时,则退出,而mLoaded只有在初始化完成,在异步线程执行完文件读取操作才会置为true
这也就是,我们在线上环境,经常看到我们异常捕获平台,如bugly等上传的SharedPreferences anr记录里的一种情况,也就是我们开篇提到的第一种情况,初始化anr,通常体现在getString、getXx等方法执行耗时。
一般产生这种情况是因为:我们在SharedPreferences里存储了较大的数据,
别拿来存json了,哥们,别人设计的结构就只是XML,没想着你能给这么多
5、commit
我们接着来看看commit(),都说commit是一个耗时方法,会引起ANR,那让我们来从源码里看看具体做了什么?
public boolean commit() {
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
首先调了一个commitToMemory(),看了下这个方法的实现,主要是处理内存中的一些值
比如我们调用clear的时候,会清除值,还有就是会将我们之前put的新值进行一个替换。
接着重要的来了,又是一个写文件的任务enqueueDiskWrite(),调用了之后执行了writtenToDiskLatch.await(),等待锁的释放,
所以这就是耗时所在呢。会去等待其它线程的任务完成
我们看看enqueueDiskWrite()的具体实现
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
这里会根据是否传入postWriteRunnable来判断isFromSyncCommit的值,是否是同步提交,其实后面讲到的apply()也会调到这里,只是这个值会有所不同而已,这里因为我们在commit()传入的是null,则是同步提交,会直接调用writeToDiskRunnable.run(),去完成文件的写入,这里有一个细节就是有判断writeToDiskRunnable的值,去决定是否直接运行,而这个值是在前面commitToMemory中进行更改的。
那有没有可能writeToDiskRunnable!=1的情况,应该是有的,比如我们在多线程场景下去同时提交,就会出现这种情况,最终也还是会调到QueuedWork.queue里
到这里,我们知道,我们调用commit(),会产生一个写入文件的操作,而这个操作是发生在我们commit()调用的线程,在Android中,我们如果在主线程调用,则有可能产生ANR
6、apply
那将commit()换成了apply(),解决了同步的问题,是不是万无一失了呢?答案肯定是否
它引入了新的问题!我们继续看源码
public void apply() {
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最终也会调用到writeToDiskRunnable,只是此时isFromSyncCommit==false,这里和commit有一点不同的时,有调用QueuedWork.addFinisher,往QueuedWork中加入一个等待的Runnable,这里主要的用处是后面其它组件调用waitFinish时,以此来判断是否有未完成的任务
接下来最终会调用到QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit)中,前后还说了多线程同步commit时也会走到这里,那我们去看看这个方法的具体实现
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
我们发现,首先将我们前面创建的Runnable任务加入到了数组List里,然后才去判断同步异步,这里的同步和异步的判断其实可以忽略,只是异步的时候会发送一个延迟0.1s的消息到handler,我们继续来看看Handler的实现
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;
}
}
咚咚咚~发现没有,这里其实都是用子线程来实现的,然后我们直接看Run方法,会间接调到processPendingWork(),我们来看下实现
private static void processPendingWork() {
...
synchronized (sProcessingWork) {
LinkedList<Runnable> work;
synchronized (sLock) {
work = sWork;
sWork = new LinkedList<>();
...
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
}
}
}
很简单哈,就是会去执行我们之前add到数组里的各个Runnable。
那这样看,其实符合我们的述求对吧,都是在子线程去提交了,难道还有坑?接着看
QueuedWork.waitFinish()的实现
public static void waitToFinish() {
...
Handler handler = getHandler();
synchronized (sLock) {
if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
handler.removeMessages(QueuedWorkHandler.MSG_RUN);
...
}
sCanDelay = false;
}
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try {
processPendingWork();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
//等待任务执行完成
try {
while (true) {
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
sCanDelay = true;
}
}
这个方法的意思就是当我们QueuedWork中的Handler还有消息没有处理时,则手动移除消息,然后手动去执行我们之前往QueuedWork.queue提交的任务,处理完成后,还会去等待前面我们add的任务,即在apply时,QueuedWork.addFinisher(awaitCommit)调用添加的Runnable,如果异步任务已完成,Runnable.run就不会被阻塞,否则的话会阻塞,直到最终任务完成
那什么时候这个方法会执行呢?
注释里说的是在Activity.onPause,BroadcastReceiver.onReceive,Service的命令处理等会被调用,我们去源码里验证下
android 11 之前
public void handlePauseActivity(ActivityClientRecord r, boolean finished, boolean userLeaving,
int configChanges, PendingTransactionActions pendingActions, String reason) {
。。。
if (r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
mSomeActivitiesChanged = true;
}
android 11之后
public void handleStopActivity(ActivityClientRecord r, int configChanges,
PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
...
if (!r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
...
}
可以发现Android11之前和之后会有点小差别,11之后会从onPause()调整到onStop()
private void handleStopService(IBinder token) {
mServicesData.remove(token);
Service s = mServices.remove(token);
if (s != null) {
QueuedWork.waitToFinish();
}
}
private void handleServiceArgs(ServiceArgsData data) {
CreateServiceData createData = mServicesData.get(data.token);
Service s = mServices.get(data.token);
if (s != null) {
QueuedWork.waitToFinish();
}
}
在Service stop的时候和命令处理也会调用。
好了,最终我们得出结论,会在ActivityonPause或者onStop中会去调这个方法,值得注意的是,这里可都是在主线程,那就意味着最后waitFinish执行的时候也在主线程,这也就会间接导致我们的应用ANR
可以用流程图形象表达这一过程
图片出自今日头条文章
SharedPreferences 解决ANR
经过了上面的源码分析,我们得出了SharedPreferences会导致anr的3个关键结点
- 初始化时
- commit时
- apply后,页面生命周期回调时
那这个问题有解吗?我的回答是有解的,现在我们针对这三点逐个解答
1、初始化时产生的ANR
初始化产生卡顿的原因是因为:加载的sp文件存储内容过多,那我们除了消减文件大小,将大文件拆分成各个小模块的文件,整治K、V存储的规范,防止存入大Key和大Value外,还有其它整治方案吗?
有的!不过这里主要是在我们应用启动去处理
我们知道产生ANR的原因是因为,我们第一次去取值或者存储时,文件的内容还没有读取到内存的原因,那我们能不能提前将所有的sp文件进行读取呢?
可以呀,我们只需要收集我们需要提前读取的sp文件名称就行了,然后在我们应用启动的页面中加入一个异步任务,这样在启动时就处理好了这一问题。
不过这种方式可能会对应用的启动速度有一定影响
2、commit时产生的ANR
commit就不多说了,我们直接切到子线程去调用就行了,或者直接不要用这个api
3、apply时产生的ANR
我们知道apply是产生ANR的根源是因为其它组件调用了waitFinish,导致了主线程去处理了文件写入的操作,那我们可以绕过这个逻辑吗,答案是有的
不过需要分版本来进行 我们先列一下Android最近的版本
| API 32 | android 13 |
| API 31 | android 12 |
| API 30 | android 11 |
| API 29 | android 10 |
| API 28 | android 9.0 Pie |
| API 27 | android 8.1 Oreo |
| API 26 | android 8.0 Oreo |
| API 25 | android 7.1 Nougat |
| API 24 | android 7.0 Nougat |
| API 23 | android 6.0 Marshmallow |
| API 22 | android 5.1 Lollipop |
| API 21 | android 5.0 Lollipop |
因为Android在几个大的版本都有对QueuedWork都有改动,我们需要分版本去处理
1、version < android 8.0
这里我们可以hook sPendingWorkFinishers,让poll()默认返回null
public static void waitToFinish() {
Runnable toFinish;
//hook点
while ((toFinish = sPendingWorkFinishers.poll()) != null) {
toFinish.run();
}
}
Hook示例代码
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
val clazz = Class.forName("android.app.QueuedWork")
val field = clazz.getDeclaredField("sPendingWorkFinishers")
field.isAccessible = true
val notBlockLinkedQueueDelegate =
NotBlockLinkedQueueDelegate(field.get(null) as ConcurrentLinkedQueue<Runnable?>)
field.set(null, notBlockLinkedQueueDelegate)
}
需要自定义一个NotBlockLinkedQueueDelegate
internal class NotBlockLinkedQueueDelegate(private val queueList: ConcurrentLinkedQueue<Runnable?>) :
ConcurrentLinkedQueue<Runnable?>(queueList) {
override fun add(element: Runnable?): Boolean {
return queueList.add(element)
}
override fun remove(element: Runnable?): Boolean {
return queueList.remove(element)
}
override fun poll(): Runnable? {
return null
}
override fun isEmpty(): Boolean {
return true
}
}
2、version > android 8.0
build version大于8.0时,会有两个hook点,
- processPendingWork()
- sFinishers.poll()
public static void waitToFinish() {
。。。
try {
//Hook点1
processPendingWork();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
try {
while (true) {
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
//Hook点2
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
sCanDelay = true;
}
}
1. sFinishers.poll
Hook示例代码
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.O&&
Build.VERSION.SDK_INT<Build.VERSION_CODES.S){
val clazz = Class.forName("android.app.QueuedWork")
val sLock = clazz.getDeclaredField("sLock")
sLock.isAccessible = true
val lock = sLock.get(null)
if (lock != null) {
val field = clazz.getDeclaredField("sFinishers")
field.isAccessible = true
val o: Any = NotBlockListDelegate(field.get(null) as LinkedList<Runnable>)
synchronized(lock) { field.set(null,o) }
}
}
需要自定义一个NotBlockListDelegate
internal class NotBlockListDelegate(private val queueList: LinkedList<Runnable>) :
LinkedList<Runnable>(queueList) {
override fun add(element: Runnable): Boolean {
return queueList.add(element)
}
override fun remove(element: Runnable): Boolean {
return queueList.remove(element)
}
override fun poll(): Runnable? {
return null
}
override fun isEmpty(): Boolean {
return true
}
}
2. processPendingWork
processPendingWork() Android 11之前和之后有点小不一样
versioo <= android 11
private static void processPendingWork() {
。。。
synchronized (sProcessingWork) {
LinkedList<Runnable> work;
synchronized (sLock) {
work = (LinkedList<Runnable>) sWork.clone();
sWork.clear();
。。。
}
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
}
}
}
Hook示例代码
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.S
) {
val clazz = Class.forName("android.app.QueuedWork")
val method: Method = clazz.getDeclaredMethod("getHandler")
method.isAccessible = true
val handler = method.invoke(null) as Handler
val looper = handler.looper
val sWorkField: Field = clazz.getDeclaredField("sWork")
sWorkField.isAccessible = true
val sProcessingWorkField: Field = clazz.getDeclaredField("sProcessingWork")
sProcessingWorkField.isAccessible = true
val lock = sProcessingWorkField.get(null)
val sWork = sWorkField.get(null) as LinkedList<Runnable>
val handlerLinkedListDelegate = HandlerLinkedListDelegate(sWork, looper)
synchronized(lock) { sWorkField.set(null,handlerLinkedListDelegate) }
}
需要自定义一个HandlerLinkedListDelegate
internal class HandlerLinkedListDelegate(
private val queueList: LinkedList<Runnable>,
private val looper: Looper
) : LinkedList<Runnable>(queueList) {
private val mHandler = Handler(looper)
override fun clone(): Any {
val works = super.clone() as LinkedList<Runnable>
if (works.size > 0) {
mHandler.post {
for (w in works) {
w.run()
}
}
}
return LinkedList<Runnable>()
}
override fun isEmpty(): Boolean {
return queueList.isEmpty()
}
override fun clear() {
queueList.clear()
}
}
version > android 11
Android 12之后,替换掉了之前的 LinkedList clone的逻辑,直接替换了对象,并且每次都会重新new一个LinkedList给sWork
private static void processPendingWork() {
。。。
synchronized (sProcessingWork) {
LinkedList<Runnable> work;
synchronized (sLock) {
work = sWork;
sWork = new LinkedList<>();
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
}
}
}
看了下代码提交记录,官方这里在Android12之后做了一个优化,是因为之前的做法会涉及到数据的复制和清理,相比于之前的做法,现在直接new一个新的对象,性能会更高点。
Eliminate redundant churn in SharedPreferences
Don't clone-then-discard, just work from what we have already, and start
fresh for potential new work.
Bug: 161534313
Test: atest android.content.cts.SharedPreferencesTest
Change-Id: I6edb2b09537f5e77cc2ad3e4d2f32a89b945ad80
那此时没有了clone()可以让我们hook,这里怎么解决这里这个任务执行的同步问题呢?
答案是有的,我们可以看到下面调用了work.size(),我们可以从这个方法入手
我们还是hook掉sWork,只是这里的LinkedList我们需要换一下,让LinkedList size()默认返回0,然后在里面去处理还没有执行完的任务
同时因为有下面这个重新赋值的过程
work = sWork;
sWork = new LinkedList<>();
我们可以在 work.size()中去处理,因为第1次一定会走到我们的代理中去,我们在代理类中的size(),重新去代理前面被重新赋值的sWork,即可解决问题。
TODO:这里示例代码晚点补充
MMKV的问题
目前MMKV的两个问题是
- 丢失数据问题
- 不支持getAll
这两个问题其实还挺硬伤的,第一个问题个人认为如果没有用来存储一些敏感数据,我觉得还好,理论上MMKV这种发生错误的几率应该还是蛮小的
第2个问题的话,是我们必须要重视的,在我们接入MMKV时,一定要完全考虑好,如果接入的时候不去处理getAll这个问题,那以后再来处理就非常麻烦了。其实处理很简单,我们在自己的封装类里去对key进行处理,将value的类型也存储到key上就可以了。
如果之前没有支持getAll,但又接入了MMKV的小伙伴们,我觉得你们还是趁早优化掉这个遗留问题,因为现在不做将来总是要做得,而且现在做了,还能让我们KPI更好看不是 🐶
完全解决掉SharedPreferences
要完全去杜绝SharedPreferences引起的anr,我们可以去全盘切换成mmkv,为什么要这样做呢,是因为我们经常在二方库、三方库里发现使用SharedPreferences的情况,那理论上就可能存在导致anr的问题,因为这些是我们没法规范和整治的。所以一劳永逸的做法就是,直接用ASM字节码插桩替换掉 getSharedPreferences调用的地方为我们自己创建的代理就可以了。
但是别忘了处理好getAll
还有一种方案是替换为Jetpack的DataStore,这种更为友好,综合前面看Google填上QueuedWork的这个Hook点,官方是不建议我们之前那样去做的。同时他们也没去主动解决SharedPreferences带来的潜在问题,我猜测是有点想让你主动放弃,选择新框架的意思。
结论
其实从SharedPreferences这个问题,可以看出在我们日常的编码环境中,最大的祸就是没有注意好使用姿势,为什么没有使用好姿势呢?是因为我们不了解。
怎样了解它呢?最好最快的办法还是去通过官方文档的Api,去粗读一遍源码