Skip to content

字节码插桩

源:Android Gradle Plugin API | ASM

在编译期修改字节码,实现无痕埋点、性能监控等高级功能。

字节码插桩概念

什么是字节码插桩

字节码插桩(Bytecode Instrumentation):在 .class 文件生成后、打包进 APK 前,动态修改字节码。

执行时机

Kotlin/Java 源码
    ↓ 编译
.class 文件
    ↓ 插桩 ← 这里!
修改后的 .class
    ↓ 打包
DEX / APK

使用场景

无痕埋点

  • 自动记录所有点击事件
  • 记录页面访问
  • 网络请求监控

性能监控

  • 方法耗时统计
  • 内存分配追踪
  • 线程监控

修复 Bug

  • 修改第三方库行为
  • 热修复
  • AOP 编程

ASM 基础

ASM 是什么

ASM:Java 字节码操作和分析框架。

核心 API

  • ClassVisitor:访问类
  • MethodVisitor:访问方法
  • FieldVisitor:访问字段

字节码结构

简单示例

kotlin
fun greet(name: String) {
    println("Hello, $name")
}

字节码

public final greet(Ljava/lang/String;)V
   L0
    LDC "Hello, "
    ALOAD 1
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.stringPlus(...)
    INVOKEVIRTUAL java/io/PrintStream.println(...)
   L1
    RETURN

Transform API(已废弃)

传统 Transform

AGP 7.0 之前

kotlin
// ❌ 已废弃,不推荐
class MyTransform : Transform() {
    override fun transform(transformInvocation: TransformInvocation) {
        // 处理所有 class 文件
    }
}

问题

  • 性能差
  • 不支持增量
  • 难以并行

AsmClassVisitorFactory API

现代插桩方案

AGP 7.0+:使用 AsmClassVisitorFactory

核心接口

kotlin
interface AsmClassVisitorFactory<ParametersT : InstrumentationParameters> :
    InstrumentationArtifact {
    
    fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor
    
    fun isInstrumentable(classData: ClassData): Boolean
}

基本实现

kotlin
import com.android.build.api.instrumentation.AsmClassVisitorFactory
import com.android.build.api.instrumentation.ClassContext
import com.android.build.api.instrumentation.ClassData
import com.android.build.api.instrumentation.InstrumentationParameters
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes

interface MyParameters : InstrumentationParameters {
    // 参数
}

abstract class MyClassVisitorFactory :
    AsmClassVisitorFactory<MyParameters> {
    
    override fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor {
        return MyClassVisitor(nextClassVisitor)
    }
    
    override fun isInstrumentable(classData: ClassData): Boolean {
        // 过滤:只处理自己的代码
        return classData.className.startsWith("com.example.myapp")
    }
}

class MyClassVisitor(nextVisitor: ClassVisitor) :
    ClassVisitor(Opcodes.ASM9, nextVisitor) {
    
    override fun visitMethod(
        access: Int,
        name: String,
        descriptor: String,
        signature: String?,
        exceptions: Array<String>?
    ): MethodVisitor {
        val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
        return MyMethodVisitor(mv, name)
    }
}

class MyMethodVisitor(
    nextVisitor: MethodVisitor,
    private val methodName: String
) : MethodVisitor(Opcodes.ASM9, nextVisitor) {
    
    override fun visitCode() {
        super.visitCode()
        
        // 在方法开始插入代码
        mv.visitLdcInsn(methodName)
        mv.visitMethodInsn(
            Opcodes.INVOKESTATIC,
            "com/example/Monitor",
            "onMethodEnter",
            "(Ljava/lang/String;)V",
            false
        )
    }
}

注册插桩

build.gradle.kts

kotlin
androidComponents {
    onVariants { variant ->
        variant.instrumentation.transformClassesWith(
            MyClassVisitorFactory::class.java,
            InstrumentationScope.ALL
        ) {
            // 配置参数
        }
        
        variant.instrumentation.setAsmFramesComputationMode(
            FramesComputationMode.COPY_FRAMES
        )
    }
}

实战案例

案例1:方法耗时统计

