前言
前边陆续总结学习了 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侧方向。