上一篇讲了 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。
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/位置等敏感字段
踩过的坑
Kotlin Metadata 丢失:
ClassWriter.COMPUTE_MAXS会让 Kotlin Metadata 失效,处理时单独保留lambda 内部方法名是
invoke$lambda$N:要还原原始名字才能定位AGP 8.x 的 KGP 兼容性:Kotlin Gradle Plugin 升级时 ASM API 可能变,要用 ASM 9+
混淆后插桩:R8 会重命名类、删除方法。最好在 R8 前插桩
小结
字节码插桩是"高级 Android 工程师"的必修课。性能监控、线上问题排查、统一行为都靠它。但 ASM 学习曲线陡,建议从"看懂别人写的 Visitor"开始,再自己写。性能监控这种通用需求直接用成熟 SDK(Matrix、ArgusAPM),自己造轮子意义不大。
下一篇进入热修复原理——ASM 在"修复线上 bug"中的实战。