9. Android <卡顿九>解剖Matrix卡顿监控:微信Matrix源码深度分析解读(卡顿原理)

前面讲了Matrix的集成和使用!

突破性性能监控方案Matrix---揭开微信亿级用户背后的流畅秘密 (卡顿监控工具集成)

TraceCanary----ANR,卡顿,慢函数监控

TracePlugin 它是tracer管理器,其内部定义了四个跟踪器。

TraceCanary 分为慢方法、ANR、帧率,启动、四个模块

需要注意的是 TracePlugin 包含了四个 Tracer,暂且取名为追踪器:

  • AnrTracer:ANR 追踪器。

  • EvilMethodTracer:慢函数追踪器;

  • FrameTracer:帧率追踪器;
  • StartupTracer:启动追踪器;

UIThreadMonitor已经被丢弃了

官网的文档说明: github.com

最本质的2个问题: 主线程的监控和方法插桩耗时计算

1. ANR监控

Matrix 使用双机制监控 ANR,确保准确性和可靠性。

tag : plugin_anr

ANR有2个: LooperAnrTracer和SignalAnrTracer

1.1.LooperAnrTracer ANR监测(Looper)

Matrix的LooperAnrTracer是ANR监控的核心组件之一,它通过监控主线程Looper的消息处理机制来检测ANR。

主线程耗时监控(printer) UI卡顿有2个地方,1.主线程 2. 绘制是否在16.6ms完成

本文主要记录 Matrix 监听主线程的方式:

  • 利用 Choreographer 接收 VSync 信号监听每帧回调;
  • 利用 Looper 中的 Printer 对象获取事件处理前后的信息。

1.1.2 核心原理

LooperAnrTracer基于Looper的Printer机制,通过设置自定义的Printer来监控主线程消息处理的开始和结束时间。当消息处理时间超过阈值(默认5秒)时,判定为ANR发生。

主线程所有执行的任务都在 dispatchMessage 方法中派发执行完成,我们通过 setMessageLogging 的方式给主线程的 Looper 设置一个 Printer ,因为 dispatchMessage 执行前后都会打印对应信息。我们可以计算出执行前后的时间花费。

1.1.3 实现细节:looper打点

  • 使用 Looper.getMainLooper().setMessageLogging(printer) 设置自定义 Printer
  • Printerprintln 方法在消息处理前后被调用,计算时间差。

计算出执行前后的时间花费: 开始时间和结束时间, <<<<<<<< 或者>>>>>>>>

(老版本) ANR 监控原理:在 Looper 分发消息时,往后台线程插入一个延时(5s 后执行)任务,Looper 消息分发完毕后就删除,如果过了 5s,该任务未 被删除,就认为出现了 ANR。

1.1.4 LooperAnrTracer源码分析

typescript 复制代码
public class LooperAnrTracer extends Tracer {
    private static final String TAG = "Matrix.LooperAnrTracer";
    
    // ANR检测阈值,默认5秒
    private long mAnrThresholdMs = 5000;
    
    // 是否启用ANR监控
    private boolean mIsAnrTraceEnable = true;
    
    // ANR处理器
    private AnrHandle mAnrHandle;
    
    @Override
    public void onStart() {
        super.onStart();
        if (mIsAnrTraceEnable) {
            // 初始化ANR处理器
            mAnrHandle = new AnrHandle(mAnrThresholdMs);
            // 注册Looper监控
            LooperMonitor.register(new LooperMonitor.LooperDispatchListener() {
                @Override
                public void onDispatchStart(String x) {
                    // 消息分发开始
                    mAnrHandle.start(x);
                }

                @Override
                public void onDispatchEnd(String x) {
                    // 消息分发结束
                    mAnrHandle.stop(x);
                }
            });
        }
    }
}

LooperAnrTracer不是一种严格意义上的anr监控,它基于消息机制,通过一个延时5s的逻辑来操作,5s内消息没有被执行就认为发生了anr,这种监控方式监控anr成功捕获的几率很低,并且真的上报了问题也不能表示应用就一定出现了anr的情况,只能说明出现了卡顿

1.2 SignalAnrTracer(Linux的信号机制)

1.2.1 SignalAnrTracer介绍

SignalAnrTracer是严格意义上的anr监控。它基于Linux的信号机制,通过对SIGQUIT信号的监听,再加上一些辅助性的验证逻辑,实现了一个完善的ANR监控方案,在微信上平稳运行了很长时间,可靠性得到了验证。

SignalAnrTracer 是 Matrix 中用于 ANR 监控的另一个重要组件,它通过监听 Linux 信号(特别是 SIGQUIT)来检测 ANR 事件。与 LooperAnrTracer 不同,SignalAnrTracer 采用了一种更底层的信号拦截机制,能够更直接地捕获系统级的 ANR 事件。

  • 作用 :确保在 Looper 监控失效时也能捕获 ANR 事件,提高监控的覆盖率。

1.2.2 工作原理: 监听系统触发 ANR 时发出的信号(如 SIGQUIT),作为补充机制

  1. 信号注册

    • onStart() 方法中,通过反射注册自定义的 SIGQUIT 信号处理器
    • 保存原始信号处理器,确保系统原有行为不受影响
  2. 信号捕获

    • 当发生 ANR 时,系统会向进程发送 SIGQUIT 信号
    • 自定义信号处理器捕获该信号,并调用 onAnrSignal() 方法
  3. ANR 处理

    • 在主线程中处理 ANR 事件,避免信号处理函数的限制
    • 收集线程堆栈、进程信息等诊断数据
    • 通过 TracePlugin 上报 ANR 事件
  4. 误报过滤

    • 实现 isRealAnr() 方法,过滤可能的误报情况

1.2.3 系统监控ANR的原理

我们知道当Android系统发现anr的时候会弹窗提示应用无响应,那么在此之前系统都做了哪些处理?简单来说,分为如下几步:

  1. 收集所有相关的进程,拿到它们的进程id,为后边dump进程信息作准备。

  2. AMS开始按照第1步得到的进程顺序依次dump每个进程的堆栈。

  3. AMS开始dump后,流程会进入Debug.dumpJavaBacktraceToFileTimeout方法中,通过sigqueue方法向需要dump堆栈的进程发送SIGQUIT信号。

  4. 每个进程启动后都会创建一个SignalCatcher线程,当SignalCatcher线程收到SIGQUIT信号时,开始dump自身堆栈。从这里也可以发现,anr发生后,只有dump堆栈的行为会在发生ANR的进程中。

1.2.4 SignalAnrTracer源码分析

java 复制代码
public class SignalAnrTracer extends Tracer {
    private static final String TAG = "Matrix.SignalAnrTracer";
    
    // 原始信号处理器
    private static SignalHandler sOriginalSignalHandler;
    // 是否已初始化
    private static volatile boolean sIsInitialized = false;
    
    // ANR 处理器
    private AnrHandle mAnrHandle;
    // ANR 阈值(默认5秒)
    private long mAnrThresholdMs = 5000;
    
    @Override
    public void onStart() {
        super.onStart();
        
        // 初始化 ANR 处理器
        mAnrHandle = new AnrHandle(mAnrThresholdMs);
        
        // 注册信号处理器(仅一次)
        if (!sIsInitialized) {
            synchronized (SignalAnrTracer.class) {
                if (!sIsInitialized) {
                    installSignalHandler();
                    sIsInitialized = true;
                }
            }
        }
    }
    
    // 安装信号处理器
    private void installSignalHandler() {
        try {
            // 获取原始 SIGQUIT 信号处理器
            sOriginalSignalHandler = SignalHandlerUtils.getOriginalSignalHandler("SIGQUIT");
            
            // 注册自定义信号处理器
            SignalHandlerUtils.registerSignalHandler("SIGQUIT", new SignalHandler() {
                @Override
                public void handle(Signal signal) {
                    // ANR 发生时的处理
                    onAnrSignal(signal);
                    
                    // 调用原始信号处理器(保持系统原有行为)
                    if (sOriginalSignalHandler != null) {
                        sOriginalSignalHandler.handle(signal);
                    }
                }
            });
            
            MatrixLog.i(TAG, "Signal handler installed successfully");
        } catch (Exception e) {
            MatrixLog.e(TAG, "Install signal handler failed: " + e.getMessage());
        }
    }
}

