“我报名参加金石计划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,去粗读一遍源码
引证
1. 今天头条 ANR 优化实践系列 – 告别 SharedPreference 等候
2. Android 怎样处理使用SharedPreferences 形成的卡顿、ANR问题