Android性能优化系列-腾讯matrix-TracePlugin之EvilMethodTracer源码分析

前言

经过不懈的努力,我们终于将tracer相关的底层基础打牢了,包括插件的字节码插桩功能、用来回溯方法执行情况的AppMethodBeat、用来监听主线程消息队列消息执行情况的LooperMonitor以及在LooperMonitor之上又封装了一层来提供更详细消息运行状态信息的的UIThreadMonitor,有了这些基础,再来看EvilMethodTracer以及后边的几个tracer就非常的简单了。没有看过这些基础文章的读者,建议先从这几篇底层基础开始,否则可能会比较吃力:

回到EvilMethodTracer,EvilMethodTracer顾名思义是用来检测有害方法的,有害方法指的就是运行缓慢的方法。和StartupTracer一样,EvilMethodTracer是TracePlugin中的一员,在TracePlugin初始化时进行创建。要了解StartupTracer的实现,请参考Android性能优化系列-腾讯matrix-TracePlugin启动速度优化之StartupTracer源码分析。今天针对EvilMethodTracer的源码,我们直接步入正题,从几个关键方法入手:

  1. 构造方法
  2. onStartTrace
  3. onCloseTrace

构造方法

构造方法逻辑很简单,接收config参数,并从config中拿到一个关键参数evilThresholdMs,这个值表示监控阈值,指的是一个VSYNC(VSync是Vertical Synchronization的简写,它利用vertical sync pulse来保证双缓冲在最佳时间点才进行交换,具体可自行百度)期间消息执行消耗的时长,并非一个方法执行消耗的时长,用户可自定义配置,matrix默认的参数是700毫秒,当超过700毫秒的时候,认为它出现耗时问题。

arduino 复制代码
public EvilMethodTracer(TraceConfig config) {
    this.config = config;
    //耗时阈值
    this.evilThresholdMs = config.getEvilThresholdMs();
    //是否开启
    this.isEvilMethodTraceEnable = config.isEvilMethodTraceEnable();
}

onStartTrace

onStartTrace是基类Tracer的方法,它会调用到EvilMethodTracer的onAlive方法,onAlive方法执行时,通过调用UIThreadMonitor的addObserver方法将自身添加为一个监听者。这样一来,EvilMethodTracer就可以收到三个方法回调,这三个方后边单独分析。UIThreadMonitor做了什么,为什么添加为observer后就可以收到这几个回调,在# Android性能优化系列-腾讯matrix-TracePlugin之UIThreadMonitor源码分析中有详细分析。

  • dispatchBegin:message消息分发前
  • doFrame:message消息分发完成
  • dispatchEnd:也表示message消息分发完成,在doFrame后立即调用
scss 复制代码
@Override
public void onAlive() {
    super.onAlive();
    if (isEvilMethodTraceEnable) {
        UIThreadMonitor.getMonitor().addObserver(this);
    }
}

onCloseTrace

onCloseTrace是基类Tracer的方法,它会调用到EvilMethodTracer的onDead方法,当onDead方法执行时,可以看到,只是通过UIThreadMonitor移除了监听,所以从onStartTrace、onCloseTrace这两个方法的调用上来看,EvilMethodTracer的逻辑基本上完全依赖于UIThreadMonitor的回调来实现。

scss 复制代码
@Override
public void onDead() {
    super.onDead();
    if (isEvilMethodTraceEnable) {
        UIThreadMonitor.getMonitor().removeObserver(this);
    }
}

接下来看一下UIThreadMonitor的三个回调方法。

dispatchBegin

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

java 复制代码
@Override
public void dispatchBegin(long beginNs, long cpuBeginMs, long token) {
    super.dispatchBegin(beginNs, cpuBeginMs, token);
    indexRecord = AppMethodBeat.getInstance().maskIndex("EvilMethodTracer#dispatchBegin");
}

上边调用mark方法之后得到了一个AppMethodBeat.IndexRecord对象,将其保存在了indexRecord变量中。

复制代码
AppMethodBeat.IndexRecord indexRecord

doFrame

message消息分发完成。在doFrame方法中,仅仅是将方法传递的三个时间值保存了起来。

arduino 复制代码
@Override
public void doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {
    //input类型消息执行消耗的时间
    queueTypeCosts[0] = inputCostNs;
    //animation类型消息执行消耗的时间
    queueTypeCosts[1] = animationCostNs;
    //traversal类型消息执行消耗的时间
    queueTypeCosts[2] = traversalCostNs;
}

dispatchEnd

表示message消息分发完成,在doFrame后立即调用。可以看到当消息分发所消耗的时长超过阈值时,通过AppMethodBeat调用copyData方法,传入上边markIndex方法执行后返回的indexRecord对象,来得到这一时间段内所有执行方法信息的一个拷贝:long[]数组。看到这里你也许会奇怪,方法执行信息为什么用一个long类型表示?Android性能优化系列-腾讯matrix-AppMethodBeat专项分析这里会告诉你答案。matrxi为了控制内存,所以特意将方法执行id和方法耗时信息压缩到了一个long类型变量中存储,这样即使存储100万个方法信息,内存占用也仅仅在7M左右。

