Android 解决 SharedPreferences 导致的ANR问题

背景

APM平台在上线 ANR 监控平台后,线上收集到了较多的ANR日志 ,从火焰图信息上看,函数阻塞在了QueuedWork 相关函数上 ,本文主要介绍的这一现象的原因以及如何解决这一问题。
在这里插入图片描述

本文介绍的解决方案 已开源到githubhttps://github.com/Knight-ZXW/SpWaitKiller

SP任务 阻塞主线程导致ANR的原理

首先简单介绍下 QueuedWork这个类,QueuedWork主要是用来执行和跟踪一些进程全局的工作,但目前主要调度的SP相关的异步任务 ,当调用 SharedPreferences的 applay方法时,其所要执行的SP文件变更操作会被转化成对应的任务 并调用QueuedWork.queue 方法发送到QueuedWork类中进行执行。
在这里插入图片描述

同时,系统为了保证这些任务在一些关键动作触发前(如 页面跳转启动Activity) 已经被执行完成, 设计了一个等待机制。简单描述以下这个机制

  • SP的apply操作 会产生2个Runnable对象,其实一个为具体文件修改的工作任务(Work Runnable),另一个为 等待任务(awitCommit), 当工作任务被执行完成时,会通过一个CountDownLatch对象通知 等待任务,而awitCommit内部主要就是等待这个CountDownLatch计数器
  • 工作任务最终会通过 QueueWork的queue被发送到异步线程执行
  • 等待任务(awitCommit)会通过QueueWork的addFinisher函数被添加到 QueueWork内部的等待队列中
  • 最后,在系统的一些关键流程,比如ActivityThread执行handleStopActivity时会通过waitToFinish保证这些异步任务都已经被执行完成

在这里插入图片描述

而此时如果系统资源(cpu、io)比较紧张、或者是提交的异步任务较多,则可能导致onStop执行时间较长,从而导致ANR。

另外 在Android 8.0.0 以上的版本,google 在 waitToFinish的实现中做了一些改动,在原有的等待所有异步任务执行完成的基础上,会通过调用 processPendingWork 将QueueWork中未执行的任务直接取出在当前线程直接执行。这个变更的原因是waitToFinish调用的时机一般是主线程, 主线程的优先级会比QueueWork内部线程的优先级更高,因此未执行的任务重新分发到主线程直接执行,提高执行效率。
在这里插入图片描述

在这里插入图片描述

SP阻塞问题解决

反射替换 finishers队列对象

解决SP 造成的阻塞问题,有很多方式,比如将应用内使用SP的代码 通过字节码插桩改为MMKV或其他更高效键值存储库实现。另一种方式是 字节跳动在一篇分享的文章中提出的 通过代理替换Queuework类内部的sFinishers对象,保证执行 waitToFinish时 队列长度为空实现的。
在这里插入图片描述

这里 sFinishers.poll 函数的调用在整个类中,只有这一个地方调用,因此 通过动态代理替换该对象,重写poll函数实现 使其总是返回null对象,并不会对其他流程造成影响

sFinishers对象在不同的版本具体使用的类不同

  • android 8.0以下版本使用的是 ConcurrentLinkedQueue
  • android 8.0 之后 使用的是LinkedList

以8.0以上版本为例,创建一个代理类,修改poll的实现
![image.png](https://img-blog.csdnimg.cn/img_convert/bfea9fd102c986a73904b05e5d31c948.png#clientId=u7de972e4-0b4c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=323&id=u999ef82a&margin=[object Object]&name=image.png&originHeight=645&originWidth=891&originalType=binary&ratio=1&rotation=0&showTitle=false&size=74648&status=done&style=none&taskId=ua50e5755-4c71-468d-bb44-d2cccf1e2f5&title=&width=445.5)
再通过反射替换掉该实现类。
在这里插入图片描述

解决processPendingWork调用

之前介绍过在 8.0及以上版本 调用waitToFinish 时,除了在执行等待finishers队列之前,会在当前线程直接调用processPendingWork函数。以下是程序运行时主线程 和 异步工作线程之间的关系图。
请添加图片描述

因此processPendingWork可能在主线程执行 也可能在异步线程中执行, 在 8.0~11.0下 processPendingWork的调用可能存在两个block点

  1. 异步线程正在执行 processPendingWork函数,异步工作线程持有 sProcessingWork锁,因此主线程执行 processPendingWork时 ,因为获取不到 sProcessingWork锁 ,出现锁等待
  2. 当主线程成功获取到 sProcessingWork锁,调用clone函数时,sWork队列中 确实存在未执行的任务,这部分任务将在主线程直接执行,如果此时IO操作较慢,则主线程因为慢IO出现阻塞甚至ANR

在这里插入图片描述

由于这两个原因,因此只代理clone函数是不可行的,因为如果异步线程正在执行processPendingWork函数,并且执行的比较慢,那么主线程还是会出现等待的情况。 最终 采取的方式是,无论实在哪个线程执行,代理的clone函数都返回空队列,这样保证了processPendingWork的调用不会出现互相阻塞,相当于processPendingWork实际上没有执行任何操作, 并且通过反射获取QueuedWork的mHandler的Looper对象,创建一个新的Hander,并将sWork中的任务提交到这个Handler去执行,从而实现了无阻塞运行。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

需要注意的是,由于hidden API的限制, sWork成员变量 只能在 target sdk version小于以下的app中被反射得到,因此如果希望在target大于28 的app正常工作,还需要 突破系统hidden api的限制,这里可以使用 LSPosed 提供 hiddenApiBypass库。

另外 在Android 12 版本,这部分代码又发生了变更, 不再使用clone 和 clear的方式 拷贝集合副本,而是直接替换 sWork的引用来实现.
在这里插入图片描述

这样,替换clone函数的方案就不可行了,并且由于 sWork变量指向的对象在每次调用processPendingWork 都会发生变更,因此动态代理替换sWork对象的操作不能只执行一次。 继续寻找可以hook的点, 对于

for (Runnable v: work)

这个代码 在字节码层面其实会被转换为迭代器的调用,因此 可以将之前的操作 转换到 iterator函数中执行,返回一个空的迭代器对象,因此将之前的方案从 代理 clone函数 改为代理 iterator函数,并且需要保证 每次调用获取迭代器函数后 再此将sWork对象重新代理掉。

最后

上述 方案代码量其实不多,因此我 在github上建了一个工程用来模拟并解决QueueWork任务阻塞造成的ANR问题, 可供参考 https://github.com/Knight-ZXW/SpWaitKiller .
在上线时,应当对使用到SP的业务进行相应的测试,比如如果存在跨进程组件依赖同一个SP文件的情况,由于我们取消了Activity 在Stop时的 SP文件变更的刷盘行为,因此如果跳转到其他进程的组件,而该组件又依赖于跳转前的SP变更的最新配置值,那么可能会出现问题。
另外事实上,从收集ANR的其他上下文信息来看,虽然SP的操作阻塞导致了ANR操作,但是并不能说明真正的原因是因为SP导致的,比如可能由于物理内存紧张、频繁发生swa 操作影响了正常的io操作,影响了SP的刷盘速度,最终导致了ANR出现.

参考项目及资料

1.https://weishu.me/2016/10/13/sharedpreference-advices/
2.https://github.com/LSPosed/AndroidHiddenApiBypass

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卓修武

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值