总结: SignalAnrTrace的核心功能是基于Linux的信号机制,总结一下SignalAnrTrace的逻辑:

  • 底层设置对SIGQUIT信号的监听。

  • 监听到SIGQUIT信号后再结合主线程的执行状态进一步确认ANR的发生。

  • hook ANR的写入时机,拦截write方法,从而将ANR trace信息写入指定文件。

  • 转发SIGQUIT信号给进程的SignalHandler,继续完成系统ANR的流程

与 LooperAnrTracer 的对比

特性 SignalAnrTracer LooperAnrTracer
检测机制 信号拦截 (SIGQUIT) Looper 消息监控
检测时机 系统认定 ANR 时 消息处理超时时
准确性 高(系统级检测) 较高(应用级检测)
兼容性 较低(依赖反射) 较高(标准 API)
诊断信息 完整系统堆栈 应用线程堆栈

2. 卡顿监控之慢函数追踪器

EvilMethodTracer 耗时函数监测(插桩)

对应报告里面的: tag: Trace_EvilMethod

EvilMethodTracer 是 Matrix 中用于检测慢函数(耗时超过阈值的方法)的核心组件。它通过 ASM 字节码插桩技术结合运行时监控机制,能够精确捕获并分析应用中执行时间过长的函数调用

2.1. 慢函数监测

慢方法监测的原理是在 Looper 分发消息时,计算分发耗时(endMs - beginMs),如果大于阈值(可通过 IDynamicConfig 设置,默认为 700ms),就收集信息并上报。它是基于 ASM 插桩实现的,用于监控界面流畅性、启动耗时、页面切换耗时、慢函数及卡顿等问题

慢函数原理总结
  • ASM 插桩 :在编译阶段,通过 ASM 字节码技术在每个方法的入口和出口插入监控代码(调用 AppMethodBeat.i()AppMethodBeat.o())。
  • 触发机制 :通过 LooperMonitor 监听主线程消息处理。在 onDispatchBeginonDispatchEnd 中记录时间差,如果耗时超过阈值,则收集该时间段内的方法调用栈。
  • 数据分析 :使用 AnalyseTask 分析调用栈,过滤无关方法,生成慢函数报告并上报。

2.2 卡顿慢方法监控架构图

步骤详解

步骤 1: onMethodEnter (插针) - 方法进入时插桩
  • 动作 :在编译期或运行期,通过ASM等字节码操作技术,在所有需要被监控的方法的入口处插入记录代码。
  • 具体代码 :插入 AppMethodBeat.i(1)。这里的数字 1 是一个唯一的方法ID ,是在编译期通过一个常量池(如图中的 2.constant)预先分配好的。
  • 目的 :当一个方法(如方法A)开始执行时,立即记录一个时间戳。这个调用会告诉性能监控核心:"方法ID为1的方法现在开始执行了"。
步骤 2: onMethodExit (插针) - 方法退出时插桩
  • 动作 :同样通过字节码技术,在所有需要被监控的方法的出口处(包括return和抛出异常的地方)插入记录代码。
  • 具体代码 :插入 AppMethodBeat.o(1)。这里的数字 1 必须与入口处的方法ID对应。
  • 目的 :当方法执行结束时,再记录一个时间戳。通过 i(1)o(1) 的时间差,就可以计算出方法A的精确执行耗时。
步骤 3 & 4: 注册与回调 - 监听UI消息循环开始
  • 动作

    • 步骤3 (注册) :在应用启动时,向系统注册一个监听器(LoopPerMonitor),用来监听Android主线程Looper的消息处理(dispatch)事件。
    • 步骤4 (回调) :每当主线程开始处理一条新消息(如启动一个Activity、处理一个点击事件、执行一次绘制)时,系统会回调监听器的 onDispatchBegin() 方法。
  • 目的 :这是一个关键的边界界定 。它标志着一个"监控会话"的开始。一次消息处理可能包含成千上万个方法调用,onDispatchBegin 标志着这个庞大调用树的根节点开始了。

步骤 5: 收集数据 - 在消息处理过程中
  • 动作 :从onDispatchBegin开始,到onDispatchEnd结束,这期间所有被插桩的方法(方法A, 方法B ...)的 i()o() 调用都会被记录。

  • 具体过程

    • record start1AppMethodBeat.i(1) 被调用,记录方法A的开始时间和方法ID。
    • record end1AppMethodBeat.o(1) 被调用,记录方法A的结束时间。
    • indexRecord++:一个内部的索引指针(如maskIndex)会递增,指向下一个存储位置。所有这些耗时数据都被临时存入一个long[]数组缓冲区。
  • 目的:高效地、低开销地在内存中积累原始性能数据。

步骤 6: dump 数据采集 & mergeData - 消息处理结束,触发数据收集
  • 动作

    • 步骤6 (回调) :当主线程处理完当前消息时,系统会回调监听器的 onDispatchEnd() 方法。这标志着一个"监控会话"的结束。
    • dump :在 onDispatchEnd 回调中,会触发数据采集动作。这意味着:"刚才那一次完整的消息处理已经完了,现在把这段时间里收集的所有数据拿出来处理"。
    • mergeData :这个过程会调用你之前提到的 mergeData 逻辑。它会检查缓冲区(long[]数组)的使用情况(通过maskIndex判断),然后通过copyData将有效数据拷贝出来,准备进行上报。
  • 目的:以一个完整的、有业务意义的事件(如"点击按钮打开页面")为单位,来聚合和输出性能数据,而不是零散地上报单个方法耗时。

步骤 7: 数据聚合分类 & AnalyseTask() - 后台分析与上报
  • 动作 :从缓冲区dump出来的原始数据(一堆方法ID和耗时的时间戳)被送入一个后台分析任务(AnalyseTask)。

  • 具体处理

    • 聚合分类 :分析任务会解析这些数据,根据方法ID还原出方法名,并将它们组织成一个调用树(Call Stack Tree) 。它可以计算出:

      • 整个消息的总耗时。
      • 每个方法的自身耗时(扣除子方法耗时后的时间)。
      • 每个方法的完整耗时(包括所有子方法的时间)。
      • 关键路径识别。
    • 统计分析:计算平均耗时、最大最小值、TP90、TP99等指标。

  • 目的:将原始的、扁平的时间戳数据,转化为具有业务洞察力的、结构化的性能分析报告,并最终上报到服务器供开发者查看。

typescript 复制代码
public class EvilMethodTracer extends Tracer {
    private static final String TAG = "Matrix.EvilMethodTracer";
    
    // 慢函数阈值(默认700ms)
    private long mEvilThresholdMs = 700;
    // 索引记录
    private AppMethodBeat.IndexRecord mIndexRecord;
    
    @Override
    public void onStart() {
        super.onStart();
        
        // 注册Looper监听器
        LooperMonitor.register(new LooperMonitor.LooperDispatchListener() {
            @Override
            public void onDispatchBegin(String x) {
                // 消息分发开始
                onDispatchBegin(x);
            }

            @Override
            public void onDispatchEnd(String x) {
                // 消息分发结束
                onDispatchEnd(x);
            }
        });
    }
}

2.3 慢方法的原理,源码解析

方法执行记录核心, 最重要的2个方法!

通过onDispatchEnd触发慢方法!