java 复制代码
@Override
public void dispatchEnd(long beginNs, long cpuBeginMs, long endNs, long cpuEndMs, long token, boolean isVsyncFrame) {
    super.dispatchEnd(beginNs, cpuBeginMs, endNs, cpuEndMs, token, isVsyncFrame);
    //endNs - beginNs得到的是从开始到结束消耗的总时长
    long dispatchCost = (endNs - beginNs) / Constants.TIME_MILLIS_TO_NANO;
    try {
        //假如总时长大于设定的阈值,那么认为有卡顿问题产生
        if (dispatchCost >= evilThresholdMs) {
            long[] data = AppMethodBeat.getInstance().copyData(indexRecord);
            long[] queueCosts = new long[3];
            System.arraycopy(queueTypeCosts, 0, queueCosts, 0, 3);
            String scene = AppActiveMatrixDelegate.INSTANCE.getVisibleScene();
            //拿到方法运行信息后,开始进入子线程进行分析
            MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(isForeground(), scene, data, queueCosts, cpuEndMs - cpuBeginMs, dispatchCost, endNs / Constants.TIME_MILLIS_TO_NANO));
        }
    } finally {
        indexRecord.release();
    }
}

AnalyseTask

耗时问题发生后,就要进入分析阶段,我们直接去看AnalyseTask的analyse方法。analyse方法在report问题之前,会收集整理一些关键的系统环境信息,如cpu信息、内存信息、调用栈信息、当前所在页面等等,这里最关键的两点,一是当前内存的执行堆栈,二是从AppMethodBeat中收集过来的一个时间段内所有方法执行的耗时信息。这些信息能快速有效的帮助分析人员定位问题的所在。

ini 复制代码
void analyse() {
    //拿到进程信息,nice值、priority值
    int[] processStat = Utils.getProcessPriority(Process.myPid());
    //计算cpu使用时长占比
    String usage = Utils.calculateCpuUsage(cpuCost, cost);
    LinkedList<MethodItem> stack = new LinkedList();
    if (data.length > 0) {
        //解析方法long数组,将方法信息封装到MethodItem集合中
        TraceDataUtils.structuredDataToStack(data, stack, true, endMs);
        //遍历,找出耗时最长的30个方法,
        TraceDataUtils.trimStack(stack, Constants.TARGET_EVIL_METHOD_STACK, new TraceDataUtils.IStructuredDataFilter() {
             ...
        });
    }


    StringBuilder reportBuilder = new StringBuilder();
    StringBuilder logcatBuilder = new StringBuilder();
    long stackCost = Math.max(cost, TraceDataUtils.stackToString(stack, reportBuilder, logcatBuilder));
    String stackKey = TraceDataUtils.getTreeKey(stack, stackCost);
    //拼装信息report
    try {
        TracePlugin plugin = Matrix.with().getPluginByClass(TracePlugin.class);
        if (null == plugin) {
            return;
        }
        JSONObject jsonObject = new JSONObject();
        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_CPU_USAGE, usage);
        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(SharePluginInfo.TAG_PLUGIN_EVIL_METHOD);
        issue.setContent(jsonObject);
        plugin.onDetectIssue(issue);

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

}

report格式示例

看一下report的异常信息, 信息不全,stackKey原本应该有很多的方法耗时信息,但是可能是由于模拟器的缘故,没有相关信息。

bash 复制代码
[  
    {  
        "machine":"BAD",  
        "cpu_app":0,  
        "mem":2079719424,  
        "mem_free":1001052,  
        "detail":"ANR",  
        "cost":5000,  
        "stackKey":"",  
        "scene":"sample.tencent.matrix.trace.TestTraceMainActivity",  
        "stack":"",  
        "threadStack":" android.os.SystemClock:sleep(131) sample.tencent.matrix.trace.TestTraceMainActivity:L(204) sample.tencent.matrix.trace.TestTraceMainActivity:A(150) sample.tencent.matrix.trace.TestTraceMainActivity:testANR(132) java.lang.reflect.Method:invoke(-2) android.view.View$DeclaredOnClickListener:onClick(6263) android.view.View:performClick(7448) android.view.View:performClickInternal(7425) android.view.View:access$3600(810) android.view.View$PerformClick:run(28305) android.os.Handler:handleCallback(938) android.os.Handler:dispatchMessage(99) android.os.Looper:loop(223) android.app.ActivityThread:main(7656)",  
        "processPriority":10,  
        "processNice":-10,  
        "isProcessForeground":true,  
        "memory":**{  
            "dalvik_heap":12971,  
            "native_heap":19082,  
            "vm_size":13954288  
        },  
        "tag":"Trace_EvilMethod",  
        "process":"sample.tencent.matrix",  
        "time":1695426899842  
    }  
]

总结

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

相关推荐
雨白8 小时前
Android 快捷方式实战指南:静态、动态与固定快捷方式详解
android
hqk8 小时前
鸿蒙项目实战:手把手带你实现 WanAndroid 布局与交互
android·前端·harmonyos
LING8 小时前
RN容器启动优化实践
android·react native
侑虎科技10 小时前
在UE5中,预测脚步IK实现-PredictFootIK
性能优化·unreal engine
恋猫de小郭11 小时前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker16 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴16 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭1 天前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab1 天前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
BoomHe1 天前
Now in Android 架构模式全面分析
android·android jetpack