Android Gradle - ASM + AsmClassVisitorFactory插桩使用

447 阅读10分钟

前言

  前边陆续总结学习了 Task 和 Plugin,今天就开始总结插桩的使用,正好能把前边学的知识串起来,温故而知新。

  我们都知道Apk 是从资源编译 -> 源代码 (java/kotlin)-> 编译器(javac/kotlinc) -> JVM字节码 (.class) -> D8/R8 编译器 -> DEX字节码(.dex) -> 最后到我们的APK,字节码操作主要在 JVM这一步,发生在编译后,打包前的class文件环节,比较适合处理一些有规则的代码插入,在编写代码层面无侵入 不容易遗漏。

插桩

  字节码插桩(Bytecode Instrumentation)是在编译后、打包前修改 .class 文件的技术。

先看一个例子:

public class Hello {
    public void sayHello() {
        System.out.println("Hi");
    }
}

javac 编译后,生成的 Hello.class 是这样的:

CA FE BA BE 00 00 00 34 00 1D 0A
00 06 00 0F 09 00 10 00 11 08 00
12 0A 00 13 00 14 07 00 15 07 00
16 01 00 06 3C 69 6E 69 74 3E ...
(全是二进制数据)种 JVM 指令的二进制编码、计算字节偏移、维护常量池索引……

  这就是我们需要 ASM 的原因:ASM 是一个 Java 库,帮你读取、修改、生成 .class 文件,让你不用关心底层的二进制细节,借助工具来修改 Class 文件,更加简单方便。

方式优点缺点
手写代码直观、可控侵入性强、容易遗漏、难以统一
AOP 注解使用简单运行时开销、无法处理三方库
字节码插桩无侵入、可处理所有代码学习成本高

ASM重要方法

  这里插入一下 ASM几个重要的方法和作用,因为下边会有一些实例代码,关于 Transfrom 中如何修改的案例会使用到。

类名作用
ClassReader读取 .class 文件,包括类中的信息方法名和字段
ClassVisitorClassReader读取,可以在 visit、visitField和visitMethod方法中查看
MethodVisitor访问方法内部
ClassWriter生成新的 .class

ASM 和 Transform

  Transform 在AGP 4.+版本,我们默认都用的Transform 来实现Class 拦截和输出工作。但是在高版本AGP中, 根据 官方的说法 Transform API 会被移除:为了提高构建性能,使用 Transform API 很难与其它 Gradle 特性结合使用 ,不过就我自己的使用体验,确实Transform 使用起来代码更多,需要手动处理:遍历文件、读取字节、处理 Jar、写入输出等 ,多个Transform串联执行,执行效率不高

由于项目中之前是使用的 Bytex,这里我写一个 简单的transform 实例代码,仅用于参考

class MyTransform : Transform() {

    // Task 名称
    override fun getName(): String = "MyTransform"

    // 处理什么类型 class 文件
    override fun getInputTypes(): Set<QualifiedContent.ContentType> {
        return setOf(QualifiedContent.DefaultContentType.CLASSES)
    }

    // 处理代码范围
    override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
        return mutableSetOf(
            QualifiedContent.Scope.PROJECT,
            QualifiedContent.Scope.SUB_PROJECTS,
            QualifiedContent.Scope.EXTERNAL_LIBRARIES
        )
    }

    // 增量编译
    override fun isIncremental(): Boolean = true

    // 处理输入,生成输出
    override fun transform(transformInvocation: TransformInvocation) {
        val inputs = transformInvocation.inputs
        val outputProvider = transformInvocation.outputProvider
        val isIncremental = transformInvocation.isIncremental

        if (!isIncremental) {
            outputProvider.deleteAll()
        }

        // 手动遍历所有输入
        inputs.forEach { input ->

            // 处理目录输入
            input.directoryInputs.forEach { dirInput ->
                val outputDir = outputProvider.getContentLocation(
                    dirInput.name,
                    dirInput.contentTypes,
                    dirInput.scopes,
                    Format.DIRECTORY
                )

                // 需要手动遍历每个 class 文件
                dirInput.file.walkTopDown().forEach { file ->
                    if (file.isFile && file.name.endsWith(".class")) {
                        // 需要手动读取字节
                        val classBytes = file.readBytes()

                        // 使用 ASM 处理
                        val classReader = ClassReader(classBytes)
                        val classWriter = ClassWriter(ClassWriter.COMPUTE_FRAMES)
                        val classVisitor = MyClassVisitor(classWriter)
                        classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)

                        // 需要手动写入输出
                        val outputFile = File(outputDir, file.relativeTo(dirInput.file).path)
                        outputFile.parentFile.mkdirs()
                        outputFile.writeBytes(classWriter.toByteArray())
                    }
                }
            }

            // 处理 Jar 输入(依赖库)
            input.jarInputs.forEach { jarInput ->
                // 需要手动解压、处理、重新打包 Jar
                val outputJar = outputProvider.getContentLocation(
                    jarInput.name,
                    jarInput.contentTypes,
                    jarInput.scopes,
                    Format.JAR
                )
                // 解压 → 遍历 → 处理 → 重新打包...
                // 省略大量代码...
            }
        }
    }
}

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val android = project.extensions.getByType(AppExtension::class.java)
        // AGP 8.0 中已被删除
        android.registerTransform(MyTransform())
    }
}