ini 复制代码
       @Override

       public void onDispatchBegin(String log) {

//            MatrixLog.w(TAG,"onDispatchBegin");

            indexRecord = AppMethodBeat.getInstance().maskIndex("EvilMethodTracer#dispatchBegin");

           AppMethodBeat.i(AppMethodBeat.METHOD_ID_DISPATCH);

       }



       @Override

       public void onDispatchEnd(String log, long beginNs, long endNs) {

//            MatrixLog.w(TAG,"onDispatchEnd"+Thread.currentThread().getName());

           AppMethodBeat.o(AppMethodBeat.METHOD_ID_DISPATCH);

           long dispatchCost = (endNs - beginNs) / Constants.TIME_MILLIS_TO_NANO;

           try {

               if (dispatchCost >= evilThresholdMs) {

                   long[] data = AppMethodBeat.getInstance().copyData(indexRecord);

                   String scene = AppActiveMatrixDelegate.INSTANCE.getVisibleScene();

                   MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(isForeground(), scene, data, dispatchCost, endNs));

               }

           } finally {

                indexRecord.release();

           }

       }

如何dump数据和处理数据的?

ini 复制代码
 void analyse() {

               // process

               int[] processStat = Utils.getProcessPriority(Process.myPid());

               LinkedList<MethodItem> stack = new LinkedList<>();

               if (data.length > 0) {

                   TraceDataUtils.structuredDataToStack(data, stack, true, endMs);

                   TraceDataUtils.trimStack(stack, Constants.TARGET_EVIL_METHOD_STACK, new TraceDataUtils.IStructuredDataFilter() {

                       @Override

                       public boolean isFilter(long during, int filterCount) {

                           return during < (long) filterCount * Constants.TIME_UPDATE_CYCLE_MS;

                       }

                       @Override

                       public int getFilterMaxCount() {
                           return Constants.FILTER_STACK_MAX_COUNT;

                       }

                       @Override
                       public void fallback(List<MethodItem> stack, int size) {

                           MatrixLog.w(TAG, "[fallback] size:%s targetSize:%s stack:%s", size, Constants.TARGET_EVIL_METHOD_STACK, stack);

                           Iterator<MethodItem> iterator = stack.listIterator(Math.min(size, Constants.TARGET_EVIL_METHOD_STACK));

                           while (iterator.hasNext()) {

                                iterator.next();

                                iterator.remove();

                           }

                       }

                   });

               }

  

               StringBuilder reportBuilder = new StringBuilder();

               StringBuilder logcatBuilder = new StringBuilder();

               long stackCost = Math.max(cost, TraceDataUtils.stackToString(stack, reportBuilder, logcatBuilder));

               String stackKey = TraceDataUtils.getTreeKey(stack, stackCost);


               MatrixLog.w(TAG, "%s", printEvil(scene, processStat, isForeground, logcatBuilder, stack.size(), stackKey, cost)); // for logcat

  

               // report

               try {

                   TracePlugin plugin = Matrix.with().getPluginByClass(TracePlugin.class);

                   if (null == plugin) {

                       return;

                   }

                   JSONObject jsonObject = new JSONObject();

                   DeviceUtil.getDeviceInfo(jsonObject, Matrix.with().getApplication());

  


                    jsonObject.put(SharePluginInfo.ISSUE_STACK_TYPE, Constants.Type.NORMAL);

                    jsonObject.put(SharePluginInfo.ISSUE_COST, stackCost);

                    jsonObject.put(SharePluginInfo.ISSUE_SCENE, scene);

                    jsonObject.put(SharePluginInfo.ISSUE_TRACE_STACK, reportBuilder.toString());

                    jsonObject.put(SharePluginInfo.ISSUE_STACK_KEY, stackKey);

  
                   Issue issue = new Issue();
                    issue.setTag("GW_"+SharePluginInfo.TAG_PLUGIN_EVIL_METHOD);
                    issue.setContent(jsonObject);
                    plugin.onDetectIssue(issue);

               } catch (JSONException e) {
                   MatrixLog.e(TAG, "[JSONException error: %s", e);

               }

           }

LooperMonitor 做的工作主要是什么?(重点)

通过给Looper设置Printer,监听消息执行前后的回调。

内部队列记录当前消息最近的历史消息耗时情况,辅助分析耗时问题。

LooperMonitor 是 Matrix 监控体系的"心脏"和"触发器" 。它本身不直接执行具体的监控逻辑(如计算ANR或分析慢方法),而是作为一个全局的、中央式的事件分发器 ,通过监控主线程 Looper 的消息执行来为其他 Tracer(特别是 AnrTracerEvilMethodTracer)提供最基础、最关键的生命周期事件。

LooperMonitor 的核心职责

  1. 全局监听主线程消息循环
    它通过 Looper.getMainLooper().setMessageLogging(printer) 设置一个自定义的 Printer。主线程 Looper 在执行每个消息(dispatchMessage)的前后 会分别打印 ">>>>> Dispatching to...""<<<<< Finished to..."。LooperMonitor 通过检查日志行的前缀来感知消息处理的开始和结束。
  2. 充当事件分发中心
    它维护了一个监听器列表 (List<LooperDispatchListener>)。当捕获到消息开始或结束的事件时,它会同步地通知所有注册的监听器。这使得多个监控组件可以共享同一个监控点,极大减少了性能开销和对代码的侵入性。
  3. 提供监控的"时间切片"依据
    EvilMethodTracer 需要知道一个消息处理了多久,AnrTracer 需要知道一个消息是否超时。LooperMonitor 提供的 onDispatchStartonDispatchEnd 回调正是这两个时间点,使得耗时计算成为可能。
scss 复制代码
private void dispatch(boolean isBegin, String log) {

       if (isBegin) {
           if (historyMsgRecorder) {

                messageStartTime = System.currentTimeMillis();

                latestMsgLog = log;

                recentMCount++;

           }

           synchronized (oldListeners) {

               for (LooperDispatchListener listener : oldListeners) {

                   if (listener.isValid()) {

                        listener.onDispatchStart(log);

                   }

               }

           }

           synchronized (listeners) {

               for (DispatchListenerWrapper listener : listeners.values()) {

                   if (listener.isValid()) {

                        listener.onDispatchBegin(log);

                   }

               }

           }

       } else {

           if (historyMsgRecorder) {

               recordMsg(log, System.currentTimeMillis() - messageStartTime);

           }

           synchronized (oldListeners) {

               for (LooperDispatchListener listener : oldListeners) {

                   if (listener.isValid()) {

                        listener.onDispatchEnd(log);

                   }

               }

           }

           synchronized (listeners) {

               for (DispatchListenerWrapper listener : listeners.values()) {

                   if (listener.isValid()) {

                        listener.onDispatchEnd(log);

                   }

               }

           }

       }

   }

EvilMethodTracer 将 Looper 的一个消息执行周期(从 onDispatchStartonDispatchEnd)定义为一个"时间片",并在这个时间片内寻找慢方法。

  • onDispatchStart

    • 调用 AppMethodBeat.i(AppMethodBeat.METHOD_ID_DISPATCH) 记录一个"分发开始"的方法调用。
    • 调用 AppMethodBeat.getInstance().maskIndex(...) 记录当前方法索引器的位置。这个索引像是给环形缓冲区拍了个"快照",标记了监控的起始点。
  • onDispatchEnd

    • 调用 AppMethodBeat.o(AppMethodBeat.METHOD_ID_DISPATCH) 记录"分发结束"。
    • 计算消息总耗时long cost = (endNs - beginNs) / Constants.TIME_MILLIS_TO_NANO
    • 判断 :如果 cost > 阈值(如700ms),则说明这个时间片内发生了卡顿。
    • 分析 :调用 AppMethodBeat.getInstance().copyData(indexRecord)从之前"快照"的索引位置开始,拷贝到当前索引位置为止,这个"时间片"内所有方法的执行记录。这些数据最终会被分析成一个详细的调用栈树,找出到底是哪些慢方法导致了这次卡顿。

问题: 为什么将Printer对象设置给Looper,LooperMonitor就可以拿到每隔消息执行前后的回调?

IdleHandler------> queueIdle()------>Loopeer-->setMessageLogging(设置 print)

