25、线上监控方案:Matrix-Issue、BlockCanary原理、自定义Looper监控与帧率上报

做UI性能优化,最怕什么?

怕的是问题只在线上出现,而你手里只有一台开发机。你在这边测了八百遍,帧率稳如老狗,用户那边却卡成了PPT。我遇到过太多次这种事了——有一次线上反馈说某个页面滑动掉帧严重,我拿同款手机、同版本系统,怎么都复现不出来。后来才发现,是用户手机里装了某个第三方输入法,在特定场景下疯狂抢主线程。

所以,线上监控不是「锦上添花」,而是「保命手段」。今天我们就来聊聊,怎么在线上环境里,把卡顿和帧率问题抓个现行。

核心观点:线上监控的本质,是在不破坏用户体验的前提下,把性能问题「揪」出来。你不需要知道每一帧的细节,但必须知道「哪一帧出了问题」以及「为什么出问题」。

25.1 Matrix-Issue:微信团队的卡顿检测方案

Matrix 是微信开源的一套APM方案。它的核心思路,说白了就是「插桩+堆栈采样」。我最早接触它的时候,觉得这东西太暴力了——直接在编译期往所有方法里插代码,这得多大开销?后来仔细看了源码才发现,人家做得相当克制。

25.1.1 原理:插桩与堆栈采样

Matrix 的卡顿检测,主要分两步:

  1. 编译期插桩:在每一个方法的入口和出口,插入一段极简的统计代码。这段代码只记录方法名、线程ID、时间戳,不做任何复杂计算。
  2. 运行时采样:当主线程的Looper处理完一条消息后,Matrix会检查这条消息的耗时。如果超过阈值(默认200ms),就触发一次堆栈采样,把当前主线程的调用栈dump下来。

你可能会问:插桩会不会影响性能?

嗯,这里要注意。Matrix 的插桩代码,在Release包下会被ProGuard优化掉大部分。而且它只插桩「应用自己的代码」,系统方法和第三方库是不插的。所以实际运行时,额外开销可以控制在1%以内。我在项目中实测过,开启Matrix后帧率几乎没变化。

我的经验:Matrix 的阈值不要设得太低。200ms是微信团队的经验值,如果你设成100ms,线上会收到大量「假阳性」报告——很多卡顿其实是GC引起的,跟代码没关系。我建议先设300ms跑一周,再根据数据慢慢下调。

25.1.2 核心代码:Matrix 的卡顿监听器

// Matrix 的卡顿监听核心逻辑(简化版)
public class LooperMonitor implements Printer {
    private long mStartTime;

    @Override
    public void println(String x) {
        if (x.startsWith(">>>>")) {
            // 消息开始处理
            mStartTime = System.currentTimeMillis();
        } else if (x.startsWith("<<<<")) {
            // 消息处理完毕
            long cost = System.currentTimeMillis() - mStartTime;
            if (cost > THRESHOLD_MS) {
                // 触发堆栈采样
                dumpStack("UI thread blocked for " + cost + "ms");
            }
        }
    }
}

这段代码看着简单,但实际生产环境里要考虑的事情很多:比如多线程并发、堆栈采样的频率控制、日志的压缩上报等等。Matrix 把这些都封装好了,你直接用就行。

25.2 BlockCanary:轻量级的卡顿检测方案

BlockCanary 是另一个经典方案。它的思路和 Matrix 不太一样——Matrix 是「插桩+采样」,BlockCanary 是「Looper监控+堆栈快照」。

25.2.1 原理:利用Looper的Printer

Android 的 Looper 类里,有一个 setMessageLogging() 方法。你可以传入一个 Printer,这样每次Looper处理消息前后,都会回调这个Printer。BlockCanary 就是利用这个机制,在消息处理前后打时间戳,如果耗时超过阈值,就立刻dump主线程堆栈。

说白了,BlockCanary 比 Matrix 更轻量——它不需要编译期插桩,纯运行时监控。但缺点也很明显:它只能知道「主线程卡了」,但不知道「是哪个方法导致的」。因为堆栈采样只能拿到当前正在执行的方法,如果卡顿是由多个方法累积造成的,BlockCanary 就抓不准了。

避坑指南:我曾经在一个项目里同时用了 Matrix 和 BlockCanary,结果线上日志量暴增,用户手机存储差点被撑爆。后来我学乖了:线上环境只开一个监控方案,另一个作为「兜底」在本地调试时用。你想想看,两个方案同时上报同样的卡顿,除了浪费流量和存储,没有任何意义。

25.2.2 使用示例

// BlockCanary 的初始化(简化版)
public class BlockCanary {
    public static void install(Context context) {
        Looper.getMainLooper().setMessageLogging(new Printer() {
            @Override
            public void println(String x) {
                // 解析日志,判断是否卡顿
                if (isBlock(x)) {
                    dumpStack();
                }
            }
        });
    }
}

你看,代码就这么几行。但实际使用中,你还需要处理堆栈的格式化、日志的压缩、上报策略等等。BlockCanary 的源码里把这些都实现了,建议你读一读,尤其是它的堆栈去重逻辑——写得相当漂亮。

25.3 自定义Looper监控:自己动手,丰衣足食

有时候,现成的方案不一定满足你的需求。比如,你只想监控某个特定页面的卡顿,或者你想在卡顿时自动录屏。这时候,自己写一个 Looper 监控器就很有必要了。