主要功能描述都放在注释里了,这里不多赘述

AsmClassVisitorFactory示例

  相比于Transfrom,新的AsmClassVisitorFactory 帮你完成遍历 Class文件,读取字节码 遍历ClassReader/ClassWriter 并处理jar和增量编辑等逻辑,根据官网所说,代码减少五倍 (不敢苟同哈,但是以前确实要实现5个抽象方法, 相比来说较少写很多代码是真的)。

  我们这里重点讲新的Transfrom的方式,我们这里实现一个 方法耗时打印的插桩。

image.png

文件名作用关键方法/属性
MethodTracePlugin.kt插件入口,注册到 Gradleapply() - 创建扩展配置、注册 AsmClassVisitorFactory
MethodTraceExtension.ktDSL 扩展配置,供 build.gradle 使用enable、enableLog、includePackages、excludePackages、thresholdMs
MethodTraceParams.kt传递给 Factory 的参数接口与 Extension 对应的 Property 参数
MethodTraceClassVisitorFactory.ktAGP 的入口工厂类,决定哪些类需要插桩isInstrumentable() - 过滤类
createClassVisitor() - 创建 ClassVisitor
MethodTraceClassVisitor.kt类级别访问器,遍历类中的方法visitMethod() - 返回自定义的 MethodVisitor
MethodTraceMethodVisitor.kt方法级别访问器,真正插入字节码onMethodEnter() - 方法入口插桩
onMethodExit() - 方法出口插桩

超级多代码警告!!

class MethodTracePlugin : Plugin<Project> {

    companion object {
        const val TAG = "MethodTracePlugin"
    }

    override fun apply(project: Project) {
        println("$TAG: 插件已应用")
        // 创建扩展配置
        val extension = project.extensions.create(
            "methodTrace",
            MethodTraceExtension::class.java
        )
        // 获取 Android 组件扩展
        val androidComponents = project.extensions.findByType(
            AndroidComponentsExtension::class.java
        ) ?: run {
            println("$TAG: 未找到 Android 组件,跳过")
            return
        }
        // AGP 的回调执行顺序:onVariants → afterEvaluate
        androidComponents.onVariants { variant ->
            // 使用 Provider 延迟获取配置值(配置阶段还没完成)
            val enabled = extension.enable.getOrElse(true)
            
            if (!enabled) {
                println("$TAG: 插件已禁用,跳过变体 ${variant.name}")
                return@onVariants
            }
            
            println("$TAG: 配置变体 ${variant.name}")

            variant.instrumentation.transformClassesWith(
                MethodTraceClassVisitorFactory::class.java,
                InstrumentationScope.PROJECT  // 只处理项目代码
            ) { params ->
                // 使用 Provider 链接,这样值会在真正需要时才获取
                params.enableLog.set(extension.enableLog)
                params.includePackages.set(extension.includePackages)
                params.excludePackages.set(extension.excludePackages)
                params.thresholdMs.set(extension.thresholdMs)
            }

            variant.instrumentation.setAsmFramesComputationMode(
                FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
            )
        }
    }
}

  MethodTracePlugin 是整个插件的入口,是需要注册在gradle的插件中的,不然是找不到该插件的

interface MethodTraceParams : InstrumentationParameters {
    @get:Input
    val enableLog: Property<Boolean>  // 插件开关