总结:

1).通过looper获取到所有消息的间隔,然后比较消息是否700ms,然后得到插桩的调用栈!

LooperMonitor 是触发采集

2). 是从AppMethodBeat中收集过来的一个时间段内所有方法执行的耗时信息

EvilMethodTracer的逻辑汇总:

EvilMethodTracer:1).依赖主线程的消息机制运行,以主线程一个VSYNC期间消息执行消耗的时长作为触发点, 2).默认超过700毫秒时认为发现了卡顿的存在,此时会借助AppMethodBeat收集的方法信息将期间内所有方法运行的信息汇总,按照方法耗时大小排列,列出消耗时长最大的一些方法,3).并report,协助开发者进行分析定位卡顿发生的根源。

2.3.1核心方法onDispatchBegin:

typescript 复制代码
  @Override

       public void onDispatchBegin(String log) {

//            MatrixLog.w(TAG,"onDispatchBegin");

            indexRecord = AppMethodBeat.getInstance().maskIndex("EvilMethodTracer#dispatchBegin");

           AppMethodBeat.i(AppMethodBeat.METHOD_ID_DISPATCH);

       }

调用于message消息分发前,dispatchBegin执行时,通过AppMethodBeat调用maskIndex。maskIndex传递一个字符串,AppMethodBeat会将这个字符串和当前app内方法执行的index绑定,将两者封装为一个链表节点插入链表中,并返回这个节点,于是后边我们可以通过这个返回的节点得到本次标记时方法执行的位置,然后以这个开始位置为起点,以后边获取信息时的位置为终点,拿到这个区间内所有方法执行的耗时信息,从而辅助分析区间内耗时的根源所在。

重点:maskIndex的作用!

标志正在运行的方法在哪个位置!就是在onDispatchEnd的时候获取到!

2.3.2核心方法onDispatchEnd

ini 复制代码
public void onDispatchEnd(String log, long beginNs, long endNs) {

//            MatrixLog.w(TAG,"onDispatchEnd"+Thread.currentThread().getName());

           AppMethodBeat.o(AppMethodBeat.METHOD_ID_DISPATCH);

           long dispatchCost = (endNs - beginNs) / Constants.TIME_MILLIS_TO_NANO;

           try {

               if (dispatchCost >= evilThresholdMs) {

                   long[] data = AppMethodBeat.getInstance().copyData(indexRecord);

                   String scene = AppActiveMatrixDelegate.INSTANCE.getVisibleScene();

                   MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(isForeground(), scene, data, dispatchCost, endNs));

               }

           } finally {

                indexRecord.release();

           }

       }

AppBeatMethod的生命周期是被TracePlugin控制的原因

csharp 复制代码
public interface BeatLifecycle {

 


   void onStart();

 


   void onStop();

 


   boolean isAlive();

}

 


 


  public void onStart() {

       synchronized (statusLock) {

           if (status < STATUS_STARTED && status >= STATUS_EXPIRED_START) {

                sHandler.removeCallbacks(checkStartExpiredRunnable);

               MatrixHandlerThread.getDefaultHandler().removeCallbacks(realReleaseRunnable);

               if (sBuffer == null) {

                   throw new RuntimeException(TAG + " sBuffer == null");

               }

               MatrixLog.i(TAG, "[onStart] preStatus:%s", status, Utils.getStack());

                status = STATUS_STARTED;

           } else {

               MatrixLog.w(TAG, "[onStart] current status:%s", status);

           }

       }

   }

 


   public void onStop() {

       synchronized (statusLock) {

           if (status == STATUS_STARTED) {

               MatrixLog.i(TAG, "[onStop] %s", Utils.getStack());

                status = STATUS_STOPPED;

           } else {

               MatrixLog.w(TAG, "[onStop] current status:%s", status);

           }

       }

   }
  

2.3.3 核心的几个方法:

AppBeatMethod的i、o、at三个方法被字节码插桩到各个方法中,所以i、o方法调用的场景极多,每个方法进入和退出几乎都会调用到。

编译的时候,插入了方法i ,结束插入方法o ,

方法运行的时候,都会调用AppMethoBeat.i(),所以我们是有记录的!

主线程运行完,把数据放在一个内存块中,合并数据!

AppBeatMethod的i、

2.3.3.1. realExecute - 真实执行的方法

  • 功能 :这是最核心的部分,指的是被监控的原始业务方法本身。这是你想要统计耗时的目标方法。

  • 工作原理 :通过代码注入(如AOP面向切面编程、ASM字节码操作、Java Agent的Instrumentation API等),在 realExecute 方法的开始处结束处插入埋点代码。

  • 典型实现

    typescript 复制代码
    // 这是你的业务方法,即 realExecute
    public void myBusinessMethod(String params) {
        // 1. 方法开始时,插入代码: long startTime = System.nanoTime();
        // ... 原有的业务逻辑 ...
        // 2. 方法结束时,插入代码: long costTime = System.nanoTime() - startTime;
        // 3. 调用 mergeData(costTime); 或其他记录方法
    }
  • 目的:捕获方法执行的绝对起点和终点时间戳,并计算出一次执行的耗时。

2.3.3.2. mergeData - 合并/汇总数据

  • 功能 :这是一个数据聚合函数 。它不会被频繁地每次调用都上报数据,而是将多次 realExecute 执行产生的耗时数据(long costTime)收集起来,进行临时存储和汇总。

  • 工作原理

    1. 接收数据 :接收来自 realExecute 计算出的单次耗时 costTime
    2. 存储数据 :将这次耗时的值存入一个临时的数据结构中,通常就是你提到的 long[] 数组。
    3. 判断条件:它会检查当前收集的数据是否达到了一个"阈值"(例如:收集了100次耗时、或距离上次上报已过了30秒)。这个阈值策略是为了避免过于频繁的数据上报。
    4. 触发上报 :当满足条件时,mergeData 会调用后续的处理流程(可能会涉及到 maskIndexcopyData)来最终准备和上报数据。
  • 目的:减少网络开销和服务器压力,将离散的单个方法执行数据聚合成一个批次进行上报,并提供基础统计能力(如计算平均耗时、最大最小值等)。

2.3.3.3. maskIndex - 掩码或索引管理器

  • 功能 :这是一个指针或索引管理工具 ,通常用于操作存储耗时数据的 long[] 数组。它的核心作用是解决并发读写冲突

  • 工作原理

    • 它很可能是一个 AtomicInteger 或普通的 int 变量。

    • 这个索引指向 long[] 数组中下一个可写入的位置

    • mergeData 方法需要将一个新的耗时数据存入数组时,会调用 maskIndex原子性地获取当前可写入的位置,并将索引值加1。

    • 示例

      arduino 复制代码
      private AtomicInteger maskIndex = new AtomicInteger(0);
      private long[] timeCostArray = new long[100];
      
      public void mergeData(long costTime) {
          int currentIndex = maskIndex.getAndIncrement(); // 获取当前索引并后移
          if (currentIndex < timeCostArray.length) {
              timeCostArray[currentIndex] = costTime;
          } else {
              // 数组已满,触发copyData等操作
          }
      }
  • 目的 :确保在多线程环境下,多个并发的 realExecute 方法能够正确、不覆盖地向共享的 long[] 数组中写入数据,避免数据错乱。

2.3.3.4. copyData - 数据拷贝与重置

  • 功能 :这是一个数据快照和清理函数 。当 mergeData 判断需要上报数据时(例如数组已满或超时),copyData 被调用。

  • 工作原理

    1. 创建快照 :将当前 long[] 数组中的数据拷贝到一个新的数组中。这一步非常重要,因为它能固定当前批次的数据,避免在上报过程中原始数组被新的数据覆盖。
    2. 重置状态 :重置原始数组的索引(例如将 maskIndex 重置为0),并清空或重置原始数组,为接收下一批数据做准备。
    3. 交付数据:将拷贝出来的新数组(即一个完整批次的数据)交付给上报线程或队列,进行异步网络传输。
  • 目的:实现数据的"翻转",在保证数据完整性的前提下,将已收集好的数据与正在接收新数据的数组分离开,实现无锁或低锁竞争的数据上报。


