android AsmTrace 插件

转载注明出处

github地址:github.com/light-echo-...

AsmTrace 实现原理

涉及知识点:

gradle 插件开发。

asm修改字节码。了解一些字节码知识。

小工具:

android studio查看字节码插件:ASM Bytecode Viewer

1.概述

实现一个gradle插件,通过Asm对所有类的方法插桩。

在方法的入口插入代码:Trace.beginSection("className#$methodName")

在方法的出口插入代码:Trace.endSection()

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包中的类,对每个类的方法插桩。

groovy 复制代码
class AsmTrackPlugin implements Plugin<Project> {
    void apply(Project project) {
        ...
            //register this plugin
            android.registerTransform(transformImpl)
        ...
    }
}
groovy 复制代码
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,访问函数入口和出口,分别插入相应代码。 下面MethodEnterAndExitAdapter类中有一个函数:getMaxLocals,用于获取局部变量表大小, 获取后,会作为参数,传递给InsertCodeUtils.insertBegin(traceName, mv, maxLocals)。 后面会讲到,这里为什么传入局部变量表大小。

java 复制代码
public class MethodEnterAndExitAdapter extends MethodVisitor {

    ...

    /**
     * 获取局部变量表大小
     * 查找指定方法的MethodNode,获取maxLocals
     */
    private int getMaxLocals(ClassNode classNode, String methodName, String descriptor) {
        MethodNode methodNode = null;
        for (MethodNode method : classNode.methods) {
            if (method.name.equals(methodName) && method.desc.equals(descriptor)) {
                methodNode = method;
                break;
            }
        }
        assert methodNode != null;
        return methodNode.maxLocals;
    }

    /**
     * 方法开始访问时
     */
    @Override
    public void visitCode() {
        // 1.首先处理自己的代码逻辑
        // MethodEnter...

        insertBeginSection();

        // 2.然后调用父类的方法实现
        super.visitCode();
    }

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

    @Override
    public void visitInsn(int opcode) {
        // 1.首先处理自己的代码逻辑
        if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {//opcode == Opcodes.ATHROW || //throw 不要插桩(自己抛出异常又自己捕获,插桩两次,导致出错)
            // MethodExit...
            insertEndSection();
        }
        // 2.然后调用父类的方法实现
        super.visitInsn(opcode);
    }

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

}

2.3 插桩实现,以及插桩代码中增加局部变量的处理。

2.3.1.trace名称生成规则。

=FragmentActivity#onStart-118256_41103

1."=":用于区分是自己插入的trace还是android系统trace。

2.类名,不包含包名。

3.方法名。

4.遍历每个方法时,生成的num,可用于区分重载的函数。

5.运行时,进入函数时,生成的num,用于区分递归调用。

正常的递归调用,没有这一部分也能正常work;

但是出现异常时,这部分能保证trace的正确性。

2.3.2.插桩实现

java 复制代码
    @JvmStatic
    fun beginTrace(name: String?):String? {
        ...
        return newName
    }

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

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

插桩前:

java 复制代码
    private void test(int a) {
        System.out.println(a);
    }

插桩后:

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

从插桩后的代码可以看出,会增加一个局部变量var3。插桩时,要将"局部变量"插入"局部变量表"正确的位置。

java 复制代码
    @JvmStatic
    fun insertBegin(traceName: String, methodVisitor: MethodVisitor, maxLocals: 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, maxLocals)
    }

    @JvmStatic
    fun insertEnd(methodVisitor: MethodVisitor, maxLocals: Int) {
        methodVisitor.visitVarInsn(ALOAD, maxLocals)
        methodVisitor.visitMethodInsn(
            INVOKESTATIC, "com/wuzhu/libasmtrack/AsmTraceStack", "endTrace", "(Ljava/lang/String;)V", false
        )
    }

以上是插桩代码,这句代码"methodVisitor.visitVarInsn(ASTORE, maxLocals)",会将局部变量插入到局部变量表最后面,这样可以保证asm重新计算栈帧(重新计算局部变量表和操作数栈)的正确性。

ps:很多文章将增加的变量插入局部变量表第0个位置,测试发现行不通,asm重新计算后的局部变量表不正确。

TODO 更好的增加局部变量的办法:LocalVariablesSorter

参考文章:Java ASM系列:(039)LocalVariablesSorter介绍

3.方法异常trace end处理

3.1 问题

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

上图字节码对应的java代码:

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。

but,对于上面那种显示定义了"throw new Exception...",对于运行时抛出的异常,无法处理,最终会导致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

效果图:

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

Debug trace 存在的问题:

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

本插件解决的问题:

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

ps:如图

AsmTrace插件是对"System Trace"的扩展。

Debug.startMethodTracing 对应的是 "Java/Kotlin Methode Trace"。

Debug.startMethodTracingSampling 对应的是 "Java/Kotlin Methode Sample"。

TODO

发布到maven仓库。

使用LocalVariablesSorter实现增加局部变量。

相关推荐
CYRUS STUDIO1 小时前
FART 主动调用组件设计和源码分析
android·逆向·源码分析·fart·脱壳
zimoyin6 小时前
kotlin Android AccessibilityService 无障碍入门
android·开发语言·kotlin
韩仔搭建17 小时前
第二章:安卓端启动流程详解与疑难杂症调试手册
android·ui·娱乐
A-花开堪折18 小时前
Android7 Input(七)App与input系统服务建立连接
android
冰糖葫芦三剑客18 小时前
Android 自定义悬浮拖动吸附按钮
android
吃汉堡吃到饱18 小时前
【Android】从Choreographer到UI渲染(二)
android·ui
微信公众号:AI创造财富18 小时前
显示的图标跟UI界面对应不上。
android·ui
aningxiaoxixi18 小时前
安卓 Audio Stream 类型
android
奔跑吧 android18 小时前
【android bluetooth 协议分析 01】【HCI 层介绍 3】【NUMBER_OF_COMPLETED_PACKETS 事件介绍】
android·bluetooth·hci·bt·gd·aosp13
_龙小鱼_21 小时前
Kotlin扩展简化Android动画开发
android·开发语言·kotlin