Android 使用Plugin 实现自动化埋点,ASM

Android 编译的三个阶段

image.png

编译插桩的场景

  • 代码生成,减少手工重复,降低出错的可能性
  • 代码监控,利用编译插桩技术实现性能监控
  • 代码修改,修改第三方 SDK 源码,实现无痕埋点
  • 代码分析,自定义代码检查

编译插桩的几种方法

image.png

1.AspectJ
AspectJ 内部是用 [BCEL]框架完成的。也就是大名鼎鼎的切面编程技术
成熟稳定使用简单 但 切入点固定,需要匹配相应的[正则表达式] 性能较低,实现的时候会生成一些包装类,对原函数的性能有影响

  1. ASM
    操作灵活 上手比较难,需要对 Java 字节码有比较深入的了解
  2. ReDex,直接修改 Dex 文件

ASM

ASM 是一个可以的 Java 字节码操作与分析框架,它可以直接生成二进制的 .class 文件。 在 IDEA 上可以使用 ASM Bytecode Outline 插件,方便查看 ASM 的操作符。也可以先在代码写成自己想要的结果,用 ASM Bytecode Outline 查看 ASM 操作符,再把相应吃操作符复制到代码中。

1.核心类

ClassReader
主要解析编译过的 .class [字节码文件]
ClassWriter
主要是重新构建编译后的类,例如修改类名、属性以及方法,甚至可以生成新的类字节码文件
ClassVisitor
主要负责 visit 类成员信息。其中包括标记在类上的注解、类的构造方法、类的字段、类的方法、静态代码块等
AdviceAdapter
实现了 MethodVisitor 接口,主要负责 visit method 的主要信息,用来具体的方法字节码操作
流程图

操作时序图

主要方法 AnalyticsClassVisitor 类

/**
     * 源文件的信息
     * @param source
     * @param debug
     */
    @Override
    public void visitSource(String source, String debug) {
        super.visitSource(source, debug);
    }

    @Override
    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        return super.visitAnnotation(descriptor, visible);
    }

    @Override
    public void visitAttribute(Attribute attribute) {
        super.visitAttribute(attribute);
    }

    /**
     *  拿到类的信息, 然后对满足条件的类进行过滤
     */
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);

    }

    // 内部类信息
    @Override
    public void visitInnerClass(String name, String outerName, String innerName, int access) {
        super.visitInnerClass(name, outerName, innerName, access);
    }

    // 遍历类的成员变量信息
    @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        return super.visitField(access, name, descriptor, signature, value);
    }

    // 类的方法信息, 拿到需要修改的方法,然后进行修改操作
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        return super.visitMethod(access, name, descriptor, signature, exceptions);
    }

    // 遍历类中的成员信息结束
    @Override
    public void visitEnd() {
        super.visitEnd();
    }

方法说明

visit:访问类的头部,其中version指的是类的版本;
acess指的是类的修饰符;
name类的名称;
signature类的签名,如果类不是泛型或者没有继承泛型类,那么signature为空;
superName类的父类名称;
visitSource: 访问类的源码,就是.java文件,一般情况用不上;
visitModule:暂时不清楚用来干嘛的,用的比较少;
visitNestHost:访问类的nest host;
nest 指的一个共享私有成员变量的包名相同的class集合,nest中有一个host(主类)和多个members(成员类),jdk11为了提供更大,更广泛的嵌套类型,并且为了补足访问控制检测不足,引进了两个新的class文件属性,nest host 和nest member,nest host中包含了一个nest members列表,用来确定其他静态nest members;nest member中包含了一个nest host属性用来确定它的nesthost;
visitOuterClass: 访问类的外部类,一般用于nest-class;
visitAnnotation:访问类的注解;
其中:
descriptor:表示类注解类的描述;
visible表示该注解是否运行时可见;
return AnnotationVisitor:表示该注解类的Visitor,可以用来访问注解值;
visitTypeAnnotation:访问类的签名类型(某个泛型)的注解;
其中:
typeRef:指的是类型引用,在这里只能是TypeReference.(CLASS_TYPE_PARAMETER |CLASS_TYPE_PARAMETER_BOUND|CLASS_EXTENDS );
typePath:被注解的类型参数,wildcard bound,array element type,包含typeRef的static inner type;
descriptor: 注解类的描述;
visible:该注解类型运行时是否可见;
visitAttribute:访问类的非标准属性;
visitNestMember:访问嵌套类的nest member,只有host class被visited时才能调用该方法
visitInnerClass:访问一个内部类的信息;
visitField:访问一个类的域信息,如果需要修改或者新增一个域,可以通过重写此方法;其中access:表示该域的访问方式,public,private或者static,final等等;
name:指的是域的名称;
descriptro:域的描述,一般指的是该field的参数类型;
signature:指的是域的签名,一般是泛型域才会有签名;
value:指的该域的初始值
reture FiedVisitor:表示将返回一个可以访问该域注解和属性的访问对象,如果不感兴趣的话,可以设置为空;
visitMethod:访问类的方法,如果需要修改类方法信息,则可以重写此方法:其中:decsriptor:表示方法的参数类型和返回值类型;
visitEnd:访问类的尾部,只有当类访问结束时,才能调用该方法,同时必须调用该方法;