核心协作流程与 long[] 数组的角色

整个流程可以概括为以下步骤,long[] 数组是贯穿始终的核心数据结构:

  1. 采集realExecute 方法执行,计算耗时 T

  2. 收集 :调用 mergeData(T)

  3. 存入mergeData 方法通过查询 maskIndex 的值,确定在 long[] 数组中的存放位置,并将 T 存入。存入后 maskIndex 自增。

  4. 判断mergeData 检查 maskIndex 是否达到数组长度(数组已满)或是否超时。

  5. 翻转 :如果条件满足,调用 copyData

    • copyData 基于当前 maskIndexlong[] 数组,拷贝有效数据到一个新数组。
    • 重置 maskIndex 为0,并清空原数组。
  6. 上报:新数组被送入上报队列,最终将一批时间戳统计数据发送到服务器。服务器可以据此计算平均耗时、TP99、QPS等指标。

时间戳的统计

  • 采集层面 :在 realExecute 中,使用 System.nanoTime()(高精度,计算相对耗时)或 System.currentTimeMillis()(绝对时间戳)来获取时间点。

  • 数据层面long[] 数组中存储的每一个 long 值,都是一个方法执行的耗时 (单位通常是纳秒或毫秒),而不是原始的时间戳。这是因为上报耗时 比上报开始/结束时间戳更通用、数据量更小。

  • 统计层面 :服务器收到一个批次的 long[] 数据后,可以进行各种聚合统计:

    • 次数/吞吐量 :数组的长度 length 就是调用次数。
    • 平均耗时sum(array) / length
    • 最大/最小耗时:遍历数组找到 max/min。
    • 分位值(TPM) :如 TP50(中位数)、TP90、TP99。将数组排序后,取相应位置的值即可。

总结

组件 职责 类比
realExecute 目标方法:被监控的业务方法,负责产生原始耗时数据。 生产线上的工人
long[] 缓冲区:临时存储一批耗时数据的内存区域。 一个装产品的篮子
maskIndex 指针:指向篮子中下一个空位,解决多人同时放产品的冲突。 篮子的编号牌/管理员
mergeData 协调员:接收产品,放入篮子,并判断篮子是否已满。 生产线的小组长
copyData 打包员:篮子满了后,把产品打包成一个包裹,并换上一个空篮子。 打包发货员

2.3.4 EvilMethodTracer工作原理总结

  1. 编译期插桩

    • 使用 ASM 在所有方法的开始和结束处插入 AppMethodBeat.i()AppMethodBeat.o() 调用
    • 每个方法都有一个唯一的方法 ID
  2. 运行时监控

    • LooperMonitor 监控主线程消息处理
    • 在消息开始时记录当前缓冲区索引位置
    • 在消息结束时计算消息处理耗时

ASM插桩的原理,我后面会单独写一篇文章讲解!

3. FPS监控,FrameTracer 帧率监测(doframe)

对应报告里面的: tag: Trace_FPS

为什么要监控帧率呢?

帧率监控用于确保 UI 流畅性,目标帧率为 60fps(每帧 16.66ms)

根本原因是为了保证帧率的稳定。通常来讲,Android设备大多都是60fps的帧率(当然也有90fps、120fps的),也就是画面每秒更新60次,假如应用的帧率能稳定的维持在60fps的话,对用户来讲体验是最好的。而要保证帧率维持在60fps,那么就要求每次刷新在16.66毫秒内完成。我们知道Android的刷新是基于VSYNV信号的,Android系统每隔16.66毫秒发出一次VSYNC信号,触发对UI进行渲染,VSYNC机制保证了Android的刷新频率维持在一个固定的间隔内,有利于帧率的稳定

架构图:

3.1 帧率监控的核心方法

以前的老版本: UIThreadMonitor

现在用法: OnFrameMetricsAvailableListener

官方出手,官方在Android N 以上新增了Window.OnFrameMetricsAvailableListener可以监听每帧的执行状态。包含总耗时,绘制耗时,布局耗时,动画耗时,测量耗时。依次我们可以计算出帧率。

  • 计算

    • 理想帧时间frameIntervalNanos = 1_000_000_000 / refreshRate
    • 丢帧数droppedFrames = (TOTAL_DURATION - frameIntervalNanos) / frameIntervalNanos
    • FPSfps = 1_000_000_000 / TOTAL_DURATION
  • 数据聚合:每 20 帧或固定时间间隔聚合数据,计算平均耗时、丢帧数和 FPS,并按丢帧等级(Best、Normal、Middle、High、Frozen)分类上报。

丢帧的等级:FPS

collect方法中将丢帧的情况按照丢帧数量等级做了划分,分别为:

  • frozen级别-丢帧大于42
  • high级别-丢帧介于24到42
  • middle级别-丢帧介于9到24
  • normal级别-丢帧介于3到9
  • best级别-丢帧小于3

帧率监控工作原理

  1. 初始化与注册

    • FrameTracer 注册为 Activity 生命周期监听器
    • 在每个 Activity 的 onResume 中注册 OnFrameMetricsAvailableListener
    • 在每个 Activity 的 onPause 中移除监听器
  2. 帧数据收集

    • 系统在每帧渲染完成后回调 onFrameMetricsAvailable
    • 解析 FrameMetrics 对象,获取各阶段耗时
    • 计算丢帧数和 FPS
  3. 数据聚合

    • 使用 SceneFrameCollectItem 聚合多帧数据
    • 按丢帧等级分类统计
    • 计算平均耗时、平均 FPS 等指标
  4. 数据分发

    • 将聚合后的数据分发给所有注册的监听器
    • 支持多种数据接收者(上报、悬浮窗等)
  5. 数据上报

    • 通过 Issue 机制上报性能数据
    • 包含详细的各阶段耗时和丢帧统计

3.1.1 源码分析:

调用链

lua 复制代码
FrameTracer 

      forceEnable

              register

                    SceneFrameCollector-->onFrameMetricsAvailable

                              SceneFrameCollectItem---->tryCallBackAndReset

监控回调的链路

lua 复制代码
Window.OnFrameMetricsAvailableListener  -->onFrameMetricsAvailable

          SceneFrameCollector-->onFrameMetricsAvailable

                  SceneFrameCollectItem---> append

                              onDetectIssue------->Issue

3.1.2 帧率报告输出的指标

每20帧上报一次数据的更新!

scss 复制代码
  // 回调监听器(参数说明):

            // lastScene: 最后场景名称

            // durations: 各阶段平均耗时(纳秒)

            // dropLevel: 各掉帧等级出现次数

            // dropSum:  各掉帧等级掉帧总数

            // dropCount: 平均每帧掉帧数

            // refreshRate: 平均刷新率(Hz)

            // 最后参数:基于平均耗时计算的FPS(1秒/平均耗时)

            listener.onFrameMetricsAvailable(lastScene, d)
