ASM使用笔记

之前在写 SimpleLogger的插件时,有用到过 ASM 这个字节码编辑工具,使用过程中有很多需要注意的点,现在这里记录一下。

ASM 官方网站 https://asm.ow2.io/

引入 ASM

1
2
3
implementation 'org.ow2.asm:asm:6.0'
implementation 'org.ow2.asm:asm-util:6.0'
implementation 'org.ow2.asm:asm-commons: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文件就实现了类文件的代码切入。

ASM时序图

举例说明这样一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@LifeLog(special = "test")
public class Test {

@LifeLogStart
public int onLifeStart(int k) {
int s = 5;
return s;
}

@LifeLogEnd
public int onLifeEnd() {
return 100;
}
}

这个类可以在ClassVisitor 中的 visitAnnotation 方法中获得 @LifeLog 注解,在 visitMethod方法中获得方法名获得方法名,参数类型等。而后在方法里面的 AdviceAdapter 才能获取方法上注解以及实现在 onMethodEnteronMethodExit 中注入代码的操作。

换言之,若想获得类里面更多的信息可能需要 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
8
override 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
25
var 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 在 `AppExtension` 中获取 android.jar 地址
AppExtension appExtension = (AppExtension) project.getProperties().get("android");
String sdkDirectory = appExtension.getSdkDirectory().getAbsolutePath();
String compileSdkVersion = appExtension.getCompileSdkVersion();
sdkDirectory = sdkDirectory + File.separator + "platforms" + File.separator;
return sdkDirectory + compileSdkVersion + File.separator + "android.jar";

// 使用 URIClassLoader 加载
public static URLClassLoader getClassLoader(Collection<TransformInput> inputs,
Collection<TransformInput> referencedInputs,
Project project) throws MalformedURLException {
ImmutableList.Builder<URL> urls = new ImmutableList.Builder<>();
String androidJarPath = getAndroidJarPath(project);
if (androidJarPath != null) {
File file = new File(androidJarPath);
URL androidJarURL = file.toURI().toURL();
urls.add(androidJarURL);
for (TransformInput totalInputs : Iterables.concat(inputs, referencedInputs)) {
for (DirectoryInput directoryInput : totalInputs.getDirectoryInputs()) {
if (directoryInput.getFile().isDirectory()) {
urls.add(directoryInput.getFile().toURI().toURL());
}
}
for (JarInput jarInput : totalInputs.getJarInputs()) {
if (jarInput.getFile().isFile()) {
urls.add(jarInput.getFile().toURI().toURL());
}
}
}
}
ImmutableList<URL> allUrls = urls.build();
URL[] classLoaderUrls = allUrls.toArray(new URL[allUrls.size()]);
return new URLClassLoader(classLoaderUrls);
}

//最后将URIClassLoader传入 ClassWriter 中
val classWriter = AndroidClassWriter(urlClassLoader, ClassWriter.COMPUTE_MAXS)
val classVisitor = LogClassVisitor(Opcodes.ASM6, classWriter, className, logExtension)

总结

ASM 初上手比较困难,使用过程难免会碰到各种问题,这里推荐一些能加快开发效率的工具和库。
首先是一款 Android Studio 上的插件,名为 ASM Bytecode Viewer, 直接搜就能找到,它能帮助你直接把 .class 文件转成 ASMified 文件,可以直观的查看Java代码是如何使用 ASM 实现的

另外一个是 Hunter 的开源库,它能帮你快速开发插件,在编译过程中修改字节码,它的底层也是基于ASM 和 Gradle Transform API 实现的。里面代码很简单,主要就是帮你处理了增量编译,ASM的Android适配等一些常规问题。上面说的 Android 适配问题就是从里面找到解决的。


参考资料
ASM 简介
ASM 4.0 A Java bytecode engineering library

文章作者: cpacm
文章链接: http://www.cpacm.net/2019/11/20/ASM使用笔记/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 cpacm
打赏
  • 微信
  • 支付宝

评论