前言
ASM作为一个声名在外的字节码编制工具,无数“传奇”框架都基于此展现了花里胡哨的魔法。
最近在工作中发现需要加强这部分能力,不然很多技术方案总是很麻烦...但是仅靠ASM实际也无法“无所欲为”,因为说到底它也只是一个方便的改写class的工具。想要使其发挥战斗力,还需要配合诸如:Gradle的transform api、注解等角色的支持。
因此接下来的一段时间内,我会尽可能的把自己在这方面的实战内容输出出来。
正文
这一篇咱们主要聊ASM的一些用法,核心聚焦于ASM。所以关于字节码的部分就不展开了,有相关兴趣的同学可以自行了解吧
说实话ASM整体使用起来很简单,主要有数个核心类:ClassReader
、ClassWrite
、ClassVisitor
...等各种Visitor系列类。
从类名的定义上,我们可以猜到ClassReader
用于读取class文件;ClassWrite
用于改写class文件。而ClassVisitor
、FieldVisitor
...则抽象类、方法、对象访问的流程。
第一步先把依赖加上,AMS现在已经出到很高的版本了,不过咱们还是随便用个版本,反正够用~
implementation 'org.ow2.asm:asm:6.0'
implementation 'org.ow2.asm:asm-util:6.0'
一、读Class
下边看一个简单的读class的demo代码:
fun main() {
val cp = ClassPrinter()
val cr = ClassReader("com.test.asm.AsmDemo")
cr.accept(cp, 0)
}
class AsmDemo {
private val hello = "Hello ASM"
fun testAsm() {
invokeMethod()
}
private fun invokeMethod() {
print(hello)
}
}
class ClassPrinter : ClassVisitor(ASM6) {
override fun visit(
version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array
) {
println("$name extends $superName {")
}
// 为了看起来简单,移除了一些不是特别重要的方法
override fun visitField(access: Int, name: String?, desc: String?, signature: String?, value: Any?): FieldVisitor? {
println(" visitField(name:$name desc:$desc signature:$signature)")
return null
}
override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?, exceptions: Array?): MethodVisitor? {
println(" visitMethod(name:$name desc:$desc signature:$signature)")
return null
}
override fun visitEnd() {
println("}")
}
}
这段代码run起来之后输出了如下的信息:
com/test/asm/AsmDemo extends java/lang/Object {
visitField(name:hello desc:Ljava/lang/String; signature:null)
visitMethod(name:testAsm desc:()V signature:null)
visitMethod(name:invokeMethod desc:()V signature:null)
visitMethod(name: desc:()V signature:null)
}
从demo中我们看到通过我们通过ClassReader("com.test.asm.AsmDemo")
,读取对应的Class。而ClassPrinter : ClassVisitor(ASM6)
通过对应的visit方法来了解对应类的细节。
二、写Class
对于写的过程相对要复杂一些,毕竟操作空间比较大。比如下边这个移除某方法的操作:
fun main() {
val cr = ClassReader("com.test.asm.AsmDemo")
val cw = ClassWriter(cr, 0)
val adapter = RemoveMethodAdapter(cw, "testAsm")
cr.accept(adapter, 0)
// 输出class
val outFile = File("...本地地址/com/test/asm/TestAsmDemo.class")
outFile.writeBytes(cw.toByteArray())
}
public class RemoveMethodAdapter extends ClassVisitor {
private String mName;
private String mDesc;
public RemoveMethodAdapter( ClassVisitor cv, String mName, String mDesc) {
super(ASM7, cv);
this.mName = mName;
this.mDesc = mDesc;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (name.equals(mName) && desc.equals(mDesc)) {
// 不要委托至下一个访问器 -> 这样将移除该方
return null;
}
return cv.visitMethod(access, name, desc, signature, exceptions);
}
}
打开输出的TestAsmDemo.class
:
我们可以发现testAsm()
方法已经在ClassWriter
这个实例中被移除了。
三、源码速读
整个流程的开始,就在于ClassReader的accept()方法:
public void accept(
final ClassVisitor classVisitor,
final Attribute[] attributePrototypes,
final int parsingOptions) {
// 省略大量代码
if ((parsingOptions & SKIP_DEBUG) == 0
&& (sourceFile != null || sourceDebugExtension != null)) {
classVisitor.visitSource(sourceFile, sourceDebugExtension);
}
}
上述简单截取了一段代码,这里本质就是读取class文件,然后按class的规范去解析,然后回调ClassVisitor对应的接口方法。
对于ClassVisitor的实现来说,可以是我们自己的实现类,这样我们就可以访问到ClassReader解析class的过程。
但是一般来说我们需要去改写class,此外核心的类便是ClassWriter。
public class ClassWriter extends ClassVisitor {
// 省略大量代码
@Override
public final MethodVisitor visitMethod(
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions) {
MethodWriter methodWriter =
new MethodWriter(symbolTable, access, name, descriptor, signature, exceptions, compute);
if (firstMethod == null) {
firstMethod = methodWriter;
} else {
lastMethod.mv = methodWriter;
}
return lastMethod = methodWriter;
}
}
这里我们单独截取了visitMethod()
方法,可以看到这里真正的实现是通过MethodWriter
实现的:
final class MethodWriter extends MethodVisitor {
// 省略大量代码
@Override
public void visitMethodInsn(
final int opcode,
final String owner,
final String name,
final String descriptor,
final boolean isInterface) {
lastBytecodeOffset = code.length;
Symbol methodrefSymbol = symbolTable.addConstantMethodref(owner, name, descriptor, isInterface);
if (opcode == Opcodes.INVOKEINTERFACE) {
code.put12(Opcodes.INVOKEINTERFACE, methodrefSymbol.index)
.put11(methodrefSymbol.getArgumentsAndReturnSizes() >> 2, 0);
} else {
code.put12(opcode, methodrefSymbol.index);
}
// 省略部分代码
}
}
可以看到,这里封装了写class的代码。走到这我们就能明白:我们之所以能够做到改写class的效果,本质是因为ClassWriter这个类的封装,而这个类是基于ClassVisitor的访问者模式来了解到ClassReader加载解析class的过程。
更多的咱们就不看了,大家有兴趣自己跟一波吧。毕竟以上的代码就足以让我们理解ASM整体的工作流程。
四、小总结
结合上述的内容,咱们来一个总结
首先ASM整体基于访问者模式(不了解这个模式也没关系,不影响理解)。
- ClassReader解析class文件,并回调对应ClassVisitor接口的方法
- ClassReader只负责分析class文件,然后回调给ClassVisitor,至此ClassReader也就结束了它的工作
- ClassWriter 是ClassVisitor接口的实现
- 这里封装了对class文件写的操作
- 具体代码在ClassWriter里边的各种Writer实现。
- ClassWriter也是一个ClassVisitor
- 它是咱们的第一层“代理”,我们的自定义ClassVisitor通过传入ClassWriter,来做到visitor流程的转发
- 这里封装了对class文件写的操作
因此,可以这么说:ClassReader + ClassVisitor 采用标准的访问者模式。目的在于:ASM框架基于我们一套接口,可以让我们访问到一个class文件的各种流程。
当我们需要改写class时候,我们则需要ClassWriter这个特别的ClassVisitor来进行能力的增强。
尾声
本篇内容很短,但也算是拉开“幕后”编改字节码的序幕。
后面的文章会一步步的走入真正的应用中去。相关知识涉及面众多,我会尽可能的用通俗的方式将这部分内容展现给大家看。
更新更多的文章,欢迎关注我们的公众号:咸鱼正翻身。