ini 复制代码
public void onFrameMetricsAvailable(@NonNull String sceneName, long[] avgDurations, int[] dropLevel, int[] dropSum, float avgDroppedFrame, float avgRefreshRate, float avgFps) {
           MatrixLog.i(TAG, "[report] FPS:%s %s", avgFps, toString());
           try {
               TracePlugin plugin = Matrix.with().getPluginByClass(TracePlugin.class);
               if (null == plugin) {
                   return;
               }

               JSONObject dropLevelObject = new JSONObject();
               JSONObject dropSumObject = new JSONObject();
               for (DropStatus dropStatus : DropStatus.values()) {
                    dropLevelObject.put(dropStatus.name(), dropLevel[dropStatus.ordinal()]);
                    dropSumObject.put(dropStatus.name(), dropSum[dropStatus.ordinal()]);
               }

               JSONObject resultObject = new JSONObject();
               DeviceUtil.getDeviceInfo(resultObject, plugin.getApplication());

                resultObject.put(SharePluginInfo.ISSUE_SCENE, sceneName);
                resultObject.put(SharePluginInfo.ISSUE_DROP_LEVEL, dropLevelObject);
                resultObject.put(SharePluginInfo.ISSUE_DROP_SUM, dropSumObject);
                resultObject.put(SharePluginInfo.ISSUE_FPS, avgFps);


               for (FrameDuration frameDuration : FrameDuration.values()) {
                    resultObject.put(frameDuration.name(), avgDurations[frameDuration.ordinal()]);
                   if (frameDuration.equals(FrameDuration.TOTAL_DURATION)) {
                       break;
                   }
               }
               if (sdkInt >= Build.VERSION_CODES.S) {
                    resultObject.put("GPU_DURATION", avgDurations[FrameDuration.GPU_DURATION.ordinal()]);
               }
                resultObject.put("DROP_COUNT", Math.round(avgDroppedFrame));
                resultObject.put("REFRESH_RATE", (int) (avgRefreshRate));

               Issue issue = new Issue();
                issue.setTag(SharePluginInfo.TAG_PLUGIN_FPS);
                issue.setContent(resultObject);
                plugin.onDetectIssue(issue);

帧率的计算

java 复制代码
void onFrameMetricsAvailable(String sceneName, FrameMetrics frameMetrics, float droppedFrames, float refreshRate);

 

public void onActivityResumed(Activity activity) {

   //获取刷新频率

    defaultRefreshRate = getRefreshRate(activity.getWindow());

   MatrixLog.i(TAG, "default refresh rate is %dHz", (int) defaultRefreshRate);

   //Google官方推荐使用 OnFrameMetricsAvailableListener 测量布局渲染时间(用于Android7.0及以上)

   Window.OnFrameMetricsAvailableListener onFrameMetricsAvailableListener = new Window.OnFrameMetricsAvailableListener() {

  


     //这里应该是 Build.VERSION_CODES.N

     @RequiresApi(api = Build.VERSION_CODES.O)

     @Override

     public void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics, int dropCountSinceLastInvocation) {

       //只有在前台才会去做数据处理,应用不可见时不会进行UI渲染

       if (isForeground()) {

         //跳过无意义数据的回调

         // skip not available metrics.

         for (int i = FrameDuration.UNKNOWN_DELAY_DURATION.ordinal(); i <= FrameDuration.TOTAL_DURATION.ordinal(); i++) {

           long v = frameMetrics.getMetric(FrameDuration.indices[i]);

           if (v < 0 || v >= HALF_MAX) {

             // some devices will produce outliers, especially the Honor series, eg: NTH-AN00, ANY-AN00, etc.

             return;

           }

         }

         //先copy一份, 之后从copy的数据中获取对应渲染时间参数

         FrameMetrics frameMetricsCopy = new FrameMetrics(frameMetrics);

         //TOTAL_DURATION: CPU 渲染到传递到 GPU 所用的总时间, 上述所花费的有意义的时间之和 , 单位纳秒

         long totalDuration = frameMetricsCopy.getMetric(FrameMetrics.TOTAL_DURATION);

         //不掉帧时的数据

         float frameIntervalNanos = Constants.TIME_SECOND_TO_NANO / cachedRefreshRate;

         //计算掉帧数= (总时间-不掉帧的时间)/刷新率

         float droppedFrames = Math.max(0f, (totalDuration - frameIntervalNanos) / frameIntervalNanos);

         //累加

          droppedSum += droppedFrames;

         //【掉帧收集】掉帧数>=阈值,收集起来(不指定dropFrameListener不会收集)

         if (dropFrameListener != null && droppedFrames >= cachedThreshold) {

            dropFrameListener.onFrameMetricsAvailable(

             ProcessUILifecycleOwner.INSTANCE.getVisibleScene(),

              frameMetricsCopy, droppedFrames, cachedRefreshRate);

         }

         synchronized (listeners) {

           //【收集所有】回调的SceneFrameCollector的onFrameMetricsAvailable,对应上面addListener(sceneFrameCollector)

           for (IFrameListener observer : listeners) {

              observer.onFrameMetricsAvailable(

               ProcessUILifecycleOwner.INSTANCE.getVisibleScene(),

                frameMetricsCopy, droppedFrames, cachedRefreshRate);

           }

         }

       }

     }

   };

   //监听器存入map,每个Activity对应一个监听器

   this.frameListenerMap.put(activity.hashCode(), onFrameMetricsAvailableListener);

   //向Activity界面的Window窗口对象,添加监听器,同时设置Handler,测量渲染在这个Handler所在的线程执行

    activity.getWindow().addOnFrameMetricsAvailableListener(

      onFrameMetricsAvailableListener, MatrixHandlerThread.getDefaultHandler());

   MatrixLog.i(TAG, "onActivityResumed addOnFrameMetricsAvailableListener");

}

  


@Override

public void onActivityDestroyed(Activity activity) {

   try {

     //onDestory时移除监听

      activity.getWindow().removeOnFrameMetricsAvailableListener(

        frameListenerMap.remove(activity.hashCode()));

   } catch (Throwable t) {

     MatrixLog.e(TAG, "removeOnFrameMetricsAvailableListener error : " + t.getMessage());

   }

}

丢帧数量的计算

// 获取该帧的总耗时(纳秒)

long totalDuration = frameMetricsCopy.getMetric(FrameMetrics.TOTAL_DURATION);

// 计算理想情况下每帧应耗时多少纳秒(1秒/刷新率 → 纳秒)

float frameIntervalNanos = Constants.TIME_SECOND_TO_NANO / cachedRefreshRate;

// 计算掉帧数:(实际耗时 - 理想耗时)/ 理想耗时

// 例如:总耗时是 32ms,理想是 16ms(60Hz),则掉帧数 = (32-16)/16 = 1 帧

float droppedFrames = Math.max(0f, (totalDuration - frameIntervalNanos) / frameIntervalNanos);

scss 复制代码
// 要求 Android N(API 24)及以上版本

@RequiresApi(Build.VERSION_CODES.N)

private class SceneFrameCollectItem {

   // 存储各阶段耗时的数组(按FrameDuration枚举顺序)

   private final long[] durations = new long[FrameDuration.values().length];

   // 按掉帧等级统计的次数数组(按DropStatus枚举顺序)

   private final int[] dropLevel = new int[DropStatus.values().length];

   // 按掉帧等级统计的掉帧总数数组(按DropStatus枚举顺序)

   private final int[] dropSum = new int[DropStatus.values().length];

   private float dropCount;       // 总掉帧数

   private float refreshRate;     // 平均刷新率

   private float totalDuration;   // 平均总耗时

   private long beginMs;         // 统计开始时间戳

   private String lastScene;     // 最后记录的场景名

   private int count = 0;         // 收集的帧数




   ISceneFrameListener listener;  // 数据回调接口




   // 构造方法

   SceneFrameCollectItem(ISceneFrameListener listener) {

       this.listener = listener;

   }




   /**

    * 添加帧数据到统计项

    * @param scene 当前场景名称

    * @param frameMetrics 帧指标数据

    * @param droppedFrames 掉帧数

    * @param refreshRate 当前刷新率

    */

   public void append(String scene, FrameMetrics frameMetrics, float droppedFrames, float refreshRate) {

       // 如果跳过首帧且当前是首帧,或掉帧数未达到阈值则忽略

       if ((listener.skipFirstFrame() && frameMetrics.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == 1)

               || droppedFrames < (refreshRate / 60) * listener.getThreshold()) {

           return;

       }

       

       // 如果是第一次收集,记录开始时间

       if (count == 0) {

            beginMs = SystemClock.uptimeMillis();

       }

       

       // 累加各阶段耗时(UNKNOWN_DELAY到TOTAL_DURATION)

       for (int i = FrameDuration.UNKNOWN_DELAY_DURATION.ordinal(); i <= FrameDuration.TOTAL_DURATION.ordinal(); i++) {

            durations[i] += frameMetrics.getMetric(FrameDuration.indices[i]);

       }

       

       // Android S+ 额外记录GPU耗时

       if (sdkInt >= Build.VERSION_CODES.S) {

            durations[FrameDuration.GPU_DURATION.ordinal()] += frameMetrics.getMetric(FrameMetrics.GPU_DURATION);

       }




       // 更新统计数据

        dropCount += droppedFrames;

       collect(Math.round(droppedFrames));  // 按掉帧等级分类统计

       this.refreshRate += refreshRate;

       // 计算单帧理想耗时(防止除0)

       float frameIntervalNanos = Constants.TIME_SECOND_TO_NANO / refreshRate;

       // 取实际耗时和理想耗时的较大值(避免异常值)

        totalDuration += Math.max(frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION), frameIntervalNanos);

       ++count;




        lastScene = scene;

       // 达到统计时间间隔时触发回调

       if (SystemClock.uptimeMillis() - beginMs >= listener.getIntervalMs()) {

           tryCallBackAndReset();

       }

   }




   /**

    * 尝试触发回调并重置统计(当满足条件时)

    */

   void tryCallBackAndReset() {

       // 至少收集20帧才触发回调(避免数据波动)

       if (count > 20) {

           // 计算平均值

            dropCount /= count;

           this.refreshRate /= count;

            totalDuration /= count;

           // 计算各阶段平均耗时

           for (int i = 0; i < durations.length; i++) {

                durations[i] /= count;

           }

           // 回调监听器(参数说明):

           // lastScene: 最后场景名称

           // durations: 各阶段平均耗时(纳秒)

           // dropLevel: 各掉帧等级出现次数

           // dropSum:  各掉帧等级掉帧总数

           // dropCount: 平均每帧掉帧数

           // refreshRate: 平均刷新率(Hz)

           // 最后参数:基于平均耗时计算的FPS(1秒/平均耗时)

            listener.onFrameMetricsAvailable(lastScene, durations, dropLevel, dropSum,

                    dropCount, this.refreshRate, Constants.TIME_SECOND_TO_NANO / totalDuration);

       }

       reset();  // 无论是否回调都重置数据

   }




   /**

    * 按掉帧等级分类统计

    * @param droppedFrames 当前帧掉帧数(整数)

    */

   private void collect(int droppedFrames) {

       // 按预设阈值分级统计(典型值示例):

       // frozenThreshold: 24帧(严重卡顿)

       // highThreshold:  12帧(明显卡顿)

       // middleThreshold: 6帧 (轻微卡顿)

       // normalThreshold: 3帧 (轻微掉帧)

       if (droppedFrames >= frozenThreshold) {

            dropLevel[DropStatus.DROPPED_FROZEN.ordinal()]++;

            dropSum[DropStatus.DROPPED_FROZEN.ordinal()] += droppedFrames;

       } else if (droppedFrames >= highThreshold) {

            dropLevel[DropStatus.DROPPED_HIGH.ordinal()]++;

            dropSum[DropStatus.DROPPED_HIGH.ordinal()] += droppedFrames;

       } else if (droppedFrames >= middleThreshold) {

            dropLevel[DropStatus.DROPPED_MIDDLE.ordinal()]++;

            dropSum[DropStatus.DROPPED_MIDDLE.ordinal()] += droppedFrames;

       } else if (droppedFrames >= normalThreshold) {

            dropLevel[DropStatus.DROPPED_NORMAL.ordinal()]++;

            dropSum[DropStatus.DROPPED_NORMAL.ordinal()] += droppedFrames;

       } else {

           // 优秀表现(掉帧数小于normalThreshold或为0)

            dropLevel[DropStatus.DROPPED_BEST.ordinal()]++;

            dropSum[DropStatus.DROPPED_BEST.ordinal()] += Math.max(droppedFrames, 0);

       }

   }




   /**

    * 重置所有统计指标

    */

   private void reset() {

        dropCount = 0;

        refreshRate = 0;

        totalDuration = 0;

        count = 0;




       Arrays.fill(durations, 0);

       Arrays.fill(dropLevel, 0);

       Arrays.fill(dropSum, 0);

   }

}