用法

添加类的成员变量和方法

FieldVisitor 是修改类的成员变量 MethodVisitor 是用来修改类的方法,他们都在 ClassVisitor#visitEnd 方法里面修改

@Override
    public void visitEnd() {
        super.visitEnd();
        System.out.println("addFile visitEnd");

        // 增加一个字段
        FieldVisitor fieldVisitor = this.visitField(
                Opcodes.ACC_PUBLIC,
                "xyz",  // 成员变量的名称
                "Ljava/lang/String", // 成员变量的描述符
                null,
                null);
        if (fieldVisitor != null) {
            fieldVisitor.visitEnd();
        }

        // 增加一个方法
        MethodVisitor methodVisitor = this.visitMethod(
                Opcodes.ACC_PUBLIC,
                "test03",
                "(ILjava/lang/String;)V",
                null,
                null
        );
        if (methodVisitor != null) {
            methodVisitor.visitEnd();
        }
    }

移除类的成员变量和方法

移除类的成员变量和方法比较简单,只要匹配到相应的名称,返回 null 即可

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
    if ("test01".equals(name)) {
        return null;
    }
    return super.visitMethod(access, name, descriptor, signature, exceptions);
}

在方法调用前和调用后增加内容

在方法调用前和调用后增加内容,有两种方式
第一种方式是直接继承 MethodVisitor 在 visitCode 和 visitInsn(int opcode) 中增加内容,这种要处理的逻辑较为复杂,暂不考虑
第二种方式是继承已有的公共类 AdviceAdapter, 在 onMethodEnter 和 onMethodExit 中增加内容

AdviceAdapter 用法

继承 AdviceAdapter 要在 test01 方法里面增加内容,则需要当访问到该方法时,需要返回自定义的 MethodAdapter 类

// ModifyClassVisitor.java
public class ModifyClassVisitor extends ClassVisitor{

    public ModifyClassVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM7, classVisitor);
    }

 override fun visitMethodInsn(
        opcode: Int,
        owner: String?,
        name: String?,
        descriptor: String?,
        isInterface: Boolean
    ) {
}
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        System.out.println("ModifyClassVisitor visitField name " + name + " descriptor " + descriptor + " signature " + signature);
        MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
        if ("test01".equals(name)) {
            methodVisitor = new MethodAdapter(methodVisitor, access, name, descriptor);
        }
        return methodVisitor;
    }
}

MethodAdapter 是继承 AdviceAdapter, AdviceAdapter 是 ASM 框架里面提供的一个方便在方法或在构造函数前后插入内容的类,它的主要方法是 onMethodEnter, 在方法调用前插入, onMethodExit 方法调用后插入

// SensorsAutoTrackMethodVisitor.kt
package com.sensorsdata.analytics.android.plugin.viewclick

import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.commons.AdviceAdapter

