在上篇文章中,咱们以AspectJ为引子介绍了AOP及其设计思想,传送门Android AspectJ详解,咱们用AspectJ能够方便的实现一些简单的代码织入,而不须要关心底层字节码的实现,而ASM则偏向底层一些,ASM提供的API彻底是面向Java字节码编程,若是你对Java字节码的结构和原理不甚了解,很难直接上手。html
但正是由于ASM的原理是直接操做字节码,那么理论上对字节码的任意修改,均可以用ASM实现。由于不管是哪一种AOP技术,最终跑在JVM上的都是class字节码。java
而AspectJ所处的位置更偏向应用层,它将操做字节码这件事封装到内部,给外部提供的就是一些筛选切面的注解,并在这个切面下编写java代码,最终是经过AspectJ的ajc编译器实现代码的织入。android
文中的ASM项目示例戳这里。git
ASM是一个字节码操做框架,可用来动态生成字节码或者对现有的类进行加强。ASM能够直接生成二进制的class字节码,也能够在class被加载进虚拟机前动态改变其行为,好比方法执行先后插入代码,添加成员变量,修改父类,添加接口等等。github
ASM官方网站编程
ASM经过访问者模式依次遍历class字节码中的各个部分,并不断的经过回调的方式通知上层(这有点像SAX解析xml的过程),上层可在业务关心的某个访问点,修改原有逻辑。segmentfault
之因此能够这么作,是由于java字节码是按照严格的JVM规范生成二进制字节流,ASM只是按照这个规范对java字节码的一次解释,将晦涩难懂的字节码背后对应的JVM指令一条条的转换成ASM API。数组
好比,一句简单的日志打印bash
Log.d("tag", " onCreate");
复制代码
转换成ASM API将会是下面这样:app
mv.visitLdcInsn("tag");
mv.visitLdcInsn("onCreate");
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(POP);
复制代码
若是你稍懂JVM汇编指令的话,能够看出大体意思。
而后咱们经过javap指令查看一下这行代码对应的JVM汇编指令,以下图:
这样是否是就很清楚了?就是这四条指令,ASM作的就是按照JVM的规范,生成代码对应的JVM指令并写入字节码文件。
上面的例子,用到了javap指令,所以咱们首先须要对java字节码结构作一个大体的介绍,这样整个ASM流程最底层的原理就算清楚了。
咱们经过javac指令将一个java源文件编译成.class的字节码文件,这个文件直接经过文本编辑器打开将会看到全是16进制的字节码。
class Demo {
int i = 0;
public void test() {
i += 1;
}
}
复制代码
class字节码结构组成结构如图。
各个部分占用字节大小:
其中u一、u二、u四、u8分别表明1个字节、2个字节、4个字节、8个字节的无符号数。无符号数用于描述数字、索引引用、数量值、字符串值。
cp_info、field_info这些以info结尾的是表,一个表由一个或多个元素组成,这里元素能够是常量、字段、方法等等。
好比按咱们Demo.class字节码的信息,cafe babe是魔数,按表顺序后面跟的四个字节0000 0034是分别是次版本和主版本,转换成10进制是52.0,查看java虚拟机版本映射关系表,52表示JDK 1.8,也就是该类是用JDK 1.8进行编译的。
以后的两个字节0012表示常量池大小,为十进制的18,因为常量池常量下标从1开始,也就是有17个常量。
0a00后面的内容就是第一个具体的常量信息。
常量分为两类字面量和符号引用
0a对应十进制的10,10表示MethodRef,即方法引用。
字节码结构的后续内容较多,并非本文重点,再也不展开,除此以外还须要掌握JVM常见的指令,好比aload、invokespecial、ldc等等,感兴趣的小伙伴可参考认识 .class 文件的字节码结构,补充学习。
但在目前,即便咱们不懂这些也不妨碍咱们开发,由于ASM提供了相应工具帮助咱们编写ASM API代码,莫慌~~
字节码严格遵照着JVM规范,直接读字节码文件是疯狂的事情,咱们可经过javap指令能够将字节码反编译成易懂的汇编指令。
javap -v Demo.class
-v 表示verbose,将会打印 行号+本地变量表信息+反编译汇编代码+常量池等所有信息。
在Android Studio中,可经过jclasslib插件查看更清晰。
ASM经过访问者模式,将类文件的内容从头至尾扫描一遍,每次扫描到相应内容时,会回调ClassVisitor内部相应的方法。
常见的visitor以下表。
类型 | visitor |
---|---|
Class | ClassVisitor |
Field | FieldVisitor |
Method | MethodVisitor |
Annotation | AnnotationVisitor |
ClassVisitor的调用顺序为:
visit
visitSource?
visitOuterClass?
( visitAnnotation | visitAttribute )*
( visitInnerClass | visitField | visitMethod )*
visitEnd
复制代码
MethodVisitor的调用顺序为:
visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )*
( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |
visitLocalVariable | visitLineNumber )*
visitMaxs )?
visitEnd
复制代码
完整的访问顺序咱们能够经过时序图了解:
ClassReader能够方便地让咱们对class文件进行读取与解析,解析到某一个结构就会通知到ClassVisitor的相应方法,好比解析到类方法时,就会回调ClassVisitor.visitMethod方法。
咱们能够经过更改ClassVisitor中相应结构方法返回值,实现对类的代码切入,好比更改ClassVisitor.visitMethod()方法的返回值MethodVisitor实例。
经过ClassWriter的toByteArray()方法,获得class文件的字节码内容,最后经过文件流写入方式覆盖掉原先的内容,实现class文件的改写。
咱们举个例子,咱们想为FragmentActivity这个类的onCreate方法中添加一段日志打印,能够按下面的步骤。
//建立ClassReader,传入class字节码的输入流
ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
//建立ClassWriter,绑定classReader
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
//建立自定义的LifecycleClassVisitor,并绑定classWriter
ClassVisitor cv = new LifecycleClassVisitor(classWriter)
//接受一个实现了 ClassVisitor接口的对象实例做为参数,而后依次调用 ClassVisitor接口的各个方法
classReader.accept(cv, EXPAND_FRAMES)
//toByteArray方法会将最终修改的字节码以 byte 数组形式返回。
byte[] code = classWriter.toByteArray()
复制代码
最终code就是修改后的字节码数组。
咱们能够将它写入文件输出到本地。
File file = new File("Test.class");
FileOutputStream fos = new FileOutputStream(file);
fos.write(classFile);
fos.close();
复制代码
在Android体系下咱们经过Gradle Transform工具,在java代码编译成.class文件以后,.class优化为.dex文件前将代码织入。 使用Transform须要开发一个自定义的gradle plugin,plugin的开发不是本文的核心,咱们暂且跳过。
咱们只须要知道在一次transform过程当中,Gradle会将本地工程中编译的代码、jar包 / aar包 / 依赖的三方库中的代码,做为输入源交由咱们的插件处理,这也就是说ASM一样能够对工程外部的类进行修改或织入。
若是咱们须要在指定的类,指定的方法中织入代码,须要编写相应的过滤条件,这也是相比于AspectJ而言不太方便的地方,AspectJ可经过声明切面注解完成精准的织入。
下面举个例子,假设咱们想在FragmentActivity的onCreate方法执行前打印一行日志,能够这么作。
建立LifecycleClassVisitor类继承于ClassVisitor,复写visitMethod方法。
public class LifecycleClassVisitor extends ClassVisitor implements Opcodes {
...
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
//匹配FragmentActivity
if ("android/support/v4/app/FragmentActivity".equals(this.mClassName)) {
if ("onCreate".equals(name) ) {
//处理onCreate
return new LifecycleOnCreateMethodVisitor(mv);
}
}
return mv;
}
}
复制代码
访问到onCreate这个方法时,咱们须要继续自定义一个MethodVisitor,告诉ASM你想如何处理这个方法。
根据上述的访问时序图咱们知道,在方法访问开始时会回调MethodVisitor的visitCode方法,所以咱们复写此方法后将会在onCreate方法开头织入代码。
public class LifecycleOnCreateMethodVisitor extends MethodVisitor {
...
@Override
public void visitCode() {
super.visitCode();
//方法执行前插入
mv.visitLdcInsn("tag");
mv.visitLdcInsn("onCreate start");
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(POP);
}
@Override
public void visitInsn(int opcode) {
//方法执行后插入
if (opcode == Opcodes.RETURN) {
mv.visitLdcInsn("tag");
mv.visitLdcInsn("onCreate end");
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(POP);
}
super.visitInsn(opcode);
}
@Override
public void visitEnd() {
super.visitEnd();
//warn 若想在方法最后织入代码,写在这里是无效的
}
}
复制代码
这里值得注意的是若想在方法最后织入代码,写在visitEnd方法内是无效的,回调它的时候类已经访问结束了。 咱们只能迂回解决,咱们知道方法执行结束前都会有一个return指令,若是你的方法返回值为void,那编译成字节码时会默认补上一个return指令。 return指令根据返回对象的类型不一样,会有不一样的指令,好比:
因为咱们知道onCreate方法的返回值就是空,因此咱们只须要捕获这个return指令就能够了。 这里的指令范围很是广,好比加减乘除、条件判断、aload等等,这些指令常量被封装到Opcodes类中。
访问者模式为指令提供的回调就是visitInsn方法,所以就有了上面visitInsn方法的代码。
因为在方法先后插入代码这种需求很常见,而上述模板代码写起来又太难看,所以ASM还提供了一个AdviceAdapter类,对一些常见的切面作了二次封装。
若是咱们用AdviceAdapter编写上述代码会变得更直观清爽。
public class OnCreateMethodAdapter extends AdviceAdapter {
...
@Override
protected void onMethodEnter() {
super.onMethodEnter();
//方法开头织入代码
mv.visitLdcInsn("tag");
mv.visitLdcInsn("onCreate start");
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(POP);
}
@Override
protected void onMethodExit(int opcode) {
//方法末尾织入代码
}
}
复制代码
ok,到这里咱们以在某个方法先后织入一段代码的例子讲完了,ASM能实现关于字节码的任何修改,其中涉及的API能够十分复杂,对于好比修改类名、添加方法等,最好经过查阅ASM官方文档完成开发。
考虑到直接使用ASM API编写JVM指令比较困难,所以官方提供了一个插件帮助咱们完成API的编写。
咱们只须要先在任意位置编写须要织入的java代码,而后即可经过这个插件生成对应的ASM代码,爱了爱了...
虽然ASM很强大,但若是你使用了AspectJ以后再开看ASM,就会发现有一些新的问题。
不过,ASM优势更加明显:
AOP相关参考旧文:谈谈Android AOP技术方案。