Android Choreographer 和 looper 结合使用 监控

ChoreographerLooper 结合使用,是构建一个强大、全面的 Android 应用流畅度监控体系的核心。这种组合能够从两个最关键且互补的维度来度量性能:Choreographer 监控渲染结果(帧率),Looper 监控原因(消息执行耗时)

下面我来详细讲解如何将两者结合,实现从"发现问题"到"定位问题"的完整监控链路。

1. 核心原理:双剑合璧

  • Choreographer ------ 结果监控器(检测"病了") 它就像一位严格的监工,在每一帧渲染开始前都会通过 doFrame 回调通知我们。通过记录两次回调的时间间隔,我们可以精确计算出是否发生了掉帧。如果间隔大于16ms(60Hz刷新率),就说明这一帧"生病了"(卡顿)。它的优势在于直接反映用户的视觉感受,是衡量流畅度的黄金标准。

  • Looper ------ 原因监控器(诊断"病因") 它则像一位手术室里的记录员,监控着主线程这个"手术台"上每一个任务(Message)的执行情况。通过设置Printer,我们可以记录下每个消息开始和结束的日志,从而计算出这个任务的执行耗时。一旦 Choreographer 报告"生病了",我们就可以从 Looper 的记录中,找到是哪段"手术"(哪个消息)耗时过长,导致了这场"事故"。它的优势在于能够定位到具体的耗时方法和代码

2. 结合实战:从Matrix框架看最佳实践

业界顶尖的APM框架,如腾讯的Matrix,正是将这种结合发挥到了极致。它通过一个名为 UIThreadMonitor 的组件,优雅地整合了 LooperChoreographer 的能力。

2.1 核心思路:Hook关键节点

UIThreadMonitor 的核心思想是,在系统渲染流程的关键节点"埋下"自己的监控代码,从而精确测量每一阶段的耗时。这主要通过以下方式实现:

  1. 监听Looper :注册一个 LooperMonitor,用于监听主线程每条消息处理的开始和结束,对应 dispatchBegindispatchEnd 方法。
  2. Hook Choreographer :通过反射,将自定义的、用于统计耗时的 Runnable 插入到 Choreographer 内部维护的 CallbackQueue头部 。这样做是为了确保我们的监控代码能在系统的Input、Animation、Traversal等回调之前被执行,从而能够完整记录这些核心渲染阶段的耗时。

2.2 完整的数据采集流程

当一个VSync信号到来时,整个监控流程就像一场精密的"接力赛":

  1. VSync信号到达:系统底层通知应用层开始新的一帧渲染。
  2. Choreographer 调度开始Choreographer开始处理这一帧。由于我们已经通过反射在队列头部插入了任务,我们的 UIThreadMonitor(它本身是一个Runnable)会率先被调用。
  3. Input阶段耗时记录 :在 UIThreadMonitorrun() 方法中,它立即记录下Input阶段开始的标记(doQueueBegin(CALLBACK_INPUT)),然后马上将下一个用于监控Animation阶段的 Runnable 插入队列头部,最后结束Input阶段的耗时记录(doQueueEnd(CALLBACK_INPUT))。
  4. Animation阶段耗时记录 :系统接着处理Animation回调,此时我们插入的第二个 Runnable 被执行。它同样先记录Animation阶段结束(同时也是绘制阶段开始),再插入用于监控Traversal阶段的 Runnable
  5. Traversal阶段耗时记录 :系统处理Traversal回调,我们插入的第三个 Runnable 被执行,记录下Traversal阶段的结束时间。
  6. 渲染完成,通知监听者 :最终,当 LooperdispatchEnd 方法被调用时,说明这一帧的所有UI工作已经处理完毕。UIThreadMonitor 会在这里收集所有阶段记录下的耗时数据(queueCost),并通过 doFrame 回调通知给所有注册的观察者(LooperObserver),从而实现了帧率的全面监控。

3. 不同场景下的组合策略

根据监控目标和场景的不同,我们可以灵活选择这两种工具的组合方式:

监控目标 核心策略 特点
宏观帧率 & 整体流畅度 单独使用 ChoreographerFrameCallback,连续计算两帧之间的时间差。 非常适合做线上大盘监控,统计应用的全局或页面帧率、卡顿率,快速判断版本质量是变好了还是变差了。
微观卡顿 & 原因定位 将两者结合,形成 "Choreographer 报警,Looper 定位" 的联动模式。 用于线下或灰度测试 。当 doFrame 检测到掉帧时,立即触发 Looper 记录的耗时信息和当时的线程堆栈,精确定位是哪个方法导致了卡顿。
无感知的详细诊断 通过反射深入 Choreographer 内部,分离统计Input、Animation、Traversal各阶段的耗时,并结合 Looperdispatch 时间。 Matrix方案。优点是数据详尽,能精确到渲染流水线的每个环节,对开发者定位问题帮助最大。缺点是使用了反射,实现复杂,适用于对性能和稳定性要求极高的深度优化场景。

4. 总结

ChoreographerLooper 的结合,是 Android 性能监控领域非常经典的组合拳。

  • Choreographer 是我们的"眼睛",用来发现哪里卡了。
  • Looper 是我们的"放大镜",用来看清为什么卡。