class SensorsAutoTrackMethodVisitor(
    mv: MethodVisitor,
    access: Int,
    name: String,
    desc: String
) : AdviceAdapter(Opcodes.ASM7, mv, access, name, desc) {

    private var isTracked = false
    private var isTrackedOnEnter = false
    private var isTrackViewOnClick = false

    // 执行时机:当方法开始执行时触发
    // 作用:可以在方法开始时插入自定义逻辑,例如记录方法调用、初始化变量等
    public override fun onMethodEnter() {
        super.onMethodEnter()
        // 检查方法是否为公共且非静态
        if (SAUtils.isPublic(access) && !SAUtils.isStatic(access)) {
            // 处理特定方法
            if (nameDesc == "onClick(Landroid/view/View;)V") {
                // 复制 View 参数
                mMethodVisitor.visitInsn(Opcodes.DUP)
                // 调用跟踪方法
                mMethodVisitor.visitMethodInsn(
                    Opcodes.INVOKESTATIC,
                    "com/sensorsdata/analytics/SensorsAnalytics",
                    "trackViewOnClick",
                    "(Landroid/view/View;)V",
                    false
                )
                isTrackedOnEnter = true
            }
        }
    }

    // 执行时机:当方法即将结束时触发
    // 作用:可以在方法结束时插入自定义逻辑,例如记录方法返回值、清理资源等
    public override fun onMethodExit(opcode: Int) {
        super.onMethodExit(opcode)
        // 检查是否需要在方法退出时处理
        if (!isTrackedOnEnter) {
            // 处理方法退出逻辑
            if (nameDesc == "onItemClick(Landroid/widget/AdapterView;Landroid/view/View;IJ)V") {
                // 复制参数
                mMethodVisitor.visitVarInsn(ALOAD, 1)
                mMethodVisitor.visitVarInsn(ALOAD, 2)
                mMethodVisitor.visitVarInsn(ILOAD, 3)
                // 调用跟踪方法
                mMethodVisitor.visitMethodInsn(
                    Opcodes.INVOKESTATIC,
                    "com/sensorsdata/analytics/SensorsAnalytics",
                    "trackItemClick",
                    "(Landroid/widget/AdapterView;Landroid/view/View;I)V",
                    false
                )
                isTracked = true
            }
        }
    }

    // 执行时机:当 ASM 访问器访问到方法调用指令时触发
    // 作用:生成或修改方法调用指令,可以在方法调用前后插入自定义逻辑
    override fun visitMethodInsn(
        opcode: Int,
        owner: String?,
        name: String?,
        descriptor: String?,
        isInterface: Boolean
    ) {
        // 检查是否是特定方法调用
        if (opcode == Opcodes.INVOKEVIRTUAL &&
            name == "show" &&
            descriptor == "()V" &&
            owner == "android/app/Dialog"
        ) {
            // 在调用 show() 之前插入跟踪代码
            mMethodVisitor.visitInsn(Opcodes.DUP)  // 复制 Dialog 实例引用
            mMethodVisitor.visitMethodInsn(
                Opcodes.INVOKESTATIC,
                "com/sensorsdata/analytics/SensorsAnalytics",
                "trackDialogShow",
                "(Landroid/app/Dialog;)V",
                false
            )
        }
        // 继续调用父类的方法
        super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
    }

    // 执行时机:当 ASM 访问器访问到 invokedynamic 指令时触发
    // 作用:生成或修改 invokedynamic 指令,可以用于处理 Lambda 表达式、方法引用等动态调用的逻辑
    override fun visitInvokeDynamicInsn(
        name: String,
        descriptor: String,
        bootstrapMethodHandle: Handle,
        vararg bootstrapMethodArguments: Any
    ) {
        // 检查是否是 Lambda 表达式
        if (bootstrapMethodHandle.owner == "java/lang/invoke/LambdaMetafactory") {
            // 处理 Lambda 表达式
            // 例如,记录 Lambda 方法的信息
            Logger.info("Lambda expression detected: $name, $descriptor")
        }
        // 继续调用父类的方法
        super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, *bootstrapMethodArguments)
    }

    // 执行时机:当 ASM 访问器完成对一个方法的访问时触发
    // 作用:可以在方法结束时插入自定义逻辑,例如添加注解、记录方法信息等
    override fun visitEnd() {
        // 检查是否已经插入了跟踪代码
        if (isTracked) {
            // 添加自定义注解
            visitAnnotation("Lcom/sensorsdata/analytics/SensorsDataInstrumented;", false)
        }
        // 继续调用父类的方法
        super.visitEnd()
    }

    // 执行时机:当 ASM 访问器访问到方法或类的注解时触发
    // 作用:读取和处理注解信息,根据注解信息决定是否需要插入跟踪代码
    override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? {
        if (descriptor == "Lcom/sensorsdata/analytics/SensorsDataTrackViewOnClick;") {
            isTrackViewOnClick = true
        }
        // 继续调用父类的方法
        return super.visitAnnotation(descriptor, visible)
    }

    // 执行时机:当 ASM 访问器访问到字段访问指令时触发
    // 作用:生成或修改字段访问指令,可以在字段访问前后插入自定义逻辑
    override fun visitFieldInsn(
        opcode: Int,
        owner: String,
        name: String,
        descriptor: String
    ) {
        // 检查是否是特定字段访问
        if (opcode == Opcodes.GETFIELD &&
            name == "isSensorsCheckKeyboard" &&
            descriptor == "Z"
        ) {
            // 插入自定义逻辑
            mMethodVisitor.visitInsn(Opcodes.ICONST_0)
            mMethodVisitor.visitFieldInsn(
                Opcodes.PUTFIELD,
                owner,
                name,
                descriptor
            )
        }
        // 继续调用父类的方法
        super.visitFieldInsn(opcode, owner, name, descriptor)
    }
}

