之前在写 SimpleLogger
的插件时,有用到过 ASM
这个字节码编辑工具,使用过程中有很多需要注意的点,现在这里记录一下。
ASM
官方网站 https://asm.ow2.io/
引入 ASM
1 | implementation 'org.ow2.asm:asm:6.0' |
ASM 提供了两组API:Core和Tree:
Core是基于访问者模式来操作类的
Tree是基于树节点来操作类的
这边主要使用的是 ASM 的 CoreAPI。
CoreAPI
ASM 内部采用 访问者模式 将 .class 类文件的内容从头到尾扫描一遍,每次扫描到类文件相应的内容时,都会调用ClassVisitor内部相应的方法。
比如:
- 扫描到类文件时,会回调ClassVisitor的visit()方法;
- 扫描到类注解时,会回调ClassVisitor的visitAnnotation()方法;
- 扫描到类成员时,会回调ClassVisitor的visitField()方法;
- 扫描到类方法时,会回调ClassVisitor的visitMethod()方法;
- ······
扫描到相应结构内容时,会回调相应方法,该方法会返回一个对应的字节码操作对象(比如,visitMethod()返回MethodVisitor实例),通过修改这个对象,就可以修改class文件相应结构部分内容,最后将这个ClassVisitor字节码内容覆盖原来.class文件就实现了类文件的代码切入。
举例说明这样一个类1
2
3
4
5
6
7
8
9
10
11
12
13
14"test") (special =
public class Test {
public int onLifeStart(int k) {
int s = 5;
return s;
}
public int onLifeEnd() {
return 100;
}
}
这个类可以在ClassVisitor
中的 visitAnnotation
方法中获得 @LifeLog
注解,在 visitMethod
方法中获得方法名获得方法名,参数类型等。而后在方法里面的 AdviceAdapter
才能获取方法上注解以及实现在 onMethodEnter
和 onMethodExit
中注入代码的操作。
换言之,若想获得类里面更多的信息可能需要 ASM 多层访问才能实现。
类型描述符
Java类型分为基本类型和引用类型,在 JVM 中对每一种类型都有与之相对应的类型描述,同样,load指令也是需要一一对应的,如下表:
Java | JVM 描述符 | LOAD 指令 |
---|---|---|
boolean | Z | ILOAD |
char | C | ILOAD |
byte | B | ILOAD |
short | S | ILOAD |
int | I | ILOAD |
float | F | FLOAD |
long | J | LLOAD |
double | D | DLOAD |
Object | Ljava/lang/Object; | ALOAD |
int[] | [I | ALOAD |
ps: 几维数组就表示在前面加几个中括号,String[][] => [[Ljava/lang/String;
理解类型描述符后就能读懂一个方法描述符传达了什么意义,比如在访问类的方法中:1
2
3
4
5
6
7
8override fun visitMethod(
access: Int,
name: String?,
desc: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
}
access 代表方法修饰符,比如final,static 等都可以从中获取
name 自然就是代表方法名
desc 参数描述符,可以知道参数类型和返回类型,如 (IF)V
代表 void m(int i, float f)
signature 泛型标识
exceptions 异常抛出
Warning 注意:在利用方法 visitVarInsn(int,int)
循环load方法参数时,有两个点要注意,
当方法不为 static 时,第一个参数为 this 故 index 要为 1
DLOAD 和 LLOAD 中的字节属于双字节,故需要在下一次load时的 index 多加上1.
如下,这是循环拼接参数的一段代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25var extraPlus = if (isStatic) 0 else 1
for (i in 0 until count) {
val loadCode = getParamCode(paramList[i])
mv.visitVarInsn(loadCode, i + extraPlus)
if (loadCode == Opcodes.DLOAD || loadCode == Opcodes.LLOAD) {
extraPlus += 1
}
mv.visitMethodInsn(
INVOKEVIRTUAL,
"java/lang/StringBuilder",
"append",
"(${getParamStr(paramList[i])})Ljava/lang/StringBuilder;",
false
)
if (i != count - 1) {
mv.visitLdcInsn(",");
mv.visitMethodInsn(
INVOKEVIRTUAL,
"java/lang/StringBuilder",
"append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;",
false
)
}
}
适配 Android
ASM 读取类时的 ClassLoader 是没有加载 Android.jar,故如果要放在 Android 环境下使用,则需要传入一个已经加载 Android.jar 的URLClassLoader
1 | // 在 `AppExtension` 中获取 android.jar 地址 |
总结
ASM 初上手比较困难,使用过程难免会碰到各种问题,这里推荐一些能加快开发效率的工具和库。
首先是一款 Android Studio 上的插件,名为 ASM Bytecode Viewer
, 直接搜就能找到,它能帮助你直接把 .class 文件转成 ASMified 文件,可以直观的查看Java代码是如何使用 ASM 实现的
另外一个是 Hunter 的开源库,它能帮你快速开发插件,在编译过程中修改字节码,它的底层也是基于ASM 和 Gradle Transform API 实现的。里面代码很简单,主要就是帮你处理了增量编译,ASM的Android适配等一些常规问题。上面说的 Android 适配问题就是从里面找到解决的。