各阶段平均耗时

// durations: 各阶段平均耗时(纳秒)

// dropLevel: 各掉帧等级出现次数

// dropSum: 各掉帧等级掉帧总数

// dropCount: 平均每帧掉帧数

// refreshRate: 平均刷新率(Hz)

总耗时= 每次耗时累加

掉帧总数=每次掉帧累加

long[] avgDurations: 各阶段平均耗时 = 总耗时/次数

int[] dropLevel, 各掉帧等级出现次数

int[] dropSum, 各掉帧等级掉帧总数

float avgDroppedFrame, 平均每帧掉帧数 =掉帧的总次数/ 次数

float avgRefreshRate, 平均刷新率 = 累计刷新率/次数

float avgFps 基于平均耗时计算的FPS(1秒/平均耗时)

csharp 复制代码
listener.onFrameMetricsAvailable(

   "HomeActivity",                         // 最后场景名称

   [1200000, 2500000, 1800000, 3000000],   // durations各阶段平均耗时(ns)

   [80, 25, 10, 5, 0],                     // dropLevel各等级出现次数

   [0, 100, 80, 75, 0],                   // dropSum各等级掉帧总数

   0.625f,                                 // 平均每帧掉帧数

   59.8f,                                 // 平均刷新率(Hz)

   48.2f                                   // 实际FPS

);

3.1.3 数据聚合

  • 每 20 帧或固定时间间隔聚合数据,计算平均耗时、丢帧数和 FPS,并按丢帧等级(Best、Normal、Middle、High、Frozen)分类上报。 4.悬浮窗的实时帧率显示原理