最终调用

private static void modifyMethod(String path) throws IOException {
        byte[] bytes = getBytes(path, "com.yxhuang.asm.MyMain");
        ClassReader classReader = new ClassReader(bytes);
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        ClassVisitor modifyClassVisitor = new ModifyClassVisitor(classWriter);
        classReader.accept(modifyClassVisitor, 0);
        byte[] bytesModified = classWriter.toByteArray();
        FileUtils.writeByteArrayToFile(new File(path + "com/yxhuang/asm/MyMain.class"), bytesModified);
    }

附录

对于不了解ASM的人,可能无法理解上文中出现的 mv.visitFieldInsn,mv.visitMethodInsn等方法及参数意义
可以参考 Nickid2018的博客
这里附上一些在埋点开发过程主要使用的,进行说明
MethodVisitor的方法,下面这些方法第一个参数都为字节码,第二个参数伟索引位置:
visitInsn(int):访问一个零参数要求的字节码指令,如ACONST_NULL
visitVarInsn(int, int):访问一个有关于局部变量的字节码指令,如ALOAD
visitMethodInsn(int, String, String, String, boolean):访问一个有关于方法调用的字节码,如INVOKESPECIAL
visitInvokeDynamicInsn(String, String, Handle, Object...):基于INVOKEDYNAMIC,动态方法调用,会在lambda表达式和方法引用里面说到
字节码介绍
注意: 接下来的x可以为a(针对对象)、i(针对int)、l(针对long)、f(针对float)、d(针对double)、b(针对byte)、c(针对char)、s(针对short),它代表了操作对象的类型。有些时候没有针对于byte和short的专用字节码,这是因为在JVM中,byte和short在被计算时会被强制拉长为int,所以它们使用的和int一样。char和int能互相转换。boolean类似,它们也需要使用int的字节码,而且boolean值的false就是int值0,而true就是int值1。
加载字节码:xload与xload_n,从局部变量表中加载指定索引的变量到操作数栈
存储字节码:xstore与xstore_n,将操作数栈顶的值存储到局部变量表中指定索引的变量
复制栈顶字节码:dup家族,这个字节码是用于复制栈顶元素并插入到栈中的字节码

操作栈与局部变量表
操作栈:用于存储中间计算结果、方法参数和返回值。
局部变量表:用于存储方法参数和局部变量。每个局部变量都有一个索引,编译器根据方法签名分配这些索引。具体索引规则: 当这个方法为静态方法时,局部变量表会将参数列表中的变量按顺序放入局部变量表中;当这个方法不是静态方法,局部变量表的0位是this,之后才会将参数列表变量依次放入表中
真实案例分析1

if (nameDesc == "onClick(Landroid/view/View;)V") {
    isOnClickMethod = true
    variableID = newLocal(Type.getObjectType("java/lang/Integer"))
    mMethodVisitor.visitVarInsn(ALOAD, 1)
    mMethodVisitor.visitVarInsn(ASTORE, variableID)
    mMethodVisitor.visitVarInsn(ALOAD, variableID )
    mv.visitMethodInsn(
            INVOKESTATIC,
            SensorsAnalyticsHookConfig.SENSORS_ANALYTICS_API,
            "trackViewOnClick",
            "(Landroid/view/View;)V",
            false
        )
}

解释:
mMethodVisitor.visitVarInsn(ALOAD, 1):从局部变量表中加载索引为 1 的对象引用(即 View 参数)到操作数栈。onClick(Landroid/view/View;)V 是一个实例方法,所以 this 占用了索引 0。方法参数 Landroid/view/View; 被分配到索引 1。
mMethodVisitor.visitVarInsn(ASTORE, variableID):将操作数栈顶的对象引用存储到索引为 variableID 的局部变量。
mMethodVisitor.visitVarInsn(ALOAD, variableID ) 这一步再将索引variableID 的对象压入操作栈(123步骤目的只是为了讲解)
mMethodVisitor.visitMethodInsn(
INVOKESTATIC,
SensorsAnalyticsHookConfig.SENSORS_ANALYTICS_API,
"trackViewOnClick",
"(Landroid/view/View;)V",
false
)用于调用方法,它会从操作数栈中弹出参数并传递给被调用的方法,
第一个参数代表类型一般埋点调用的均是静态方法,INVOKEVIRTUAL:用于调用实例方法,方法的接收者(this)在操作数栈上。
INVOKESTATIC:用于调用静态方法,不需要方法的接收者。
INVOKESPECIAL:用于调用实例初始化方法(构造方法)或父类的方法。
INVOKEINTERFACE:用于调用接口方法。
INVOKE_DYNAMIC:用于调用动态方法,通常与 Lambda 表达式相关。
第二个所调用方法类全路径,
第三个参数方法名称,
四个参数是方法的描述符(descriptor),它描述了方法的参数类型和返回类型,方法描述符的格式
参数类型:按顺序列出每个参数的类型描述符。
返回类型:在参数类型描述符之后,使用 ) 表示参数列表的结束,然后是返回类型的描述符。 L 代表对象引用类型(Object Reference Types)
最后一个代表是否是接口
真实案例分析2

  if (opcode == Opcodes.INVOKEVIRTUAL &&
            (name == "show" || name == "dismiss") &&
            descriptor == "()V" &&
            isDialogClass(owner)
        ) {
            // 在调用show()之前插入跟踪代码
            mMethodVisitor.visitInsn(Opcodes.DUP)  // 复制Dialog实例引用
            // 根据方法名选择对应的跟踪方法
            val trackMethodName = if (name == "show") "trackDialogShow" else "trackDialogDismiss"
            mMethodVisitor.visitMethodInsn(
                Opcodes.INVOKESTATIC,
                SensorsAnalyticsHookConfig.SENSORS_ANALYTICS_API,
                trackMethodName,
                "(Landroid/app/Dialog;)V",
                false
            )
        }

