4. 主线程消息队列监控:使用Looper.getMainLooper().setMessageLogging或Choreographer检测卡顿

4.1 现象识别:从“慢”到“不响应”

主线程卡顿的典型表现:用户点击、滑动、键盘输入滞后 100~200ms 即感知“卡”,超过 500ms 用户会重复操作,超过 2000ms(Input 超时阈值)系统弹 ANR 对话框。生产环境中,ANR 上报前通常会有一段长消息积压,利用队列监控能在“临界状态”捕获根本原因。

  • 初步症状:帧率下降(掉帧≥3帧/秒)、触摸事件丢包、View 绘制延迟。
  • 严重症状:Toast 不弹出、页面跳转白屏超过 2s、系统弹出 ANR 弹窗。
  • 排查难点:用户环境复杂,复现率低;普通 log 无法精准定位单次 Dispatch 耗时。
边界提醒:主线程单条 Message 处理时间 > 200ms 即可视为“严重卡顿”,>1000ms 是 ANR 高危。但并非所有长消息都会触发 ANR,需要结合 5 秒内是否有输入事件 综合判断。

4.2 数据采集:Looper.setMessageLogging 与 Choreographer 双组件

Android 框架为开发者提供了两层检测钩子:Looper.setMessageLogging 直接监听每个 Message 的 dispatch 前后时刻,Choreographer 通过 FrameCallback 监听每一帧的绘制耗时。

  1. Looper.setMessageLogging:在主线程 Looper 注册 Printer,框架在每次 dispatch 前后调用 printer.println(msg)。利用前后时间差可精确统计每条 Message 执行耗时。
  2. Choreographer.FrameCallback:在 doFrame 回调中记录上一帧时长,搭配自定义 Looper Printer 可区分“消息耗时”与“帧间间隙”。
// 1) Looper 级监控示例
Looper.getMainLooper().setMessageLogging(new Printer() {
    private long startTime;
    @Override
    public void println(String x) {
        if (x.startsWith(">>>>>> Dispatching")) {
            startTime = SystemClock.uptimeMillis();
        } else if (x.startsWith("<<<<< Finished")) {
            long cost = SystemClock.uptimeMillis() - startTime;
            if (cost > 200) {
                Log.w("ANR_GUIDE", "Long msg: " + cost + "ms -> " + x);
            }
        }
    }
});

// 2) Choreographer 帧率监控
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
    long lastFrameTime = 0;
    @Override
    public void doFrame(long frameTimeNanos) {
        if (lastFrameTime == 0) {
            lastFrameTime = frameTimeNanos;
        } else {
            long delta = (frameTimeNanos - lastFrameTime) / 1_000_000L; // ms
            if (delta > 30) { // 低于约 33fps 即为卡顿
                Log.w("ANR_GUIDE", "Frame gap: " + delta + "ms");
            }
            lastFrameTime = frameTimeNanos;
        }
        Choreographer.getInstance().postFrameCallback(this);
    }
});
两种检测手段对比
维度Looper.setMessageLoggingChoreographer.FrameCallback
监测粒度每条 Message/Runnable每帧(16.7ms 基准)
捕获瓶颈直接获知耗时任务(如 IO、Bitmap 解码)反映全帧耗时(含布局、绘制、动画)
开销每次 println 约 0.02ms,生产可接受需频繁 postFrameCallback,低负载可用
局限性不能识别多帧累积卡顿,需配合 Choreographer只能知道帧时长,无法直接定位具体 Message
最佳实践线上只 Log 耗时 > 200ms 的消息Debug 包全量,Release 包抽样

4.3 原因定位:基于监测数据的故障树

当检测工具上报卡顿时,使用以下排查规则树快速锁定根因:

  1. 判断消息类型:从 Looper 日志中的 “Dispatching to” 关键字识别 Handler 或 Runnable 来源。
  2. 排除系统输入:若消息来自 InputManager,检查当前焦点 View 的 onTouchEvent/onKeyDown 是否有耗时操作。
  3. 检查同步屏障:若出现连续的 FrameCallback 延迟,可能是 ViewRootImpl 的同步屏障被外部异步更新阻塞。
  4. 火焰图关联:将耗时消息的时间戳与 Systrace 切片交叉验证,定位到具体方法栈。
排查规则示例:卡顿日志中出现 “Dispatching to Handler (com.example.app.MyActivity$1)” 且耗时 1200ms → 展开 Systrace 发现在该 Handler 内部持锁 1100ms → 定位到 db 查询未做异步化。
主线程消息队列监控与卡顿诊断流程 用户输入/UI 事件 MessageQueue 排队 Looper.dispatchMessage() printer (>>/<<) 捕获耗时 耗时>200ms? 帧正常 卡顿/ANR 高危 Choreographer 帧记录 交叉定位具体任务 Tip: 长时间独占 CPU 的任务会 阻塞 Choreographer 回调,导致 doFrame 消失(无 nextFrameTime)。 结合 log 中 “<<<< Finished” 的 消息类型可快速缩小排查范围。

4.4 修复动作:阻止全局队列阻塞

根据定位结果采取相应修复方案:

  • UI 线程磁盘 I/O:迁移至 WorkManager / RxJava / 协程的 IO 线程池。
  • Bitmap 解码 & 复杂计算:利用 BitmapFactory 的 Options.inJustDecodeBounds 预取尺寸,或在子线程 decode 后 post 回主线程。
  • 同步锁竞争:将长临界区拆细、改用读写锁或 CopyOnWrite 容器。
  • 布局过度绘制:使用 ConstraintLayout 减少嵌套,配合 AsyncLayoutInflater 延迟加载。
  • 频繁动画/帧回调:通过 Choreographer 自身做帧率自降级(如 30fps)或取消无用动画。
边界修复案例:某电商首页首帧 ANR 原因:首页接口返回后,主线程直接解析 JSON 并创建 120 个 ViewHolder。修复:解析在子线程完成,主线程仅接收已解析好的 List<ViewModel>,首帧耗时从 3.2s 降至 120ms。

4.5 复测:阈值验证与回归

修复后在上线前执行以下复测清单:

  1. 保持 Looper Printer 和 Choreographer 监控开启,运行 Monkeys + 滑动压测。
  2. 确认单条 Message 耗时不再超过 200ms(白名单任务除外,如插件加载)。
  3. Choreographer 帧间距不低于 20fps(即 50ms/帧)持续 10s 以上。
  4. 故意插入耗时代码(Mock 100ms 运算),验证监控工具仍能捕获并上报。
  5. 灰度验证 24h,观察 ANR 率收敛。

4.6 预防:构建主线程流水线预警体系

将监控组件固化为基础 SDK 能力:

  • 周期性快照:每 10s 采集 Looper 队列深度 + 最新消息耗时,超过阈值时自动 dump 主线程堆栈。
  • 熔断降级:当检测到连续 3 条消息耗时 >500ms,主动暂停非必要动画、后台刷新请求。
  • 白名单机制:对已知慢消息(如 Application.onCreate)设置上限,超过则触发日志 + 埋点上报。
工程实践:建议将 setMessageLogging 的 Printer 替换为自定义 BlockCanary 风格的轻量框架,统一管理取样率、堆栈采集和告警。生产环境使用时务必在 Printer 内部捕获异常,避免监控代码自身引发 ANR。