Android ASM 修改 .class 文件
在 Android 开发中,ASM 是一个通用的字节码操控和分析框架,它主要用于读取、修改和生成 Java 字节码。ASM 全称为 “Abstract Syntax Manipulation”,可以直接操作 .class 文件,使开发者能够在字节码层面进行性能优化、插桩代码、性能监测和其他操作,而不需要关注 Java 源代码。这在需要动态修改字节码的库(如AOP框架)中非常常用。
testImplementation("org.ow2.asm:asm:9.6")
testImplementation("org.ow2.asm:asm-commons:9.6")
由于是测试模块使用,所以用 testImplementation
创建一个测试类 TestMain:
public class TestMain {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(1000);
}
}
然后使用 javac 把这个这个文件编译成 .class 文件
javac D:\demo\my_demo\app\src\test\java\com\example\my_demo\TestMain.java
这个路径是该文件的全路径(自行替换)
编译好之后该文件路径下就会多出一个 TestMain.class,可以双击点开查看:
假设要对这个 class 文件中的方法加一个耗时打印,则需要在 class 文件中的方法的开头和结尾都加一些逻辑
在执行的 main 方法中:
fun main() {
val fis = FileInputStream("D:\\demo\\my_demo\\app\\src\\test\\java\\com\\example\\my_demo\\TestMain.class")
val classReader = ClassReader(fis)
val classWrite = ClassWriter(ClassWriter.COMPUTE_FRAMES)
val classVisitor = object : ClassVisitor(Opcodes.ASM9, classWrite) {
// 解析到方法时,回调此函数
override fun visitMethod(access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
val rawVisitMethod = super.visitMethod(access, name, descriptor, signature, exceptions)
return MyMethodVisitor(Opcodes.ASM9, rawVisitMethod, access, name, descriptor)
}
}
classReader.accept(classVisitor, 0) // 开始访问 Class 文件
// 将新 Class 文件输出
val fos = FileOutputStream("D:\\demo\\my_demo\\app\\src\\test\\java\\com\\example\\my_demo\\TestMain2.class")
fos.write(classWrite.toByteArray())
fos.close()
}
首先拿到 class 文件,通过 ClassReader 和 ClassVisitor 配合使用,解析 class 文件中的内容(方法、字段等信息都会通过接口回调回来)。我们就可以根据回调回来的信息进行手动修改,修改完成后通过 ClassWriter 输出成一个新的 class 文件
自定义的方法修改类 MyMethodVisitor:
class MyMethodVisitor(api: Int, methodVisitor: MethodVisitor?, access: Int, name: String?, descriptor: String?) : AdviceAdapter(api, methodVisitor, access, name, descriptor) {
var start = 0
// 方法开始时调用
override fun onMethodEnter() {
super.onMethodEnter()
// 插入:long start = System.currentTimeMillis();
invokeStatic(Type.getType("Ljava/lang/System;"), Method("currentTimeMillis", "()J"))
start = newLocal(Type.LONG_TYPE)
storeLocal(start)
}
// 方法结束时调用
override fun onMethodExit(opcode: Int) {
super.onMethodExit(opcode)
// 插入:long end = System.currentTimeMillis();
invokeStatic(Type.getType("Ljava/lang/System;"), Method("currentTimeMillis", "()J"))
val end = newLocal(Type.LONG_TYPE)
storeLocal(end)
// 插入:System.out.println(end - start);
getStatic(Type.getType("Ljava/lang/System;"), "out", Type.getType("Ljava/io/Printstream;"))
loadLocal(end)
loadLocal(start)
math(GeneratorAdapter.SUB, Type.LONG_TYPE)
invokeVirtual(Type.getType("Ljava/io/Printstream;"), Method("println", "(Ljava/lang/String;)V"))
}
}
通过这样子配置后,运行一下 main 方法,就会在路径下多出一个 TestMain2.class 文件,双击点开查看:
这样子通过 ASM 来对 class 文件进行修改和输出成一个新的 class 文件。例子中是对 class 文件中所有的方法都进行插桩了,若只需要特定方法的话,在 ClassVisitor 的回调方法中进行方法名的判断即可
ASM 还可以在构建 APK 过程中结合 Gradle 插件进行使用,从而实现自动化代码修改或增强。例如,在构建 .class 文件时自动插入一些想要的逻辑。