25.3.1 核心思路

自定义 Looper 监控的核心,就是替换 Looper 的 Printer。你可以自己实现一个 Printer,在里面做三件事:

  1. 记录消息开始时间
  2. 记录消息结束时间
  3. 如果耗时超过阈值,触发自定义回调(比如上报、录屏、打日志)

我个人的习惯是,在自定义监控器里加一个「采样率」参数。线上环境设成10%,只在10%的用户设备上开启监控。这样既能发现问题,又不会影响大多数用户。

25.3.2 代码实现

public class CustomLooperMonitor {
    private static final long THRESHOLD = 200; // 200ms
    private long mStartTime;

    public void start() {
        Looper.getMainLooper().setMessageLogging(new Printer() {
            @Override
            public void println(String x) {
                if (x.startsWith(">>>>")) {
                    mStartTime = System.nanoTime();
                } else if (x.startsWith("<<<<")) {
                    long cost = (System.nanoTime() - mStartTime) / 1_000_000;
                    if (cost > THRESHOLD) {
                        // 自定义回调
                        onBlockDetected(cost, Looper.getMainLooper().getThread().getStackTrace());
                    }
                }
            }
        });
    }

    private void onBlockDetected(long cost, StackTraceElement[] stack) {
        // 上报到你的APM平台
        // 或者保存到本地,等用户反馈时上传
    }
}

小技巧:System.nanoTime() 而不是 currentTimeMillis()。nanoTime 的精度更高,而且不受系统时间调整的影响。我在一个金融类项目里吃过亏——用户手机时间被自动校准,导致所有卡顿监控数据全部错乱。从那以后,我所有的时间测量都改用 nanoTime 了。

25.4 帧率上报:从Choreographer到线上数据

卡顿监控只能告诉你「卡了」,但帧率数据能告诉你「卡得多严重」。线上帧率上报,是衡量UI性能最直观的指标。

25.4.1 原理:监听Choreographer的回调

Android 的 Choreographer 是帧率控制的「总指挥」。每一帧开始前,Choreographer 都会回调它的监听器。你可以在监听器里记录时间戳,然后计算两帧之间的间隔。如果间隔超过16.67ms(60fps),就说明这一帧掉帧了。

但这里有个坑:Choreographer 的回调是在主线程执行的。如果你在回调里做复杂计算,反而会加剧卡顿。所以,帧率采集一定要轻量——只记录时间戳,不做任何分析。

25.4.2 代码实现

public class FpsMonitor {
    private long mLastFrameTime = 0;
    private int mFrameCount = 0;
    private long mStartTime = 0;

    public void start() {
        Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
            @Override
            public void doFrame(long frameTimeNanos) {
                long now = System.currentTimeMillis();
                if (mLastFrameTime == 0) {
                    mLastFrameTime = now;
                    mStartTime = now;
                } else {
                    long delta = now - mLastFrameTime;
                    if (delta > 16.67) {
                        // 掉帧了
                        mFrameCount++;
                    }
                    mLastFrameTime = now;
                }

                // 每秒上报一次帧率
                if (now - mStartTime >= 1000) {
                    float fps = (mFrameCount * 1000f) / (now - mStartTime);
                    reportFps(fps);
                    mFrameCount = 0;
                    mStartTime = now;
                }

                // 继续监听下一帧
                Choreographer.getInstance().postFrameCallback(this);
            }
        });
    }

    private void reportFps(float fps) {
        // 上报到APM平台
    }
}

注意:Choreographer 的回调在App进入后台后会停止。所以你的帧率上报逻辑里,需要监听应用的前后台切换。否则,用户把App切到后台再切回来,你会收到一个「假高帧率」——因为后台期间没有帧回调,时间差被算成了一帧。

25.5 知识体系总览

为了让你更直观地理解这几个方案的关系,我画了一张图:

线上监控方案知识体系 线上卡顿监控 Matrix-Issue 编译期插桩 + 堆栈采样 阈值:200ms(可调) BlockCanary Looper Printer + 堆栈快照 无需插桩,纯运行时 自定义Looper监控 自定义Printer + 灵活回调 帧率上报 Choreographer回调 + 每秒上报

25.6 总结与建议

好了,我们来捋一捋这几个方案怎么选:

方案 优点 缺点 适用场景
Matrix-Issue 能精确定位到具体方法 需要编译期插桩,集成成本高 大型项目,需要精细化分析
BlockCanary 轻量,集成简单 只能知道卡顿,不知道原因 中小型项目,快速接入
自定义Looper监控 灵活,可定制 需要自己维护 有特殊需求的场景
帧率上报 直观,能衡量整体流畅度 无法定位具体问题 所有项目都建议接入

我个人建议,线上环境至少保留一个卡顿监控方案(Matrix或BlockCanary二选一)加上帧率上报。卡顿监控帮你「抓凶手」,帧率上报帮你「看趋势」。两者配合,才能把线上性能问题看得明明白白。

最后说一句:监控方案再好,也只是「事后诸葛亮」。真正的高手,是在写代码的时候就想着「这行代码会不会卡」。线上监控是底线,但不是全部。把性能意识刻在骨子里,才是真正的优化之道。

公众号:蓝海资料掘金营,微信deep3321