    @get:Input
    val includePackages: ListProperty<String> // 要处理的包名列表

    @get:Input
    val excludePackages: ListProperty<String>  // 要排除的白名单列表

    @get:Input
    val thresholdMs: Property<Long>   // 方法打印毫秒阈值
}
abstract class MethodTraceExtension {

    abstract val enable: Property<Boolean>

    abstract val enableLog: Property<Boolean>

    abstract val includePackages: ListProperty<String>

    abstract val excludePackages: ListProperty<String>

    abstract val thresholdMs: Property<Long>

    init {
        enable.convention(true)
        enableLog.convention(false)
        includePackages.convention(emptyList())
        excludePackages.convention(emptyList())
        thresholdMs.convention(0L)
    }
}

  我们可以根据自己想要自定义的部分,MethodTraceExtension为 gradle 扩展配置,方便用户在App中使用自定义关键阈值和开关等。

abstract class MethodTraceClassVisitorFactory :
    AsmClassVisitorFactory<MethodTraceParams> {

    override fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor {
        return MethodTraceClassVisitor(
            nextClassVisitor,
            classContext.currentClassData.className,
            parameters.get().enableLog.get(),
            parameters.get().thresholdMs.get()
        )
    }

    override fun isInstrumentable(classData: ClassData): Boolean {
        val className = classData.className
        val params = parameters.get()

        // 排除 MethodTracer 本身,避免死循环!
        if (className.contains("MethodTracer")) {
            return false
        }

        // 检查排除列表
        val excludePackages = params.excludePackages.get()
        for (pkg in excludePackages) {
            if (className.startsWith(pkg)) {
                return false
            }
        }

        // 检查包含列表
        val includePackages = params.includePackages.get()
        if (includePackages.isEmpty()) {
            // 如果没有配置,默认处理所有非系统类
            return !className.startsWith("android.") &&
                    !className.startsWith("androidx.") &&
                    !className.startsWith("kotlin.") &&
                    !className.startsWith("java.") &&
                    !className.startsWith("javax.")
        }
        for (pkg in includePackages) {
            if (className.startsWith(pkg)) {
                return true
            }
        }
        return false
    }
}

  接下来就实现 已经在 MethodTracePlugin 指定的工厂层 MethodTraceClassVisitorFactory ,结合 isInstrumentable 方法可以控制是否跳过对应class ,createClassVisitor方法 则是创建 对应的方法处理类。