kotlin
class TimingClassVisitor(nextVisitor: ClassVisitor) :
    ClassVisitor(Opcodes.ASM9, nextVisitor) {
    
    private var className = ""
    
    override fun visit(
        version: Int,
        access: Int,
        name: String,
        signature: String?,
        superName: String?,
        interfaces: Array<String>?
    ) {
        className = name
        super.visit(version, access, name, signature, superName, interfaces)
    }
    
    override fun visitMethod(
        access: Int,
        name: String,
        descriptor: String,
        signature: String?,
        exceptions: Array<String>?
    ): MethodVisitor {
        val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
        return TimingMethodVisitor(mv, className, name)
    }
}

class TimingMethodVisitor(
    nextVisitor: MethodVisitor,
    private val className: String,
    private val methodName: String
) : MethodVisitor(Opcodes.ASM9, nextVisitor) {
    
    override fun visitCode() {
        super.visitCode()
        
        // long startTime = System.currentTimeMillis();
        mv.visitMethodInsn(
            Opcodes.INVOKESTATIC,
            "java/lang/System",
            "currentTimeMillis",
            "()J",
            false
        )
        mv.visitVarInsn(Opcodes.LSTORE, getStartTimeSlot())
    }
    
    override fun visitInsn(opcode: Int) {
        if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
            // 在 return 前插入
            // long endTime = System.currentTimeMillis();
            mv.visitMethodInsn(
                Opcodes.INVOKESTATIC,
                "java/lang/System",
                "currentTimeMillis",
                "()J",
                false
            )
            
            // Monitor.record(className, methodName, endTime - startTime);
            mv.visitVarInsn(Opcodes.LLOAD, getStartTimeSlot())
            mv.visitInsn(Opcodes.LSUB)
            mv.visitLdcInsn(className)
            mv.visitLdcInsn(methodName)
            mv.visitMethodInsn(
                Opcodes.INVOKESTATIC,
                "com/example/Monitor",
                "record",
                "(JLjava/lang/String;Ljava/lang/String;)V",
                false
            )
        }
        super.visitInsn(opcode)
    }
    
    private fun getStartTimeSlot() = /* ... */
}

Monitor.java

java
public class Monitor {
    public static void record(long duration, String className, String methodName) {
        Log.d("Timing", className + "." + methodName + ": " + duration + "ms");
    }
}

案例2:点击事件埋点

kotlin
class ClickTrackingVisitor(nextVisitor: ClassVisitor) :
    ClassVisitor(Opcodes.ASM9, nextVisitor) {
    
    override fun visitMethod(
        access: Int,
        name: String,
        descriptor: String,
        signature: String?,
        exceptions: Array<String>?
    ): MethodVisitor {
        val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
        
        // 处理 onClick 方法
        if (name == "onClick" && descriptor == "(Landroid/view/View;)V") {
            return ClickMethodVisitor(mv)
        }
        
        return mv
    }
}

class ClickMethodVisitor(nextVisitor: MethodVisitor) :
    MethodVisitor(Opcodes.ASM9, nextVisitor) {
    
    override fun visitCode() {
        super.visitCode()
        
        // Tracker.trackClick(view);
        mv.visitVarInsn(Opcodes.ALOAD, 1)  // view 参数
        mv.visitMethodInsn(
            Opcodes.INVOKESTATIC,
            "com/example/Tracker",
            "trackClick",
            "(Landroid/view/View;)V",
            false
        )
    }
}

InstrumentationScope

作用域选择

kotlin
variant.instrumentation.transformClassesWith(
    MyFactory::class.java,
    InstrumentationScope.ALL  // 所有代码
)

// 可选值:
// - InstrumentationScope.PROJECT:仅项目代码
// - InstrumentationScope.DEPENDENCIES:仅依赖
// - InstrumentationScope.ALL:所有

最佳实践

性能优化

kotlin
override fun isInstrumentable(classData: ClassData): Boolean {
    // 尽早过滤,减少处理
    return classData.className.startsWith("com.example.myapp")
}

调试

kotlin
// 使用 ASM Bytecode Viewer 插件查看字节码
// 先写出期望的 Java 代码,再查看对应字节码

Frame 计算

kotlin
variant.instrumentation.setAsmFramesComputationMode(
    FramesComputationMode.COPY_FRAMES  // 推荐
)

错误处理

kotlin
override fun visitCode() {
    try {
        super.visitCode()
        // 插桩逻辑
    } catch (e: Exception) {
        // 记录错误但不中断构建
        logger.error("Instrumentation failed", e)
    }
}

编译开关

kotlin
// 仅在 Debug 构建启用
if (variant.buildType == "debug") {
    variant.instrumentation.transformClassesWith(...)
}