将 Choreographer 和 Looper 结合使用,是构建一个强大、全面的 Android 应用流畅度监控体系的核心。这种组合能够从两个最关键且互补的维度来度量性能:Choreographer 监控渲染结果(帧率),Looper 监控原因(消息执行耗时)。
下面我来详细讲解如何将两者结合,实现从"发现问题"到"定位问题"的完整监控链路。
1. 核心原理:双剑合璧
-
Choreographer------ 结果监控器(检测"病了") 它就像一位严格的监工,在每一帧渲染开始前都会通过doFrame回调通知我们。通过记录两次回调的时间间隔,我们可以精确计算出是否发生了掉帧。如果间隔大于16ms(60Hz刷新率),就说明这一帧"生病了"(卡顿)。它的优势在于直接反映用户的视觉感受,是衡量流畅度的黄金标准。 -
Looper------ 原因监控器(诊断"病因") 它则像一位手术室里的记录员,监控着主线程这个"手术台"上每一个任务(Message)的执行情况。通过设置Printer,我们可以记录下每个消息开始和结束的日志,从而计算出这个任务的执行耗时。一旦Choreographer报告"生病了",我们就可以从Looper的记录中,找到是哪段"手术"(哪个消息)耗时过长,导致了这场"事故"。它的优势在于能够定位到具体的耗时方法和代码。
2. 结合实战:从Matrix框架看最佳实践
业界顶尖的APM框架,如腾讯的Matrix,正是将这种结合发挥到了极致。它通过一个名为 UIThreadMonitor 的组件,优雅地整合了 Looper 和 Choreographer 的能力。
2.1 核心思路:Hook关键节点
UIThreadMonitor 的核心思想是,在系统渲染流程的关键节点"埋下"自己的监控代码,从而精确测量每一阶段的耗时。这主要通过以下方式实现:
- 监听Looper :注册一个
LooperMonitor,用于监听主线程每条消息处理的开始和结束,对应dispatchBegin和dispatchEnd方法。 - Hook Choreographer :通过反射,将自定义的、用于统计耗时的
Runnable插入到Choreographer内部维护的CallbackQueue的头部 。这样做是为了确保我们的监控代码能在系统的Input、Animation、Traversal等回调之前被执行,从而能够完整记录这些核心渲染阶段的耗时。
2.2 完整的数据采集流程
当一个VSync信号到来时,整个监控流程就像一场精密的"接力赛":
- VSync信号到达:系统底层通知应用层开始新的一帧渲染。
Choreographer调度开始 :Choreographer开始处理这一帧。由于我们已经通过反射在队列头部插入了任务,我们的UIThreadMonitor(它本身是一个Runnable)会率先被调用。- Input阶段耗时记录 :在
UIThreadMonitor的run()方法中,它立即记录下Input阶段开始的标记(doQueueBegin(CALLBACK_INPUT)),然后马上将下一个用于监控Animation阶段的Runnable插入队列头部,最后结束Input阶段的耗时记录(doQueueEnd(CALLBACK_INPUT))。 - Animation阶段耗时记录 :系统接着处理Animation回调,此时我们插入的第二个
Runnable被执行。它同样先记录Animation阶段结束(同时也是绘制阶段开始),再插入用于监控Traversal阶段的Runnable。 - Traversal阶段耗时记录 :系统处理Traversal回调,我们插入的第三个
Runnable被执行,记录下Traversal阶段的结束时间。 - 渲染完成,通知监听者 :最终,当
Looper的dispatchEnd方法被调用时,说明这一帧的所有UI工作已经处理完毕。UIThreadMonitor会在这里收集所有阶段记录下的耗时数据(queueCost),并通过doFrame回调通知给所有注册的观察者(LooperObserver),从而实现了帧率的全面监控。
3. 不同场景下的组合策略
根据监控目标和场景的不同,我们可以灵活选择这两种工具的组合方式:
| 监控目标 | 核心策略 | 特点 |
|---|---|---|
| 宏观帧率 & 整体流畅度 | 单独使用 Choreographer 的 FrameCallback,连续计算两帧之间的时间差。 |
非常适合做线上大盘监控,统计应用的全局或页面帧率、卡顿率,快速判断版本质量是变好了还是变差了。 |
| 微观卡顿 & 原因定位 | 将两者结合,形成 "Choreographer 报警,Looper 定位" 的联动模式。 |
用于线下或灰度测试 。当 doFrame 检测到掉帧时,立即触发 Looper 记录的耗时信息和当时的线程堆栈,精确定位是哪个方法导致了卡顿。 |
| 无感知的详细诊断 | 通过反射深入 Choreographer 内部,分离统计Input、Animation、Traversal各阶段的耗时,并结合 Looper 的 dispatch 时间。 |
Matrix方案。优点是数据详尽,能精确到渲染流水线的每个环节,对开发者定位问题帮助最大。缺点是使用了反射,实现复杂,适用于对性能和稳定性要求极高的深度优化场景。 |
4. 总结
Choreographer 和 Looper 的结合,是 Android 性能监控领域非常经典的组合拳。
Choreographer是我们的"眼睛",用来发现哪里卡了。Looper是我们的"放大镜",用来看清为什么卡。
下面提供一个具体的实现示例,展示如何结合 Choreographer 和 Looper 来监控 Android 应用的流畅度。该示例包含两个核心部分:
- 帧率监控 :通过
Choreographer.FrameCallback检测掉帧情况。 - 主线程耗时消息监控 :通过自定义
Printer监听Looper消息执行耗时,并捕获慢调用的堆栈。
最终,当掉帧或慢消息发生时,会打印详细的日志,方便定位问题。
下面提供一个具体的实现示例,展示如何结合 Choreographer 和 Looper 来监控 Android 应用的流畅度。该示例包含两个核心部分:
- 帧率监控 :通过
Choreographer.FrameCallback检测掉帧情况。 - 主线程耗时消息监控 :通过自定义
Printer监听Looper消息执行耗时,并捕获慢调用的堆栈。
最终,当掉帧或慢消息发生时,会打印详细的日志,方便定位问题。
代码实现
1. 定义数据结构和常量
java
public class JankMonitor {
private static final long FRAME_INTERVAL_60FPS = 16_000_000L; // 16ms in nanoseconds
private static final long FRAME_INTERVAL_120FPS = 8_333_333L; // 120Hz
private static final long SLOW_MESSAGE_THRESHOLD = 100L; // 100ms 认为是慢消息
private static final long JANK_THRESHOLD = 200L; // 掉帧超过200ms才记录(避免短时抖动)
private static volatile JankMonitor sInstance;
private boolean isMonitoring = false;
private long lastFrameTimeNanos;
private MainLooperPrinter looperPrinter;
}
2. 实现 Looper Printer(监控主线程消息)
java
private static class MainLooperPrinter implements Printer {
private long startTimeMillis;
private String startStack; // 消息开始时的堆栈
@Override
public void println(String x) {
if (x.startsWith(">>>>> Dispatching to")) {
// 消息开始
startTimeMillis = System.currentTimeMillis();
// 可选:记录当前堆栈,用于精确定位代码位置
startStack = getStackTraceString();
} else if (x.startsWith("<<<<< Finished to")) {
// 消息结束
long cost = System.currentTimeMillis() - startTimeMillis;
if (cost >= SLOW_MESSAGE_THRESHOLD) {
Log.w("JankMonitor", "Slow message detected, cost=" + cost + "ms\n" + startStack);
// 这里可以上报到APM或写入文件
}
}
}
private String getStackTraceString() {
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
StringBuilder sb = new StringBuilder();
// 跳过无关的调用(如Looper.loop本身)
for (StackTraceElement element : stackTrace) {
if (element.getClassName().contains("android.os.Looper")) continue;
sb.append("\tat ").append(element).append("\n");
}
return sb.toString();
}
}
3. 实现 Choreographer FrameCallback(监控帧)
java
private Choreographer.FrameCallback frameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
if (lastFrameTimeNanos == 0) {
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
return;
}
long diff = frameTimeNanos - lastFrameTimeNanos;
// 计算掉帧数(基于60Hz)
long expectedInterval = FRAME_INTERVAL_60FPS; // 可根据设备刷新率调整
long jankCount = diff / expectedInterval - 1; // 减1是因为当前帧本身也算一次
if (jankCount > 0) {
long jankTimeMs = diff / 1_000_000L; // 转毫秒
if (jankTimeMs >= JANK_THRESHOLD) {
Log.w("JankMonitor", "Jank detected! " + jankCount + " frames dropped, cost=" + jankTimeMs + "ms");
// 此时可以主动触发一次堆栈抓取,看看主线程在做什么
captureMainThreadStack();
}
}
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
};
private void captureMainThreadStack() {
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
StringBuilder sb = new StringBuilder("Main thread stack when jank:\n");
for (StackTraceElement element : stackTrace) {
// 过滤系统无关调用,突出重点
if (!element.getClassName().startsWith("android.") && !element.getClassName().startsWith("java.")) {
sb.append("\tat ").append(element).append("\n");
}
}
Log.w("JankMonitor", sb.toString());
}
4. 启动与停止监控
java
public static JankMonitor getInstance() {
if (sInstance == null) {
synchronized (JankMonitor.class) {
if (sInstance == null) {
sInstance = new JankMonitor();
}
}
}
return sInstance;
}
public void start() {
if (isMonitoring) return;
isMonitoring = true;
// 1. 开始 Choreographer 监控
lastFrameTimeNanos = 0;
Choreographer.getInstance().postFrameCallback(frameCallback);
// 2. 设置 Looper Printer
looperPrinter = new MainLooperPrinter();
Looper.getMainLooper().setMessageLogging(looperPrinter);
Log.i("JankMonitor", "JankMonitor started");
}
public void stop() {
if (!isMonitoring) return;
isMonitoring = false;
// 1. 移除 Choreographer 回调
Choreographer.getInstance().removeFrameCallback(frameCallback);
// 2. 清除 Looper Printer
Looper.getMainLooper().setMessageLogging(null);
looperPrinter = null;
Log.i("JankMonitor", "JankMonitor stopped");
}
使用方式
在 Application 或合适的时机启动监控:
java
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
// 建议只在 Debug 模式或特定开关下开启,避免线上性能损耗
if (BuildConfig.DEBUG) {
JankMonitor.getInstance().start();
}
}
}
关键点说明
-
双监控互补:
- Choreographer 直接反映用户感知的掉帧,能发现卡顿。
- Looper Printer 记录每个消息的耗时,当卡顿发生时,可以配合堆栈定位到具体哪个消息耗时过长。
-
堆栈抓取策略:
- 在
MainLooperPrinter中,我们只记录慢消息的堆栈(超过100ms),避免频繁抓栈影响性能。 - 在
Choreographer掉帧时,我们主动抓取一次当前主线程堆栈,帮助确认卡顿时主线程在做什么。
- 在
-
性能考虑:
Thread.getStackTrace()有一定开销,建议只在满足条件时调用(如消息耗时超过阈值、掉帧超过阈值)。- 线上环境可以关闭堆栈记录,只保留统计,或者使用采样策略。
-
拓展性:
- 可以修改阈值以适配不同刷新率设备(通过
WindowManager获取实际刷新率)。 - 可以将采集到的数据上报到 APM 平台,用于线上监控。
- 可以修改阈值以适配不同刷新率设备(通过
-
注意事项:
Looper.setMessageLogging可能会与某些库(如某些日志框架)冲突,建议在停止监控时恢复原 Printer。- 在 Android 4.3 以下,
Choreographer可能不可用,需要做兼容处理。
我们来深入分析一下 Matrix 框架中关于 Choreographer 和 Looper 结合使用的核心源码。Matrix 的实现远比我们之前手写的示例要复杂和严谨,它通过巧妙的反射 和回调插入机制,实现了对每一帧渲染的**三个阶段(Input、Animation、Traversal)**的单独耗时统计。
核心思想是:通过 LooperMonitor 监听主线程消息,通过 UIThreadMonitor 利用反射向 Choreographer 内部插入自己的 Runnable,从而将一帧的渲染过程拆解开,精确测量每一阶段的耗时,并最终将数据分发给像 FrameTracer 这样的观察者。
1. 基石:LooperMonitor 监听主线程消息
LooperMonitor 是整个监控体系的基础,它的职责就是精准地知道主线程什么时候开始处理一个消息,什么时候处理完毕。
- 核心原理 :通过
Looper.getMainLooper().setMessageLogging(printer)设置一个自定义的Printer。 - 关键实现 :
LooperPrinter会在每个消息分发前后打印特定格式的日志。LooperMonitor拦截这些日志,从而回调dispatchStart和dispatchEnd方法,通知所有注册的监听器(比如UIThreadMonitor)。 - 细节处理 :为了防止被其他库覆盖,它还通过
IdleHandler定期检查并重置Printer。
2. 核心引擎:UIThreadMonitor 的初始化与 Hook
UIThreadMonitor 是整个流畅度监控的核心引擎,它巧妙地将 LooperMonitor 的消息回调和 Choreographer 的帧回调结合在一起。
2.1 初始化:反射获取内部对象和方法
在 init() 方法中,它通过反射获取了 Choreographer 内部的核心对象,为后续插入监控代码做准备 :
java
// 代码摘自 Matrix UIThreadMonitor.init() 简化版
public void init(TraceConfig config) {
// 1. 获取主线程的 Choreographer 实例
choreographer = Choreographer.getInstance();
// 2. 反射获取 Choreographer 内部的对象锁,用于同步
callbackQueueLock = ReflectUtils.reflectObject(choreographer, "mLock");
// 3. 反射获取 CallbackQueue 数组,系统用这个数组管理三种类型的回调
callbackQueues = ReflectUtils.reflectObject(choreographer, "mCallbackQueues");
// 4. 反射获取向这三种队列添加回调的方法 (addCallbackLocked)
addInputQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_INPUT], "addCallbackLocked", ...);
addAnimationQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_ANIMATION], "addCallbackLocked", ...);
addTraversalQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_TRAVERSAL], "addCallbackLocked", ...);
// 5. 注册 Looper 监听,以便收到消息开始和结束的回调
LooperMonitor.register(looperDispatchListener);
}
2.2 启动:向 Input 队列头部插入自己的 Runnable
当 UIThreadMonitor 启动时,它做的第一件事就是通过反射,将自己(它本身是一个 Runnable)以头部插入 的方式添加到 CALLBACK_INPUT 类型的队列中 。
java
// 代码摘自 Matrix UIThreadMonitor.onStart() 简化版
@Override
public synchronized void onStart() {
// ... 初始化状态数组 ...
// 将 this (UIThreadMonitor 自身) 作为头部插入到 Input 回调队列
addFrameCallback(CALLBACK_INPUT, this, true); // true 表示插入头部
}
为什么要插入头部? 因为下一帧的 VSync 信号到来时,Choreographer 会按顺序处理 Input、Animation、Traversal 的回调。将我们的 Runnable 插入 Input 队列的头部 ,意味着它会在所有系统 Input 回调之前被首先执行。这为我们提供了在渲染流水线开始前"打点计时"的机会 。
3. 精密的"接力赛":如何拆解三个阶段
当 VSync 信号到来,一场精密的"接力赛"就开始了。UIThreadMonitor 通过不断向后续队列的头部插入新的 Runnable,实现了对每个阶段耗时的测量。
Step 1: Input 阶段开始
- 时机 :VSync 信号到达,
Choreographer开始处理 Input 回调。 - 执行 :由于我们插在头部,
UIThreadMonitor的run()方法被第一个执行 。
java
// 代码摘自 Matrix UIThreadMonitor.run() 简化版
@Override
public void run() {
final long start = System.nanoTime();
// 标记这是一个 VSync 帧
doFrameBegin(token);
// 记录 Input 阶段开始的时间
doQueueBegin(CALLBACK_INPUT);
// 立即向 Animation 队列头部插入下一个任务
addFrameCallback(CALLBACK_ANIMATION, new Runnable() {
@Override
public void run() {
// Step 2 会执行到这里
doQueueEnd(CALLBACK_INPUT); // 记录 Input 阶段结束
doQueueBegin(CALLBACK_ANIMATION); // 记录 Animation 阶段开始
}
}, true); // true 表示插入头部
// 再向 Traversal 队列头部插入下一个任务
addFrameCallback(CALLBACK_TRAVERSAL, new Runnable() {
@Override
public void run() {
// Step 3 会执行到这里
doQueueEnd(CALLBACK_ANIMATION); // 记录 Animation 阶段结束
doQueueBegin(CALLBACK_TRAVERSAL); // 记录 Traversal 阶段开始
}
}, true);
}
Step 2: Animation 阶段
- 时机 :所有 Input 回调执行完毕后,
Choreographer开始处理 Animation 回调。 - 执行 :我们在 Step 1 中插入到 Animation 队列头部的
Runnable被执行。它立刻标记 Input 阶段结束、Animation 阶段开始。
Step 3: Traversal 阶段
- 时机 :所有 Animation 回调执行完毕后,
Choreographer开始处理 Traversal 回调。 - 执行 :我们在 Step 1 中插入到 Traversal 队列头部的
Runnable被执行。它立刻标记 Animation 阶段结束、Traversal 阶段开始。
Step 4: 阶段结束与数据分发
- 时机:当所有 Traversal 回调(即 measure/layout/draw)执行完毕后,这一帧的渲染工作就结束了。
- 执行 :
LooperMonitor的dispatchEnd回调被触发 。UIThreadMonitor在dispatchEnd中会调用doFrameEnd(token),最终记录下 Traversal 阶段的结束时间 。
java
// 代码摘自 Matrix UIThreadMonitor.dispatchEnd() 简化版
private void dispatchEnd() {
// ...
if (isVsyncFrame) {
doFrameEnd(token); // 在这里调用 doQueueEnd(CALLBACK_TRAVERSAL),结束最后一个阶段
}
long endNs = System.nanoTime();
// 将收集到的三个阶段耗时 (queueCost数组) 分发给所有观察者
for (LooperObserver observer : observers) {
observer.doFrame(..., startNs, endNs, ...,
queueCost[CALLBACK_INPUT],
queueCost[CALLBACK_ANIMATION],
queueCost[CALLBACK_TRAVERSAL]);
}
// ...
}
4. 数据消费者:FrameTracer 的应用
像 FrameTracer 这样的组件,只需要调用 UIThreadMonitor.getMonitor().addObserver(looperObserver) 注册自己,就能在 doFrame 回调中拿到这一帧三个阶段的详细耗时数据 。根据这些数据和开发者设定的阈值,FrameTracer 就能判断是否发生了卡顿,甚至能分析出卡顿是发生在输入响应、动画计算还是布局绘制阶段,从而为开发者提供更精确的定位信息。
总结
Matrix 的实现精髓在于:
- 分层监控 :
LooperMonitor管消息,UIThreadMonitor管帧。 - 反射 Hook :通过反射获取
Choreographer内部队列的添加方法。 - 头部插入 :将自己的
Runnable插入队列头部,抢在系统逻辑之前执行,实现"打点" 。 - 接力计时:通过在不同类型的回调队列中接力插入任务,完整记录了一帧内三个阶段各自的耗时。
这样,Matrix 就构建了一个强大且细致的流畅度监控体系,不仅知道"卡了",还知道"是哪个阶段卡了"。
如果你对某个特定部分(比如 LooperMonitor 如何保证 Printer 不被覆盖,或者 FrameTracer 的具体计算逻辑)感兴趣,我们可以继续深入。你也可以直接查阅 Matrix 的官方源码:
- LooperMonitor : github.com/Tencent/mat...
- UIThreadMonitor : 在
matrix-trace-canary模块下。