闲鱼在业务的快速迭代过程中,面临着稳定性的考验,尤其是ANR(应用程序无响应)问题尤为突出,在舆情平台偶尔可以看到有用户反馈闲鱼App卡顿、卡死的的情况。发生ANR时系统会弹框引导用户关闭应用,或者直接杀死应用进程,非常影响使用体验,甚至造成用户流失。
ANR问题的难点在于线下极难复现,平时测试过程中几乎没有ANR问题的反馈,但是到了线上,面对Android碎片化机型、系统运行状态、用户操作习惯,导致出现ANR问题。所以必须依靠监控排查针对性地解决问题。本文主要从ANR监控、排查体系、优化案例几个方面阐述闲鱼对ANR问题治理的思路。
要解决ANR问题首先需要了解ANR引入的原因。Android系统通过对应用进程的组件(Activity,Service,Receiver,Provider、input)的响应能力进行超时监控,如果超过预定时间应用进程还未完成任务,则会触发系统的ANR警告。所以ANR引入的原因可以分为两大类
1. 主线程繁忙,来不及处理关键消息:存在耗时消息、或者消息队列拥塞,关键消息得不到调度、或者发生死锁
2. 系统繁忙,主线程得不到调度:系统或应用内部其它线程或资源负载过高(高IO、内存频繁抖动),主线程调度被严重抢占
使用FileProvider监听 /data/anr/traces.txt 文件的变化,并捕获现场进行上报。不过Android 6.0以上版本系统文件权限收紧后,没有读取这个文件的权限。之前我们采用这个监控方案导致大量高版本设备ANR问题漏报。
开启一个子线程定期post一个message到主线程,每隔一段时间(比如5秒)监测该message是否被消费掉,如果没有被处理,则说明主线程被卡住,可能发生了ANR,再通过系统服务获取当前进程的错误信息,判断是否有ANR发生。但这个会存在大量漏报的情况,并且轮询的方案性能不佳。
系统服务在触发ANR后,会发送一个SIGQUIT信号到应用进程来触发dump traces,在应用侧我们可以监听SIGQUIT信号来判断是否发生了ANR。为了排除其他进程的ANR导致的误报,需要再通过系统服务获取当前进程的错误信息,进一步过滤。第3种方案准确率高,性能损耗小,也是业界目前主流APP采用监控方案。
选择了合适的监控方案之后,还需要完善的排查体系以便对ANR问题归因分析。
Crash sdk在监听SIGQUIT信号后,会调用art虚拟机内部dump堆栈的接口,获取ANR traces信息,包含ANR进程中所有线程的堆栈,据此可以分析出是否有主线程耗时、死锁、主线程等待锁、主线程sleep等问题。
下图为相册场景下卡死ANR,通过trace文件可以定位到原因为主线程在等待子线程。
下图为webview场景下ANR,通过trace文件可以定位到原因为主线程主动循环sleep,等待资源初始化完成。
在依靠ANR traces信息修复有明确堆栈的问题之后,剩下比较多的是nativePollOnce的问题,如下图堆栈
堆栈上都是系统消息队列的源码,没有业务代码,似乎不好定位分析。进入nativePollOnce场景可能的几种情况:
1.当前没有待处理的消息,线程进入睡眠状态,等待管道另一端有入队消息唤醒;
2.消息队列其实有消息待处理,但是被设置了同步屏障,遍历队列消息列表如果没有找到异步消息,则会进入nativePollOnce等待唤醒;
3.dump traces 过于耗时导致偏移,耗时消息在dump之前发生。
对于第2点情况,可以通过hook消息队列,检测是否有同步屏障泄露的情况,我们在线上小范围采样埋点并没有发现此类问题。对于第3点情况,可以对ANR发生前主线程消息队列历史消息做监控,在发生耗时消息时主动上报,在发生ANR时把历史消息、当前消息、等待队列的消息通过crash sdk上报云端。
通过设置主线程Looper的Printer,监控每一个消息的调度,记录消息的target、callback、what,以及当前时间戳。同时开启一个子线程,如果有消息处理发生则会定时采集主线程的堆栈,并通过时间戳将堆栈与消息关联起来,从而可以了解每个消息在执行时主线程的堆栈。
publicfinalclass Looper{
public static void loop(){
......
for(;;) {
......
finalPrinter logging = me.mLogging;
if(logging !=null) {
logging.println(">>>>> Dispatching to "+ msg.target +" "+
msg.callback+ ": " +msg.what);
}
......
try{
msg.target.dispatchMessage(msg);
}finally{
...
}
......
if(logging !=null) {
logging.println("<<<<}
}
......
}
}
由于存在频繁的字符串拼接,对性能有一定损耗,只会对线上小范围采样开启。
第2次启动,只读取kv组件的一个值 我们在编译器通过切面的方式接管所有getSharedPreferences的接口调用,根据白名单配置返回mmkv实现或者原始系统的SharedPreferencesImpl实现,对业务层使用无感知。
publicclass MyApplication extends Application{
@Override
public void onCreate(){
//耗时串行任务...
isInitDone=true;
}
@Override
public Intent registerReceiver(final BroadcastReceiver receiver, final IntentFilter filter){
if(isInitDone) {
returnsuper.registerReceiver(receiver, filter);
}
mainHandler.post(newRunnable() {
@Override
public void run(){
MyApplication.super.registerReceiver(receiver,filter);
}
});
returnnull;
}
}