下面提供一个具体的实现示例,展示如何结合 ChoreographerLooper 来监控 Android 应用的流畅度。该示例包含两个核心部分:

  1. 帧率监控 :通过 Choreographer.FrameCallback 检测掉帧情况。
  2. 主线程耗时消息监控 :通过自定义 Printer 监听 Looper 消息执行耗时,并捕获慢调用的堆栈。

最终,当掉帧或慢消息发生时,会打印详细的日志,方便定位问题。


下面提供一个具体的实现示例,展示如何结合 ChoreographerLooper 来监控 Android 应用的流畅度。该示例包含两个核心部分:

  1. 帧率监控 :通过 Choreographer.FrameCallback 检测掉帧情况。
  2. 主线程耗时消息监控 :通过自定义 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();
        }
    }
}

关键点说明

  1. 双监控互补

    • Choreographer 直接反映用户感知的掉帧,能发现卡顿。
    • Looper Printer 记录每个消息的耗时,当卡顿发生时,可以配合堆栈定位到具体哪个消息耗时过长。
  2. 堆栈抓取策略

    • MainLooperPrinter 中,我们只记录慢消息的堆栈(超过100ms),避免频繁抓栈影响性能。
    • Choreographer 掉帧时,我们主动抓取一次当前主线程堆栈,帮助确认卡顿时主线程在做什么。
  3. 性能考虑

    • Thread.getStackTrace() 有一定开销,建议只在满足条件时调用(如消息耗时超过阈值、掉帧超过阈值)。
    • 线上环境可以关闭堆栈记录,只保留统计,或者使用采样策略。
  4. 拓展性

    • 可以修改阈值以适配不同刷新率设备(通过 WindowManager 获取实际刷新率)。
    • 可以将采集到的数据上报到 APM 平台,用于线上监控。
  5. 注意事项

    • Looper.setMessageLogging 可能会与某些库(如某些日志框架)冲突,建议在停止监控时恢复原 Printer。
    • 在 Android 4.3 以下,Choreographer 可能不可用,需要做兼容处理。

我们来深入分析一下 Matrix 框架中关于 ChoreographerLooper 结合使用的核心源码。Matrix 的实现远比我们之前手写的示例要复杂和严谨,它通过巧妙的反射回调插入机制,实现了对每一帧渲染的**三个阶段(Input、Animation、Traversal)**的单独耗时统计。

核心思想是:通过 LooperMonitor 监听主线程消息,通过 UIThreadMonitor 利用反射向 Choreographer 内部插入自己的 Runnable,从而将一帧的渲染过程拆解开,精确测量每一阶段的耗时,并最终将数据分发给像 FrameTracer 这样的观察者。

1. 基石:LooperMonitor 监听主线程消息

LooperMonitor 是整个监控体系的基础,它的职责就是精准地知道主线程什么时候开始处理一个消息,什么时候处理完毕

  • 核心原理 :通过 Looper.getMainLooper().setMessageLogging(printer) 设置一个自定义的 Printer
  • 关键实现LooperPrinter 会在每个消息分发前后打印特定格式的日志。LooperMonitor 拦截这些日志,从而回调 dispatchStartdispatchEnd 方法,通知所有注册的监听器(比如 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 回调。
  • 执行 :由于我们插在头部,UIThreadMonitorrun() 方法被第一个执行 。
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)执行完毕后,这一帧的渲染工作就结束了。
  • 执行LooperMonitordispatchEnd 回调被触发 。UIThreadMonitordispatchEnd 中会调用 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 的实现精髓在于:

  1. 分层监控LooperMonitor 管消息,UIThreadMonitor 管帧。
  2. 反射 Hook :通过反射获取 Choreographer 内部队列的添加方法。
  3. 头部插入 :将自己的 Runnable 插入队列头部,抢在系统逻辑之前执行,实现"打点" 。
  4. 接力计时:通过在不同类型的回调队列中接力插入任务,完整记录了一帧内三个阶段各自的耗时。

这样,Matrix 就构建了一个强大且细致的流畅度监控体系,不仅知道"卡了",还知道"是哪个阶段卡了"。

如果你对某个特定部分(比如 LooperMonitor 如何保证 Printer 不被覆盖,或者 FrameTracer 的具体计算逻辑)感兴趣,我们可以继续深入。你也可以直接查阅 Matrix 的官方源码:

相关推荐
城东米粉儿2 小时前
Android inline Hook 笔记
android
城东米粉儿2 小时前
Android 防止 Printer 覆盖笔记
android
Android系统攻城狮6 小时前
Android tinyalsa深度解析之pcm_get_timestamp调用流程与实战(一百一十八)
android·pcm·tinyalsa·android hal·audio hal
yuezhilangniao8 小时前
win10环境变量完全指南:Java、Maven、Android、Flutter -含我的环境备份
android·java·maven
奔跑吧 android9 小时前
【车载Audio】【AudioHal 06】【高通音频架构】【深入浅出 Android Audio HAL:从加载到函数指针绑定的全链路解析】
android·音视频·audioflinger·aosp13·8295·audiohal·高通音频架构
无巧不成书02189 小时前
Kotlin Multiplatform (KMP) 鸿蒙开发整合实战|2026最新方案
android·开发语言·kotlin·harmonyos·kmp
恋猫de小郭18 小时前
丰田正在使用 Flutter 开发游戏引擎 Fluorite
android·前端·flutter