class MethodTraceClassVisitor (
    nextVisitor: ClassVisitor,
    private val className: String,
    private val enableLog: Boolean,
    private val thresholdMs: Long
) : ClassVisitor(Opcodes.ASM9, nextVisitor) {

    override fun visitMethod(
        access: Int,
        name: String,
        descriptor: String,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor? {

        val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
            ?: return null

        // 跳过构造方法和静态初始化块
        if (name == "<init>" || name == "<clinit>") {
            return mv
        }

        // 跳过抽象方法和 native 方法
        if ((access and Opcodes.ACC_ABSTRACT) != 0 ||
            (access and Opcodes.ACC_NATIVE) != 0) {
            return mv
        }

        if (enableLog) {
            println("  插桩方法: $className#$name$descriptor")
        }

        // 返回自定义的 MethodVisitor,传递阈值参数
        return MethodTraceMethodVisitor(
            mv,
            access,
            className,
            name,
            descriptor,
            thresholdMs  // 传递耗时阈值
        )
    }
}

   MethodTraceClassVisitor 主要作用就是 遍历方法,过滤一些特殊的方法,可主要针对核心业务代码进行选择。

class MethodTraceMethodVisitor(
    methodVisitor: MethodVisitor,
    access: Int,
    private val className: String,      // 第3个参数:类名
    private val methodName: String,     // 第4个参数:方法名  
    descriptor: String,                 // 第5个参数:方法描述符
    private val thresholdMs: Long       // 第6个参数:耗时阈值(毫秒)
) : AdviceAdapter(Opcodes.ASM9, methodVisitor, access, methodName, descriptor) {

    // 存储开始时间的局部变量索引
    private var startTimeVarIndex = 0

    override fun onMethodEnter() {
        startTimeVarIndex = newLocal(org.objectweb.asm.Type.LONG_TYPE)

        mv.visitMethodInsn(
            INVOKESTATIC,
            "java/lang/System",
            "nanoTime",
            "()J",
            false
        )
        mv.visitVarInsn(LSTORE, startTimeVarIndex)
    }

    override fun onMethodExit(opcode: Int) {
        if (opcode != ATHROW) {
            printMethodCost()
        }
    }

    private fun printMethodCost() {
        mv.visitMethodInsn(
            INVOKESTATIC,
            "java/lang/System",
            "nanoTime",
            "()J",
            false
        )
        mv.visitVarInsn(LLOAD, startTimeVarIndex)
        mv.visitInsn(LSUB)  // 执行减法
        
        // 存储耗时结果
        val durationVarIndex = newLocal(org.objectweb.asm.Type.LONG_TYPE)
        mv.visitVarInsn(LSTORE, durationVarIndex)

        if (thresholdMs > 0) {
            val skipLabel = org.objectweb.asm.Label()
            
            // 加载 durationNs
            mv.visitVarInsn(LLOAD, durationVarIndex)
            // 加载阈值(转换为纳秒)
            mv.visitLdcInsn(thresholdMs * 1_000_000L)
            mv.visitInsn(LCMP)
            // 如果 durationNs < threshold,跳过 trace 调用
            mv.visitJumpInsn(IFLT, skipLabel)
            
            // 调用 trace
            invokeTrace(durationVarIndex)
            
            // 跳过标签
            mv.visitLabel(skipLabel)
        } else {
            // 没有阈值限制,直接调用
            invokeTrace(durationVarIndex)
        }
    }
    
    private fun invokeTrace(durationVarIndex: Int) {
        // 调用 MethodTracer.trace(className, methodName, durationNs)
        mv.visitLdcInsn(className.replace("/", "."))  // 第1个参数: className
        mv.visitLdcInsn(methodName)                    // 第2个参数: methodName
        mv.visitVarInsn(LLOAD, durationVarIndex)       // 第3个参数: durationNs (long)
        
        mv.visitMethodInsn(
            INVOKESTATIC,
            "com/xmly/gradley/trace/MethodTracer",  // 对应 app 中的类
            "trace",
            "(Ljava/lang/String;Ljava/lang/String;J)V",  // (String, String, long) -> void
            false
        )
    }
}

   这里其实才是 真正的字节码插入的地方,onMethodEnter 方法开始的地方 插入获取当前时间,onMethodExit 方法结束的时候,插入 MethodTracer.trace ,用来打印实际耗时。

最后不要忘记再 gradle-plugin 的 build.gradle.kts中注册插件

create("methodTrace") {
    id = "com.xmly.methodtrace"
    implementationClass = "com.xmly.plugin.trace.MethodTracePlugin"
    displayName = "Method Trace Plugin"
    description = "方法耗时统计插件"
}

最后就是我们的 发布了, plugin library 都是需要发布的

./gradlew :gradle-plugin:publish

对应的,我们在前边自定义了 Gradle扩展函数,所以我们可以自定义一些 plugin 逻辑,我们来到 App build.gradle 中引入插件

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
    ...
    id 'com.xmly.methodtrace' version '1.0.0'  // 方法耗时插件
}

// 基于 gradle 拓展函数 ,就可以控制开关和 时间戳限制等逻辑
methodTrace {
    enable = true
    enableLog = true
    includePackages = ['com.xmly.gradley']
    excludePackages = ['com.xmly.gradley.test']
    thresholdMs = 1L  // 只记录超过 1ms 的方法
}


最后,我们实现以下 App文件夹下 对应的 log实现类

object MethodTracer {

    private const val TAG = "MethodTracer"

    // 是否启用(运行时开关)
    private var enabled = true

    /**
     * 运行时开关,可动态禁用日志输出
     */
    @JvmStatic
    fun setEnabled(enabled: Boolean) {
        this.enabled = enabled
    }

    /**
     * 记录方法耗时
     * 
     * 注意:阈值判断已在编译时完成(ASM 插桩生成了 if 判断),
     * 所以这里只负责打印,不再做阈值判断。
     * 
     * @param className 类名
     * @param methodName 方法名
     * @param durationNs 耗时(纳秒)
     */
    @JvmStatic
    fun trace(className: String, methodName: String, durationNs: Long) {
        if (!enabled) return

        // 阈值判断已在编译时完成,这里直接打印
        val durationMs = durationNs / 1_000_000.0
        Log.d(TAG, String.format(
            "%.2fms | %s#%s",
            durationMs,
            className,
            methodName
        ))
    }
}

