android AsmTrace 插件

889 阅读3分钟

转载注明出处
github地址:github.com/light-echo-…

AsmTrace 实现原理

涉及知识点:
gradle 插件开发。
asm修改字节码。了解一些字节码知识。
小工具:
android studio查看字节码插件:ASM Bytecode Viewer

1.概述

    实现一个gradle插件,通过Asm对所有类的方法插桩。
在方法的入口插入代码:Trace.beginSection("className#$methodName")
在方法的出口插入代码:Trace.endSection()

image1.png ps:AsmTraceStack.beginTrace,AsmTraceStack.endTrace中分别调用了Trace.beginSection,Trace.endSection

2.项目结构

AsmTrackPlugin:gradle插件
lib_asmtrack:java module,要插桩的类,以及trace相关逻辑。

2.插件实现

2.1 编写Plugin,Transform

编写gradle Plugin,通过Transform,遍历所有文件夹中的类,以及jar包中的类,对每个类的方法插桩。

class AsmTrackPlugin implements Plugin<Project> {
    void apply(Project project) {
        ...
            //register this plugin
            android.registerTransform(transformImpl)
        ...
    }
}
class ASMTransform extends Transform {

    ...

    // 核心方法
    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
        ...
        // 获取输入源
        Collection<TransformInput> inputs = transformInvocation.getInputs()

        ClassLoader classLoader = createClassLoader(project, inputs)

        inputs.forEach(transformInput -> {
            //处理所有文件夹中的类
            transformInput.getDirectoryInputs().forEach(directoryInput -> {
                try {
                    System.out.println("------directoryInput = " + directoryInput)
                    HandleDirectoryInputBusiness.traceDirectory(classLoader, directoryInput,
                            transformInvocation.getOutputProvider(),
                            new Config())
                } catch (IOException e) {
                    ...
                }
            })

            //处理所有jar包中的类
            transformInput.getJarInputs().forEach(jarInput -> {
                try {
                    System.out.println("------jarInput = " + jarInput)
                    HandleJarInputBusiness.traceJarFiles(classLoader, jarInput,
                            transformInvocation.getOutputProvider(),
                            new Config())
                } catch (IOException e) {
                    ...
                }
            })

        })
    }

    ...

}

2.2 函数插桩

通过MethodVisitor,访问函数入口和出口,分别插入相应代码。
实现类:com.wuzhu.asmtrack.MethodEnterAndExitAdapter

public class MethodEnterAndExitAdapter extends AdviceAdapter {

    private final String className;
    private final String methodName;
    private final int localVarSlot;

    public MethodEnterAndExitAdapter(final int api, final MethodVisitor mv,
                                     final int access, final String methodName, final String desc,
                                     final String className) {
        super(api, mv, access, methodName, desc);
        this.className = className;
        this.methodName = methodName;
        //增加一个本地变量槽
        localVarSlot = newLocal(Type.getType(String.class));
    }

    @Override
    protected void onMethodEnter() {
        super.onMethodEnter();
        //函数入口插桩trace begin
        insertBeginSection();
    }

    private void insertBeginSection() {
        String traceName = InsertCodeUtils.generatorName(className, methodName);
        InsertCodeUtils.insertBegin(traceName, mv, localVarSlot);
    }

    @Override
    protected void onMethodExit(int opcode) {
        super.onMethodExit(opcode);
        //函数出口插桩trace end
        insertEndSection();
    }

    private void insertEndSection() {
        InsertCodeUtils.insertEnd(mv, localVarSlot);
    }

}


2.3 插桩实现

2.3.1.trace名称生成规则。

image2.png =FragmentActivity#onStart-118256_41103

1."=":用于区分是自己插入的trace还是android系统trace。
2.类名,不包含包名。
3.方法名。
4.遍历每个方法时,生成的num,可用于区分重载的函数。
5.运行时,进入函数时,生成的num,用于区分递归调用。
正常的递归调用,没有这一部分也能正常work;
但是出现异常时,这部分能保证trace的正确性。

2.3.2.插桩实现


    @JvmStatic
    fun beginTrace(name: String?):String? {
        ...
        return newName
    }

    
    @JvmStatic
    fun endTrace(newName: String?) {
        ...
    }

以上是要插桩的java源码,注意beginTrace有返回值,返回值会作为参数传递给endTrace。 对于一个函数,插桩前后会是下面的样子:

插桩前:

    private void test(int a) {
        System.out.println(a);
    }

插桩后:

    private void test(int a, int b) {
        String var3 = AsmTraceStack.beginTrace("=TestAsm#test-5025");
        System.out.println(a + b);
        AsmTraceStack.endTrace(var3);
    }