java 复制代码
view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {

           @Override

           public void onViewAttachedToWindow(View v) {

               MatrixLog.i(TAG, "onViewAttachedToWindow");

               if (Matrix.isInstalled()) {

                   TracePlugin tracePlugin = Matrix.with().getPluginByClass(TracePlugin.class);

                   if (null != tracePlugin) {

                       FrameTracer tracer = tracePlugin.getFrameTracer();

                        tracer.register(FrameDecorator.this);

                   }

               }

           }

通过: ISceneFrameListener-----> onFrameMetricsAvailable

ini 复制代码
public void onFrameMetricsAvailable(@NonNull String sceneName, long[] avgDurations, int[] dropLevel, int[] dropSum, float avgDroppedFrame, float avgRefreshRate, final float fps) {

       final String unknownDelay = String.format("unknown delay: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.UNKNOWN_DELAY_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO);

       final String inputHandling = String.format("input handling: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.INPUT_HANDLING_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO);

       final String animation = String.format("animation: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.ANIMATION_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO);

       final String layoutMeasure = String.format("layout measure: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.LAYOUT_MEASURE_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO);

       final String draw = String.format("draw: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.DRAW_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO);

       final String sync = String.format("sync: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.SYNC_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO);

       final String commandIssue = String.format("command issue: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.COMMAND_ISSUE_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO);

       final String swapBuffers = String.format("swap buffers: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.SWAP_BUFFERS_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO);

       final String gpu = String.format("gpu: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.GPU_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO);

       final String total = String.format("total: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.TOTAL_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO);

从上面的 Demo 中可以看出:

右上角展示帧率、统计柱状图。 其实展示的是一个自定义 View,将接收到的数据经过计算得出帧率。

上图绿色的 60.00 FPS 指的是过去 300ms 内的平均帧率

灰色的 sum: 3.0 是 总掉帧次数,下面的彩虹从左到右分别代表 Normal/Middle/High/Frozen 这四个级别的掉帧占总掉帧的比例,往下是当前页面的掉帧数和掉帧比例

最底下的图表是过去 10s 内平均帧率(200ms 时间段)的横向柱状图,每 5s 就会有 25 条记录,50 FPS 差不多是 Normal 的帧率下限,30 FPS 差不多是 Middle 的帧率下限

关于卡顿官方文档是这么解释的: FPS 低并不意味着卡顿发生,而卡顿发生 FPS 一定不高。

3.1.4 .官方的demo,帧率面板展示,帧率监控一直没有实时的回调和结果!

如何解决悬浮球的帧率监控?

为什么下面的没有回调?

java 复制代码
  private IDoFrameListener mDoFrameListener = new IDoFrameListener(new Executor() {
        Handler handler = new Handler(sHandlerThread.getLooper());

        @Override
        public void execute(Runnable command) {
            handler.post(command);
        }
    }) {
        @Override
        public void doFrameAsync(String focusedActivity, long startNs, long endNs, int dropFrame, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {

            super.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);

            MatrixLog.i(TAG, "[doFrameAsync]" + " costMs=" + (endNs - intendedFrameTimeNs) / Constants.TIME_MILLIS_TO_NANO

                    + " dropFrame=" + dropFrame + " isVsyncFrame=" + isVsyncFrame + " offsetVsync=" + ((startNs - intendedFrameTimeNs) / Constants.TIME_MILLIS_TO_NANO) + " [%s:%s:%s]", inputCostNs, animationCostNs, traversalCostNs);
        }

因为官方的代码有问题,需要手动修改!

4. 帧率监控二:老款监控 Choreographer:

github源码:github.com/friendlyrob...在 Matrix 的 TraceCanary 模块中,旧版本使用的是Choreographer的方式,但是新版本中使用的是Looper机制了!因为可以拿到更完整更清晰的堆栈信息。源码分析:LooperMonitor

4.1 帧率计算Choreographer: doframe

这里主要是根据每一帧的总耗时,统计掉帧个数、FPS 等数据,最后将这些数据以一定的格式回调出去掉帧原理:监听 Choreographer,doFrame 回调时统计 UI 刷新耗时,计算掉帧数及掉帧程度,当同一个 Activity/Fragment 掉帧程度超过阈值时,就上报。但 Matrix 的计算方法存在问题,可能出现频繁上报的情况,需要自行手动过滤。。

帧率Choreographer总结

主要分析了 Matrix-TraceCanary 模块中帧率 FPS、掉帧个数等数据的原理和大概流程,原理是通过Looper.getMainLooper().setMessageLogging(Printer printer)向其中添加 Printer 对象监听主线程中 MainLooper 每个消息的处理时间,当前处理的是不是绘制阶段产生的消息是根据 Choreographer 机制判断的,如果当前处理的是绘制阶段产生的消息,在处理消息的同时一定会回调 Choreographer 中回调队列mCallbackQueues中的回调方法,向回调队列mCallbackQueues添加回调是通过反射在 UIThreadMonitor 中实现的。

4.2 老款监控 使用Looper机制实现的方式存在一个缺点:

在打印消息处理开始日志和消息处理结束日志的时候,都会进行字符串的拼接,频繁地进行字符串的拼接也会影响性能(重点)

5. StartupTracer 启动耗时(插桩)

不详细说

UI刷新监控
  • UI 刷新监控是基于 Choreographer 实现的,TracePlugin 初始化时,UIThreadMoniter 就会通过反射的方式往 Choreographer 添加回调:

  • 原理 :通过 ASM 插桩在启动关键点(如 Application.onCreateActivity 生命周期方法)插入打点代码。

  • 测量:计算各启动阶段的耗时(如冷启动、首屏渲染时间)。

  • 与慢函数监控结合:可以找出启动过程中的慢方法,优化启动性能。

6. Matrix整体架构总结

组件详解

6.1. 编译期 (ASM Bytecode Instrumentation)

  • 职责 :在编译阶段修改字节码,这是所有监控的数据基础
  • 动作 :在每个方法的开始处插入 AppMethodBeat.i(methodId),在结束处插入 AppMethodBeat.o(methodId)
  • 结果:应用运行时,所有方法的执行和结束都会被自动记录。

6.2. TracePlugin

  • 职责 :监控系统的管理器集线器

  • 动作

    • 负责初始化、启动、停止所有四个 Tracer
    • 接收来自各个 Tracer 的 Issue 并上报。
  • 定位:指挥官,不负责具体监控逻辑,只负责调度和管理。

6.3. LooperMonitor

  • 职责主线程消息生命的感知器 ,是整个监控的心跳触发器
  • 原理 :通过给主线程 Looper 设置自定义 Printer,来监听每一个消息(Message)的执行开始和结束。
  • 输出 :发出 onDispatchBegin()onDispatchEnd() 事件。
  • 关键作用 :它为 AnrTracerEvilMethodTracer 提供了计时的起点和终点。

6.4. AppMethodBeat

  • 职责方法调用的记录仪黑匣子

  • 原理 :使用一个巨大的 long[] 数组作为环形缓冲区,以非常低的开销记录所有方法的调用信息(方法ID + 时间戳 + 进出状态)。

  • 核心方法

    • i() / o(): 由插桩代码调用,用于记录。
    • maskIndex(): 在某个时间点(如消息开始)打一个标记,记录当前的写入位置。
    • copyData(): 获取从标记点开始到当前时间点的所有方法记录数据。
  • 定位:数据的生产者。

6.5. 四个 Tracer (监控器)

它们是监控逻辑的消费者执行者

  • EvilMethodTracer

    • 消费 :监听 LooperMonitor 的事件。
    • 逻辑 :在 onDispatchBegin 时调用 maskIndex() 打标记。在 onDispatchEnd 时计算消息耗时,若超阈值(700ms),则调用 copyData() 获取从开始到结束的所有方法记录,分析出具体是哪个方法链导致的卡顿。
  • AnrTracer

    • 消费 :监听 LooperMonitor 的事件。
    • 逻辑 :在 onDispatchBegin 时提交一个延迟任务(如5s后执行),在 onDispatchEnd 时取消该任务。如果延迟任务执行了,说明消息5s没处理完,即发生了ANR。
  • FrameTracer

    • 消费不依赖 LooperMonitor。它使用独立的系统 API OnFrameMetricsAvailableListener
    • 逻辑:直接监听系统渲染每一帧的耗时,计算FPS和丢帧情况。
  • StartupTracer

    • 消费不直接依赖 LooperMonitor。它监听应用生命周期(如 Application.onCreate, Activity 生命周期)。
    • 逻辑 :在关键节点打点记录时间,并利用 AppMethodBeat 记录的数据来分析启动过程中的慢方法。

数据流总结

  1. 编译时 :ASM 插入记录代码 → AppMethodBeat

  2. 运行时

    • 主线程执行 :所有方法被自动记录 → AppMethodBeat 的缓冲区。
    • LooperMonitor 监听到消息开始/结束事件,分发给 EvilMethodTracerAnrTracer
    • EvilMethodTracerAppMethodBeat 中取出数据进行分析,生成结果上报给 TracePlugin
    • AnrTracer 根据超时判断,直接生成ANR报告上报给 TracePlugin
    • FrameTracerStartupTracer 独立工作,也将结果上报给 TracePlugin
相关推荐
llq_3507 小时前
peerDependencies(对等依赖)
前端
一刻缱绻7 小时前
Mixed Content 问题及解决方案详解
前端·浏览器
我想说一句7 小时前
双Token机制
前端·前端框架·node.js
怪可爱的地球人7 小时前
Symbol符号是“唯一性”的类型
前端
月亮慢慢圆7 小时前
cookie,session和token的区别和用途
前端
郭邯7 小时前
vant-weapp源码解读(3)
前端·微信小程序
golang学习记7 小时前
从0 死磕全栈第3天:React Router (Vite + React + TS 版):构建小时站实战指南
前端
Dream耀7 小时前
Promise静态方法解析:从并发控制到竞态处理
前端·javascript·代码规范