关于闲鱼的ANR治理,我有几条心得...

简介: 闲鱼ANR治理之路

背景

闲鱼在业务的快速迭代过程中,面临着稳定性的考验,尤其是ANR(应用程序无响应)问题尤为突出,在舆情平台偶尔可以看到有用户反馈闲鱼App卡顿、卡死的的情况。发生ANR时系统会弹框引导用户关闭应用,或者直接杀死应用进程,非常影响使用体验,甚至造成用户流失。

ANR问题的难点在于线下极难复现,平时测试过程中几乎没有ANR问题的反馈,但是到了线上,面对Android碎片化机型、系统运行状态、用户操作习惯,导致出现ANR问题。所以必须依靠监控排查针对性地解决问题。
本文主要从ANR监控、排查体系、优化案例几个方面阐述闲鱼对ANR问题治理的思路。
undefined

ANR引入的原因

要解决ANR问题首先需要了解ANR引入的原因。Android系统通过对应用进程的组件(Activity,Service,Receiver,Provider、input)的响应能力进行超时监控,如果超过预定时间应用进程还未完成任务,则会触发系统的ANR警告。
所以ANR引入的原因可以分为两大类

  1. 主线程繁忙,来不及处理关键消息:存在耗时消息、或者消息队列拥塞,关键消息得不到调度、或者发生死锁
  2. 系统繁忙,主线程得不到调度:系统或应用内部其它线程或资源负载过高(高IO、内存频繁抖动),主线程调度被严重抢占

监控方案

1. 监听anr目录的变化

使用FileProvider监听 /data/anr/traces.txt 文件的变化,并捕获现场进行上报。不过Android 6.0以上版本系统文件权限收紧后,没有读取这个文件的权限。之前我们采用这个监控方案导致大量高版本设备ANR问题漏报。

2. 主线程超时监测

开启一个子线程定期post一个message到主线程,每隔一段时间(比如5秒)监测该message是否被消费掉,如果没有被处理,则说明主线程被卡住,可能发生了ANR,再通过系统服务获取当前进程的错误信息,判断是否有ANR发生。
但这个会存在大量漏报的情况,并且轮询的方案性能不佳。

3. 监听 SIGQUIT 信号

系统服务在触发ANR后,会发送一个SIGQUIT信号到应用进程来触发dump traces,在应用侧我们可以监听SIGQUIT信号来判断是否发生了ANR。为了排除其他进程的ANR导致的误报,需要再通过系统服务获取当前进程的错误信息,进一步过滤。
第3种方案准确率高,性能损耗小,也是业界目前主流APP采用监控方案。

排查体系

选择了合适的监控方案之后,还需要完善的排查体系以便对ANR问题归因分析。

1. ANR traces信息

Crash sdk在监听SIGQUIT信号后,会调用art虚拟机内部dump堆栈的接口,获取ANR traces信息,包含ANR进程中所有线程的堆栈,据此可以分析出是否有主线程耗时、死锁、主线程等待锁、主线程sleep等问题。

下图为相册场景下卡死ANR,通过trace文件可以定位到原因为主线程在等待子线程。
CD24E2FA-AD04-413D-A70B-182F7FAF24F4.png

下图为webview场景下ANR,通过trace文件可以定位到原因为主线程主动循环sleep,等待资源初始化完成。
1EBA063A-E99C-4CC9-8568-2DCEB5949AE9.png

2. 主线程消息队列监控

在依靠ANR traces信息修复有明确堆栈的问题之后,剩下比较多的是nativePollOnce的问题,如下图堆栈
undefined
堆栈上都是系统消息队列的源码,没有业务代码,似乎不好定位分析。
进入nativePollOnce场景可能的几种情况:

  1. 当前没有待处理的消息,线程进入睡眠状态,等待管道另一端有入队消息唤醒;
  2. 消息队列其实有消息待处理,但是被设置了同步屏障,遍历队列消息列表如果没有找到异步消息,则会进入nativePollOnce等待唤醒;
  3. dump traces 过于耗时导致偏移,耗时消息在dump之前发生。

对于第2点情况,可以通过hook消息队列,检测是否有同步屏障泄露的情况,我们在线上小范围采样埋点并没有发现此类问题。
对于第3点情况,可以对ANR发生前主线程消息队列历史消息做监控,在发生耗时消息时主动上报,在发生ANR时把历史消息、当前消息、等待队列的消息通过crash sdk上报云端。

实现方案

通过设置主线程Looper的Printer,监控每一个消息的调度,记录消息的target、callback、what,以及当前时间戳。
同时开启一个子线程,如果有消息处理发生则会定时采集主线程的堆栈,并通过时间戳将堆栈与消息关联起来,从而可以了解每个消息在执行时主线程的堆栈。

public final class Looper {
    public static void loop() {
        ......
        
        for (;;) {
            ......
            final Printer 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("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
        }
        ......
    }
}

由于存在频繁的字符串拼接,对性能有一定损耗,只会对线上小范围采样开启。

方案效果

通过对消息队列的监控,可以看到有一个消息执行耗时155ms,挂钟耗时411ms,观察堆栈可知原因是在主线程调用较重的初始化操作,并且存在跨进程调用,一但阻塞了后面Receiver、Service等消息执行,则会触发系统服务ANR警告。
C5797459-9BC9-4ACE-8276-D5B065976AD9.png

D1433B49-289B-49BD-B855-83393DDBD4B9.png

优化案例

在具备完善准确的监控排查能力之后,下面分享一些优化案例。

1. SharedPreference优化

从线上ANR的traces数据来看,关于sp导致的anr问题主要集中在3类:

  1. 在特定消息处,主线程等待sp apply队列持久化完成
  2. 主线程对sp commit
  3. 主线程阻塞等待sp加载数据完成

在线下测试mmkv与sp对比性能数据,发现mmkv可以比较完美的解决这三个问题。
首次安装分别测试mmkv和sp的读写性能(循环1000次取总和,每个key、value均不相同):
undefined
第2次启动,只读取kv组件的一个值
undefined

我们在编译器通过切面的方式接管所有getSharedPreferences的接口调用,根据白名单配置返回mmkv实现或者原始系统的SharedPreferencesImpl实现,对业务层使用无感知。

2. 网络广播监听耗时优化

从线上ANR的traces数据来看,关于getActiveNetworkInfo的ipc调用不少,通过埋点发现一方面是由于ipc跨进程通信本身耗时,另一方面监听网络状态的广播监听者实例过多,每一个都会重复调用一次查询网络状态,每个累加造成了耗时加剧,一旦阻塞了关键消息的调度执行,则会引发ANR

优化方案为通过动态代理IConnectivityManager接口,拦截代理getActiveNetworkInfo方法,优先使用缓存,
由统一全局的网络广播监听器在异步线程IPC获取网络信息,更新缓存,后面可以直接使用缓存,避免多次IPC调用。

3. 启动组件延迟注册

启动Application#onCreate阶段有串行任务会阻塞主线程执行,此时系统发送的关键消息得不到主线程调度就会发生ANR。
修复的核心思想是尽量不要在启动阶段注册receiver、service等组件,或者延迟到onCreate全部执行完毕再注册。

public class MyApplication extends Application {
  
      @Override
    public void onCreate() {
      //耗时串行任务...
      isInitDone=true;
    }
  
    @Override
    public Intent registerReceiver(final BroadcastReceiver receiver, final IntentFilter filter) {
        if (isInitDone) {
            return super.registerReceiver(receiver, filter);
        }

        mainHandler.post(new Runnable() {
            @Override
            public void run() {
                MyApplication.super.registerReceiver(receiver, filter);
            }
        });

        return null; 
}
}

总结和展望

在对ANR问题的监控升级和排查能力的完善之后,通过解决一系列优化方案,ANR率下降一半以上,给用户带来更好的使用体验。希望本文的内容可以开发者治理ANR带来启发,把我们应用代码的性能做到极致。
后续我们会思考以下两方面的内容:

  • 继续加强对ANR问题优化治理,比如将关键消息切到异步线程执行,避免主线程队列拥塞得不到调度的情况;
  • 加强防御机制防止数据劣化,比如线下自动化稳定性测试提前发现新增问题。
相关文章
|
6月前
|
测试技术 应用服务中间件 Shell
给你的系统做好埋点
给你的系统做好埋点
61 0
|
消息中间件 传感器 监控
钉钉 ANR 治理最佳实践 | 定位 ANR 不再雾里看花
钉钉 ANR 治理最佳实践 | 定位 ANR 不再雾里看花
586 0
钉钉 ANR 治理最佳实践 | 定位 ANR 不再雾里看花
|
监控 BI 定位技术
直播程序源码开发建设:洞察全局,数据统计与分析功能
数据统计与分析功能不管是对直播程序源码平台的主播或运营者都会有极大的帮助,是了解观众需求、优化用户体验成为直播平台发展的关键功能,这也是开发搭建直播程序源码平台的必备功能之一。
直播程序源码开发建设:洞察全局,数据统计与分析功能
|
消息中间件 监控 算法
让 nativePollOnce 不再排名第一 | 钉钉 ANR 治理最佳实践
让 nativePollOnce 不再排名第一 | 钉钉 ANR 治理最佳实践
1869 0
让 nativePollOnce 不再排名第一 | 钉钉 ANR 治理最佳实践
|
存储 消息中间件 SQL
看场景、重实操,实时数仓不是“纸上谈兵”
Hologres产品负责人合一谈谈他眼中的实时数仓!
2116 4
看场景、重实操,实时数仓不是“纸上谈兵”
|
数据采集 SQL 存储
整体技术流程-数据入库(ETL)|学习笔记
快速学习整体技术流程-数据入库(ETL)
1117 1
整体技术流程-数据入库(ETL)|学习笔记
|
缓存 前端开发 数据可视化
前端同学在可观测性的启蒙与初试探--快速实现根因分析/业务大盘
前端同学在可观测性的启蒙与初试探--快速实现根因分析/业务大盘
286 0
前端同学在可观测性的启蒙与初试探--快速实现根因分析/业务大盘
|
存储 缓存 运维
调用链追踪系统在伴鱼:OpenTelemetry 最佳实践案例分享
在 理论篇 中,我们介绍了伴鱼在调用链追踪领域的调研工作,本篇继续介绍伴鱼的调用链追踪实践。在正式介绍前,简单交代一下背景:2015 年,在伴鱼服务端起步之时,技术团队就做出统一使用 Go 语言的决定。
751 0
调用链追踪系统在伴鱼:OpenTelemetry 最佳实践案例分享
|
移动开发 算法 小程序
浅谈数据埋点可行性方案 [ 新年快乐,心想事成]
埋点服务的数据库的数据量,根据APP的用户量成指数级别成正比。如果需要的话,可以采用分库分表。
256 0
|
数据采集 存储 机器学习/深度学习
数据太多、太乱、太杂?你需要这样一套数据治理流程
数据作为机器学习的基础,从 GB、TB 到 PB 已经增长了无数倍,现在大一点的业务场景,没有 TB 级数据都提供不了高效的体验。那么数据怎么治理才好,怎样与模型、算力结合才算妙?在本文中,我们将看看什么是 HAO 数据治理模型,看看公安数据到底是如何规范处理的。
294 0
数据太多、太乱、太杂?你需要这样一套数据治理流程