“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情”

前言

最近看到社区里好几篇文章都是关于<K:V>存储的,在安卓上首要体现在讨论SharedPreferencesMMKVDataSource。各种横向竖向比较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

  1. 初始化时
  2. commit时
  3. 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(),写入值,或许直接getStringgetInt获取值等,那咱们先来看看这两个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

Android重学系列(五):从源码看SharedPreferences和干掉ANR

能够用流程图形象表达这一进程

图片出自今天头条文章

SharedPreferences 处理ANR

经过了上面的源码分析,咱们得出了SharedPreferences会导致anr的3个关键结点

  1. 初始化时
  2. commit时
  3. 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点,

  1. processPendingWork()
  2. 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:这儿示例代码晚点弥补

Android重学系列(五):从源码看SharedPreferences和干掉ANR

MMKV的问题

目前MMKV的两个问题是

  1. 丢失数据问题
  2. 不支撑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问题