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

2,766 阅读15分钟

“我报名参加金石计划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

image.png

可以用流程图形象表达这一过程

图片出自今日头条文章

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:这里示例代码晚点补充

image.png

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问题