Administrator
发布于 2026-08-25 / 23 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 52 篇:ASM 字节码插桩:从方法计时到全埋点

上一篇讲了 Kotlin 编译器插件能在源码层改代码,这篇说字节码层——ASM。性能监控、无埋点统计、热修复都靠它。比编译器插件暴力,但更底层、更稳定。

什么是字节码插桩

Java/Kotlin 编译完生成 .class 文件,里面是 JVM 字节码指令。插桩就是在编译产物上插入额外指令,比如:

// 原始方法
public void onClick(View v) {
    handleClick();
}

// 插桩后
public void onClick(View v) {
    long start = System.nanoTime();
    handleClick();
    Metrics.record("onClick", System.nanoTime() - start);
}

看不到源码、不用修改 Kotlin 代码,对业务零侵入。

两种主流工具

工具

层级

优点

缺点

ASM

字节码

性能最快、控制最细

需要懂字节码

javassist

源码

写 Java 语法即可

性能差、不能改方法签名

性能监控类选 ASM(追求性能),快速原型选 javassist。

Gradle Transform 的前世今生

Android Gradle Plugin 的 Transform API 一直是字节码插桩的官方入口:

// 旧 Transform API(AGP 7.0 弃用)
class MethodTraceTransform : Transform() {
    override fun getName() = "MethodTrace"
    override fun getScopes() = setOf(SCOPE_FULL)
    override fun isIncremental() = true

    override fun transform(invocation: TransformInvocation) {
        // 遍历所有 .class 文件,调用 ASM 修改
    }
}

AGP 8.0 后废弃,改用 AndroidComponentsExtension 注册 AsmClassVisitorFactory:

// app/build.gradle.kts
androidComponents {
    onVariants(selector().all()) { variant ->
        variant.instrumentation.transformClassesWith(
            MethodTraceVisitorFactory::class.java,
            InstrumentationScope.PROJECT
        ) { params ->
            params.packageName.set("com.example.app")
        }
        variant.instrumentation.setAsmFramesComputationMode(
            FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
        )
    }
}

VisitorFactory:

abstract class MethodTraceVisitorFactory : AsmClassVisitorFactory<Params> {
    override fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor = MethodTraceClassVisitor(nextClassVisitor)

    override fun isInstrumentable(classData: ClassData): Boolean {
        // 只处理我们自己包的类
        return classData.className.startsWith("com.example.app.")
    }
}

interface Params : InstrumentationParameters {
    @get:PropertyName("packageName")
    val packageName: Property<String>
}

用 ASM 写一个计时插桩

class MethodTraceClassVisitor(next: ClassVisitor) :
    ClassVisitor(Opcodes.ASM9, next) {

    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)

        // 跳过抽象方法和 <init>
        if (access and Opcodes.ACC_ABSTRACT != 0) return mv
        if (name == "<init>") return mv

        return MethodTraceMethodVisitor(mv)
    }
}

class MethodTraceMethodVisitor(next: MethodVisitor) :
    MethodVisitor(Opcodes.ASM9, next) {

    private var startTime: Int = -1

    override fun visitCode() {
        // 方法开始:插入 long start = System.nanoTime();
        startTime = newLocal(org.objectweb.asm.Type.LONG_TYPE)
        super.visitMethodInsn(
            Opcodes.INVOKESTATIC,
            "java/lang/System",
            "nanoTime",
            "()J"
        )
        super.visitVarInsn(Opcodes.LSTORE, startTime)
        super.visitCode()
    }

    override fun visitInsn(opcode: Int) {
        // 方法返回前:插入 Metrics.record(...)
        if (opcode in returnOpcodes) {
            super.visitVarInsn(Opcodes.LLOAD, startTime)
            super.visitMethodInsn(
                Opcodes.INVOKESTATIC,
                "java/lang/System",
                "nanoTime",
                "()J"
            )
            super.visitInsn(Opcodes.LSUB)
            super.visitVarInsn(Opcodes.LSTORE, startTime)
            super.visitVarInsn(Opcodes.LLOAD, startTime)
            super.visitLdcInsn("$methodName") // 标签
            super.visitMethodInsn(
                Opcodes.INVOKESTATIC,
                "com/example/Metrics",
                "recordMethodCost",
                "(JLjava/lang/String;)V"
            )
        }
        super.visitInsn(opcode)
    }

    companion object {
        private val returnOpcodes = setOf(
            Opcodes.RETURN, Opcodes.IRETURN, Opcodes.LRETURN,
            Opcodes.FRETURN, Opcodes.DRETURN, Opcodes.ARETURN
        )
    }
}

栈帧、局部变量表控制是最容易出错的点。COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS 让 ASM 自动重算栈帧,省一半精力。

过滤要插桩的类

全量插桩非常慢,常见过滤策略:

override fun isInstrumentable(classData: ClassData): Boolean {
    val name = classData.className
    // 排除 R、BuildConfig、Generated、注解
    if (name.endsWith(".R") || name.endsWith(".BuildConfig")) return false
    if (name.contains(".generated.")) return false
    if (classData.annotations.contains("kotlin.Metadata")) return false
    // 只处理指定包
    return name.startsWith("com.example.app.feature.")
}

实战:一个方法的耗时统计 SDK

object Metrics {
    @JvmStatic
    fun recordMethodCost(nanos: Long, methodName: String) {
        if (nanos > THRESHOLD_MS) {
            Log.w("Metrics", "慢方法 $methodName: ${nanos / 1_000_000} ms")
        }
        // 实际工程中发送到 APM 服务
    }
}

构建时间会增加 5-15%(第一次编译),增量编译几乎无感。包体积增加 100KB 左右(MethodTraceVisitor)。

还有哪些用例

  • 统一日志:所有 Log.d 改成带 tag、traceId 的格式

  • 空检查:方法参数自动 null check(Java 互调时)

  • Crash 自愈:try-catch 包裹风险方法

  • 隐私合规:检测方法里有没有读写 IMEI/位置等敏感字段

踩过的坑

  1. Kotlin Metadata 丢失:ClassWriter.COMPUTE_MAXS 会让 Kotlin Metadata 失效,处理时单独保留

  2. lambda 内部方法名是 invoke$lambda$N:要还原原始名字才能定位

  3. AGP 8.x 的 KGP 兼容性:Kotlin Gradle Plugin 升级时 ASM API 可能变,要用 ASM 9+

  4. 混淆后插桩:R8 会重命名类、删除方法。最好在 R8 前插桩

小结

字节码插桩是"高级 Android 工程师"的必修课。性能监控、线上问题排查、统一行为都靠它。但 ASM 学习曲线陡,建议从"看懂别人写的 Visitor"开始,再自己写。性能监控这种通用需求直接用成熟 SDK(Matrix、ArgusAPM),自己造轮子意义不大。

下一篇进入热修复原理——ASM 在"修复线上 bug"中的实战。


评论