这里重点看trace 即可,主要就是用来打印

ASM插桩 LayoutInflater 拓展

  这里简单聊一下对上述方法的拓展,比如说我现在需要对布局绘制进行一些简单的耗时打点,就可以借助上边的办法,针对 View view = layoutInflater.inflate(R.layout.activity_main, container)的代码进行插桩。

目标示例代码如下:

// 方式一:直接替换方法调用
View view = LayoutInflaterAgent.wrapInflate(layoutInflater, R.layout.activity_main, container);

// 方式二:前后包裹(记录耗时)
long startTime = System.currentTimeMillis();
View view = layoutInflater.inflate(R.layout.activity_main, container);
long cost = System.currentTimeMillis() - startTime;
LayoutInflaterAgent.inflateHook(view, R.layout.activity_main, cost);

我们重点模拟下 方法替换 Visitor 的代码,主要是针对 三个方法,inflate(int resource, ViewGroup root)inflate(int resource, ViewGroup root, boolean attachToRoot)ViewStub.inflate()三个方法进行拦截和替换

public class InflateMethodVisitor extends MethodVisitor implements Opcodes {
    private static final String TO_CLASS = "com/ximalaya/commonaspectj/LayoutInflaterAgent";
    private String mName, mDesc;
    private MethodInsnNode targetMethodNode = new MethodInsnNode(INVOKEVIRTUAL,
            "android/view/LayoutInflater",
            "inflate",
            INFLATEDESC_1, false);

    private MethodInsnNode viewStubMethodNode = new MethodInsnNode(INVOKEVIRTUAL,
            "android/view/ViewStub", "inflate",
            "()Landroid/view/View;", false);

    public InflateMethodVisitor(int api, MethodVisitor methodVisitor, String name, String desc) {
        super(api, methodVisitor);
        this.mName = name;
        this.mDesc = desc;
    }

    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
        if (owner.equals(targetMethodNode.owner) && name.equals(targetMethodNode.name)) {
            if (descriptor.equals(INFLATEDESC_1)) {
                mv.visitMethodInsn(INVOKESTATIC, TO_CLASS, "wrapInflate", "(Landroid/view/LayoutInflater;ILandroid/view/ViewGroup;)Landroid/view/View;", false);
                XLog.INSTANCE.d( getLogTag(),"2_"+className + "#" + mName + mDesc);

            } else if (descriptor.equals(INFLATEDESC_2)) {
                mv.visitMethodInsn(INVOKESTATIC, TO_CLASS, "wrapInflate", "(Landroid/view/LayoutInflater;ILandroid/view/ViewGroup;Z)Landroid/view/View;", false);
                XLog.INSTANCE.d(getLogTag(), "3_" +className + "#" + mName + mDesc);
            } else {
                super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
            }
        } else if (owner.equals(viewStubMethodNode.owner) && name.equals(viewStubMethodNode.name) && descriptor.equals(viewStubMethodNode.desc)) {
            mv.visitMethodInsn(INVOKESTATIC, TO_CLASS, "wrapInflate", "(Landroid/view/ViewStub;)Landroid/view/View;", false);
            XLog.INSTANCE.d(getLogTag(),"3_"+className + "#" + mName + mDesc);

            XLog.INSTANCE.i(getLogTag(),"className: " + className
                    + ", name: " + name + ", descriptor: " + descriptor
                    + ", owner: " + owner + ", mName: " + mName
                    + ", mDesc: " + mDesc);
        } else {
            super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
        }

    }
}

  其实原理和 检测方法耗时是一样的,只是关于方法的替换逻辑不一样,其他的 apm其实大部分原理都差不多。

实例代码GitHub

github.com/yangmingchu…

最后

  到这里基本结束了,其实主要是列了一下 新版本的transform的使用方式以及 ASM plugin的开发和使用。关于其他的ASM用来实现什么apm逻辑,就要看自己举一反三了。最近也是终于有空把本地的 Gradle和 ASM插桩知识总结了一下,临近年关,又要对自己来一次 Review,感觉很多知识类的在AI的加持下,可以更快的上手和落地实现了,后续也会把本地的AI使用做一个归纳和总结,后续也要把输出文章重点放在AI侧方向

参考文章