Android-卡顿耗时方法监控

251 阅读3分钟

实际案例

假设某个页面中在主线程做了耗时操作,例如:

private fun haha(l: Long): Double {
    val haha2 = haha2(l)
    val haha3 = haha3(l)
    return haha2 + haha3
}

private fun haha2(l: Long): Double {
    var result = 0.0
    for (i in 0 until l) {
        result += acos(cos(i.toDouble()))
        result -= asin(sin(i.toDouble()))
    }
    return result
}

private fun haha3(l: Long): Double {
    var result = 0.0
    for (i in 0 until l) {
        result += acos(cos(i.toDouble()))
        result -= asin(sin(i.toDouble()))
    }
    return result
}

调用:
findViewById<Button>(R.id.btn_2).setOnClickListener {
    haha(10000000)
}

我们期望当页面发生卡顿时,输出卡顿期间所有方法的耗时,从而定位问题。

微信图片_20240614145843.png

具体实现

大致思路是两个部分,一个是编译的时候在需要观察的方法出入增加自定义的方法用来计时,第二部分是运行期间,通过 Looper 机制判断卡顿,收集卡顿期间的所有方法。

编译期  

1.背景

.java 文件通过 JavaCompiler 编译为 .class 文件,再通过 D8/R8 转为 .dex 文件。

JVM 和 Dalvik(ART) 重要的区别就是Dalvik(ART)有自己的二进制文件,也就是.dex文件,需要将 class 文件进行再一次转换。

dex 文件可以理解为一个class文件包,里面装着很多的class文件,让这些类能够共享数据。

4afe42606557c3c3fd10b321f8df0970.png

b43716744823436bd8d5c0db4e57338d.png

2. gradle 编译代码时获取 class 输入

通过 gradle 的自定义 plugin 在编译时代理 transformClassesWithDexTask 获取到所有文件的 class  

@Override  
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {  
    def traceConfig = project.traceConfig  
    String output = traceConfig.output  
    if (output == null || output.isEmpty()) {  
         traceConfig.output = project.getBuildDir().getAbsolutePath() + File.separator + "trace_output"  
    }  
  
    if (traceConfig.open) {  
        Config config = initConfig()  
        config.parseTraceConfigFile()  

        def transformInputs = transformInvocation.inputs  
        def outputProvider = transformInvocation.outputProvider  
        if (outputProvider != null) {  
            outputProvider.deleteAll()  
        }  

        def methodTracer = new MethodTracer(config)  
        transformInputs.each { input ->  
            methodTracer.trace(input, outputProvider)  
        }  
    }  
}

3. 通过 ASM 对所有符合条件的输入的 class 文件进行插入代码

transform流程.png

具体的插入自定义方法

override fun onMethodEnter() {  
    super.onMethodEnter()  
    val methodName = generatorMethodName()  
    mv.visitLdcInsn(methodName)  
    mv.visitMethodInsn(  
    INVOKESTATIC,  
    traceConfig.mBeatClass,  
    "start",  
    "(Ljava/lang/String;)V",  
    false  
    )  
    if (traceConfig.mIsNeedLogTraceInfo) {  
        println("MethodTrace-trace-method:{ ${methodName ?: "unknown"}}")  
    }  
}  
  
override fun onMethodExit(opcode: Int) {  
    mv.visitLdcInsn(generatorMethodName())  
    mv.visitMethodInsn(  
    INVOKESTATIC,  
    traceConfig.mBeatClass,  
    "end",  
    "(Ljava/lang/String;)V",  
    false  
    )  
}

需要注意的是,不是所有的方法/文件都需要插入,可以通过文件配置过滤不需要插入代码的文件,例如 bean 类,接口/抽象类等,自定义一个文件按照自己的规则写入和解析。
文件:xxxx.txt

#需插桩的包,空,默认所有文件
#-tracepackage com/example/android_kt_wandroid
#-tracepackage com/example/common_base
#-tracepackage com/example/module_home

#无需插桩的包
-keeppackage com/example/lib_trace
-keeppackage com/example/module_home/home/bean

#无需插桩的类
#-keepclass com/example/android_kt_wandroid/MainActivity

#插桩代码所在类
-beatclass com/example/lib_trace/core/TraceBeat

读取文件解析按规则解析:

when {  
    config.startsWith("-tracepackage ") -> {  
        config = config.replace("-tracepackage ", "")  
        mNeedTracePackageMap.add(config)  
        println("tracepackage:$config")  
    }  
    config.startsWith("-keepclass ") -> {  
        config = config.replace("-keepclass ", "")  
        mWhiteClassMap.add(config)  
        println("keepclass:$config")  
    }  
    config.startsWith("-keeppackage ") -> {  
        config = config.replace("-keeppackage ", "")  
        mWhitePackageMap.add(config)  
        println("keeppackage:$config")  
    }  
    config.startsWith("-beatclass ") -> {  
        config = config.replace("-beatclass ", "")  
        mBeatClass = config  
        println("beatclass:$config")  
    }  
    else -> {  
    }  
}

在插入访问时判断配置文件规则

@Override  
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {  
    super.visit(version, access, name, signature, superName, interfaces);  
    this.className = name;  
    String resultClassName = name.replace(".", "/");  
    //beat class  
    if (resultClassName.equals(traceConfig.getMBeatClass())) {  
        this.isBeatClass = true;  
    }  
    //config ingore class  
    if (!TextUtils.isEmpty(name)) {  
        isConfigTraceClass = traceConfig.isConfigTraceClass(className);  
    }  
    //abs/interface  
    if (access == Opcodes.ACC_ABSTRACT || access == Opcodes.ACC_INTERFACE) { 
        this.isAbsClass = true;  
    }  
    boolean isNotNeedTraceClass = isAbsClass || isBeatClass || !isConfigTraceClass;  
    if (traceConfig.getMIsNeedLogTraceInfo() && !isNotNeedTraceClass) {  
        Log.log("MethodTraceClassName::" + className);  
    }  
}

运行期

大致流程: 运行时流程.png

1. 运行时通过插桩函数记录所有方法

private static final List<Entity> methodList = new LinkedList<>();  
  
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)  
public static void start(String name) {  
    if (isOpenTraceMethod()) {  
        Trace.beginSection(name);  
        synchronized (methodList) {  
            methodList.add(new Entity(name, System.currentTimeMillis(), true, isInMainThread()));  
        }  
    }  
}  
  
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)  
public static void end(String name) {  
    if (isOpenTraceMethod()) {  
        Trace.endSection();  
        synchronized (methodList) {  
            methodList.add(new Entity(name, System.currentTimeMillis(), false, isInMainThread()));  
        }  
    }  
}

2. 通过消息队列判定是否卡顿

依赖主线程 Looper,监控每次 dispatchMessage 的执行耗时

public static void loop() {
    ...
    for (;;) {
        ...
        // This must be in a local variable, in case a UI event sets the logger
        Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }
        msg.target.dispatchMessage(msg);
        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }
        ...
    }
}

主线程所有的任务都是在 dispatchMessage(msg) 方法中执行完成,那我们通过自定义 Printer,在 dispatchMessage(msg) 执行完成后判读此次耗时是否超出阈值,即可判断。

override fun println(x: String) {  
    isValid = x[0] == '>' || x[0] == '<'  
    if (isValid) {  
        dispatch(x[0] == '>', x)  
    }  
}  
  
private fun dispatch(isBegin: Boolean, log: String) {  
    if (isBegin) {  
        mFirstTimeMillis = System.currentTimeMillis().also { mStartTimeMillis = it }  
        for (observer in observers) {  
            observer.dispatchBegin(mStartTimeMillis, mFirstTimeMillis)  
        }  
    } else {  
        mFinishTimeMillis = System.currentTimeMillis()  
        for (observer in observers) {  
            observer.dispatchEnd(mStartTimeMillis, mFinishTimeMillis)  
        }  
    }  
}

3.收集卡顿期间的方法

public static List<MethodInfo> collectTraceData(int start, int size) {  
    synchronized (methodList) {  
        List<MethodInfo> resultList = new ArrayList();   
        for (int i = 0; i < methodList.size(); i++) {  
            Entity startEntity = methodList.get(i);  
            if (!startEntity.isStart) {  
                continue;  
            }  
            startEntity.pos = i;  
            Entity endEntity = findEndEntity(startEntity.name, i + 1);  
            if (endEntity != null && endEntity.time - startEntity.time > 0) {  
                MethodInfo methodInfo = createMethodInfo(startEntity, endEntity);  
                resultList.add(methodInfo);  
            }  
        }  
        return resultList;  
    }  
}

扩展

在 Looper 消息机制的基础上,可以实现 Anr 监控

  1. 消息开始时,在主线程中 post 一个 5000ms 后执行的任务
  2. 消息结束时,把这个任务 remove 掉
  3. 在任务中收集堆栈信息,内存信息等方便后续处理

具体代码实现在 WanAndroid gradle-plugin 和 lib_trace 中