Android Gradle - ASM + AsmClassVisitorFactory插桩使用

前言

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

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

插桩

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

先看一个例子:

csharp 复制代码
public class Hello {
    public void sayHello() {
        System.out.println("Hi");
    }
}

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

r 复制代码
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 文件,包括类中的信息方法名和字段
ClassVisitor ClassReader读取,可以在 visit、visitField和visitMethod方法中查看
MethodVisitor 访问方法内部
ClassWriter 生成新的 .class

ASM 和 Transform

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

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

kotlin 复制代码
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的方式,我们这里实现一个 方法耗时打印的插桩。

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

超级多代码警告!!

kotlin 复制代码
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的插件中的,不然是找不到该插件的

kotlin 复制代码
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>   // 方法打印毫秒阈值
}
kotlin 复制代码
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中使用自定义关键阈值和开关等。

kotlin 复制代码
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方法 则是创建 对应的方法处理类。

kotlin 复制代码
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 主要作用就是 遍历方法,过滤一些特殊的方法,可主要针对核心业务代码进行选择。

kotlin 复制代码
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中注册插件

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

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

ruby 复制代码
./gradlew :gradle-plugin:publish

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

ini 复制代码
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实现类

kotlin 复制代码
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)的代码进行插桩。

目标示例代码如下:

ini 复制代码
// 方式一:直接替换方法调用
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()三个方法进行拦截和替换

java 复制代码
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其实大部分原理都差不多。

最后

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

参考文章

相关推荐
布列瑟农的星空2 小时前
webpack迁移rsbuild——配置深度对比
前端
前端小黑屋2 小时前
查看项目中无引用到的文件、函数
前端
前端小黑屋2 小时前
小程序直播挂件Pendant问题
前端·微信小程序·直播
俊男无期2 小时前
超效率工作法
java·前端·数据库
LYFlied2 小时前
【每日算法】LeetCode 46. 全排列
前端·算法·leetcode·面试·职场和发展
刘一说2 小时前
Vue Router:官方路由解决方案解析
前端·javascript·vue.js
wgego2 小时前
Polar靶场web 随记
前端
csdn12259873362 小时前
Android将应用添加到默认打开方式
android
DEMO派2 小时前
深拷贝 structuredClone 与 JSON 方法作用及比较
前端