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 监听每一帧的绘制耗时。
- Looper.setMessageLogging:在主线程 Looper 注册 Printer,框架在每次 dispatch 前后调用 printer.println(msg)。利用前后时间差可精确统计每条 Message 执行耗时。
- 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.setMessageLogging | Choreographer.FrameCallback |
|---|---|---|
| 监测粒度 | 每条 Message/Runnable | 每帧(16.7ms 基准) |
| 捕获瓶颈 | 直接获知耗时任务(如 IO、Bitmap 解码) | 反映全帧耗时(含布局、绘制、动画) |
| 开销 | 每次 println 约 0.02ms,生产可接受 | 需频繁 postFrameCallback,低负载可用 |
| 局限性 | 不能识别多帧累积卡顿,需配合 Choreographer | 只能知道帧时长,无法直接定位具体 Message |
| 最佳实践 | 线上只 Log 耗时 > 200ms 的消息 | Debug 包全量,Release 包抽样 |
4.3 原因定位:基于监测数据的故障树
当检测工具上报卡顿时,使用以下排查规则树快速锁定根因:
- 判断消息类型:从 Looper 日志中的 “Dispatching to” 关键字识别 Handler 或 Runnable 来源。
- 排除系统输入:若消息来自 InputManager,检查当前焦点 View 的 onTouchEvent/onKeyDown 是否有耗时操作。
- 检查同步屏障:若出现连续的 FrameCallback 延迟,可能是 ViewRootImpl 的同步屏障被外部异步更新阻塞。
- 火焰图关联:将耗时消息的时间戳与 Systrace 切片交叉验证,定位到具体方法栈。
排查规则示例:卡顿日志中出现 “Dispatching to Handler (com.example.app.MyActivity$1)” 且耗时 1200ms → 展开 Systrace 发现在该 Handler 内部持锁 1100ms → 定位到 db 查询未做异步化。
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 复测:阈值验证与回归
修复后在上线前执行以下复测清单:
- 保持 Looper Printer 和 Choreographer 监控开启,运行 Monkeys + 滑动压测。
- 确认单条 Message 耗时不再超过 200ms(白名单任务除外,如插件加载)。
- Choreographer 帧间距不低于 20fps(即 50ms/帧)持续 10s 以上。
- 故意插入耗时代码(Mock 100ms 运算),验证监控工具仍能捕获并上报。
- 灰度验证 24h,观察 ANR 率收敛。
4.6 预防:构建主线程流水线预警体系
将监控组件固化为基础 SDK 能力:
- 周期性快照:每 10s 采集 Looper 队列深度 + 最新消息耗时,超过阈值时自动 dump 主线程堆栈。
- 熔断降级:当检测到连续 3 条消息耗时 >500ms,主动暂停非必要动画、后台刷新请求。
- 白名单机制:对已知慢消息(如 Application.onCreate)设置上限,超过则触发日志 + 埋点上报。
工程实践:建议将 setMessageLogging 的 Printer 替换为自定义 BlockCanary 风格的轻量框架,统一管理取样率、堆栈采集和告警。生产环境使用时务必在 Printer 内部捕获异常,避免监控代码自身引发 ANR。