从插桩后的代码可以看出,会增加一个局部变量var3。

    //com.wuzhu.asmtrack.utils.InsertCodeUtils

    @JvmStatic
    fun insertBegin(traceName: String, methodVisitor: MethodVisitor, localVarSlot: Int) {
        println("------=== name = $traceName")
        methodVisitor.visitLdcInsn(traceName)
        methodVisitor.visitMethodInsn(
            INVOKESTATIC,
            "com/wuzhu/libasmtrack/AsmTraceStack",
            "beginTrace",
            "(Ljava/lang/String;)Ljava/lang/String;",
            false
        )
        //将返回值,存到新增的局部变量
        methodVisitor.visitVarInsn(ASTORE, localVarSlot)
    }

    @JvmStatic
    fun insertEnd(methodVisitor: MethodVisitor, localVarSlot: Int) {
        //读取新增的局部变量
        methodVisitor.visitVarInsn(ALOAD, localVarSlot)
        methodVisitor.visitMethodInsn(
            INVOKESTATIC, "com/wuzhu/libasmtrack/AsmTraceStack", "endTrace", "(Ljava/lang/String;)V", false
        )
    }


以上是插桩代码,这句代码“methodVisitor.visitVarInsn(ASTORE, localVarSlot)”,会将AsmTraceStack.beginTrace返回值,存到新增的局部变量,asm会重新计算栈帧(重新计算局部变量表和操作数栈)。
LocalVariablesSorter参考文章:Java ASM系列:(039)LocalVariablesSorter介绍

3.方法异常trace end处理

3.1 问题

一个java方法,只有一个入口,但是出口,可能有两个: 1.正常return;2.抛出异常。

image3.png 上图字节码对应的java代码:

    public int testThrowException(int a, int b) throws Exception {
        if (a > 100) {
            throw new Exception("test throw");
        } else {
            return a + b;
        }
    }

原本的方案是:
在入口1处,插入Trace.beginSection。
在入口 2 & 3 出,分别插入Trace.endSection。
对于上面那种显示定义了"throw new Exception...",可以保证begin,end成对调用;
但是,对于运行时抛出的异常,无法处理,最终会导致Trace.endSection调用次数 < Trace.beginSection,导致的后果就是:出现运行时异常时,很多函数没有正常end。

3.2 解决方案

每个函数生成一个唯一name标识(见2.3.1.trace名称生成规则),定义一个栈,进入函数时,将name入栈,函数结束时,去栈中找对应的name,找到后,栈中name及以上全部出栈。如此,即使中间函数出现异常,缺少调用Trace.endSection,但当有函数正常结束时,会多次调用Trace.endSection,从而保证Trace.beginSection & Trace.endSection一一对应。 详细实现见:com.wuzhu.libasmtrack.AsmTraceStack













AsmTrace 使用说明

1 适用场景

插件适用于debug环境中,对于已知卡顿场景,能检测出所有函数耗时情况(除了android系统函数),通过perfetto,打开函数时序图,可精确定位哪个方法耗时。 例如: 启动优化(能统计出所有启动时的函数耗时情况)。 切换Activity慢。 列表快速滑动等等已知卡顿场景。

此插件不建议release中使用。

2 插件使用

2.2 初始化

在application中调用 com.wuzhu.libasmtrack.AsmTraceInitializer.init

3 性能分析

Perfetto入门
Perfetto官网
Perfetto trace 数据保存位置 /data/local/traces
拉取trace数据:
adb pull /data/local/traces

效果图:
img_9.png

4 相比与Debug.startMethodTracing,Debug.startMethodTracingSampling优势。

Debug trace 存在的问题:

  1. Debug.startMethodTracing有兼容性问题,有些手机录制的Trace文件,profile打不开。
  2. 对于性能一般的手机,检测功能复杂的界面时,直接卡到无法使用。 使用Debug.startMethodTracingSampling,增大采样间隔卡顿问题能缓解一些,但是又会丢失精度,测试了一个性能差的手机,间隔要1s卡顿才能缓解。 ps:Debug.startMethodTracingSampling 会 suspend all thread。

本插件解决的问题:
插件虽然会整体拖慢执行速度,但不会像Debug.startMethodTracing那样直接卡到无法使用,且没有兼容性问题,没有采样精度问题。

img_10.png ps:如图
AsmTrace插件是对“System Trace”的扩展。
Debug.startMethodTracing 对应的是 “Java/Kotlin Methode Trace”。
Debug.startMethodTracingSampling 对应的是 “Java/Kotlin Methode Sample”。

TODO

发布到maven仓库。