在 JVM 字节码中,DUP 指令用于复制操作数栈顶的值。在你的代码中,DUP 指令用于在调用 show 或 dismiss 方法之前,复制 Dialog 实例的引用,以便在调用跟踪方法时使用

为什么在 onClick 方法中不使用 DUP?
在 onClick 方法中,View 参数已经明确存储在局部变量表中,且你只需要加载这个参数并传递给 trackViewOnClick 方法。使用 LOAD 指令可以直接从局部变量表中加载参数,不需要复制操作数栈顶的值。
具体步骤:
加载参数:mv.visitVarInsn(ALOAD, 1) 从局部变量表中加载 View 参数。
调用方法:mv.visitMethodInsn 调用 trackViewOnClick 方法,传递 View 参数。
为什么在 show/dismiss 方法中使用 DUP?
在 show/dismiss 方法中,Dialog 实例的引用是在操作数栈顶,而不是存储在局部变量表中。你需要在调用 show 或 dismiss 方法之前,复制这个引用以便传递给跟踪方法,同时保留原始引用以便继续调用 show 或 dismiss 方法。
具体步骤:
复制引用:mMethodVisitor.visitInsn(Opcodes.DUP) 复制操作数栈顶的 Dialog 实例引用。
调用方法:mMethodVisitor.visitMethodInsn 调用 trackDialogShow 或 trackDialogDismiss 方法,传递复制的 Dialog 实例引用。
继续调用:原始的 Dialog 实例引用仍然在操作数栈上,可以继续调用 show 或 dismiss 方法。
总结
LOAD 指令:
用途:从局部变量表中加载指定索引的变量到操作数栈。
使用场景:当你知道参数存储在局部变量表中,并且只需要加载这个参数时使用。
操作数栈变化:
输入:[]
输出:[Dialog]
DUP 指令:
用途:复制操作数栈顶的值。
使用场景:当你需要在不改变操作数栈的情况下,保留栈顶的值以便传递给其他方法时使用。 0 - 操作数栈变化:
输入:[Dialog]
输出:[Dialog, Dialog]
真实案例分析3
方法 开头增加一行日,System.out.println("AAAA")
实现步骤
加载 System.out 字段:
使用 visitFieldInsn 加载 System.out 字段。
加载字符串常量 "AAAA":
使用 visitLdcInsn 加载字符串常量 "AAAA"。
调用 println 方法:
使用 visitMethodInsn 调用 PrintStream.println(String) 方法。

  // 执行时机:当方法开始执行时触发
    // 作用:可以在方法开始时插入自定义逻辑,例如记录方法调用、初始化变量等
    public override fun onMethodEnter() {
        super.onMethodEnter()

        // 插入 System.out.println("AAAA") 的字节码指令

        // 1. 加载 System.out 字段
        mMethodVisitor.visitFieldInsn(
            Opcodes.GETSTATIC,
            "java/lang/System",
            "out",
            "Ljava/io/PrintStream;"
        )

        // 2. 加载字符串常量 "AAAA"
        mMethodVisitor.visitLdcInsn("AAAA")

        // 3. 调用 PrintStream.println(String) 方法
        mMethodVisitor.visitMethodInsn(
            Opcodes.INVOKEVIRTUAL,
            "java/io/PrintStream",
            "println",
            "(Ljava/lang/String;)V",
            false
        )
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

友情链接更多精彩内容