引言
在我们实际的开发过程中,很多场景需要AOP的编程思想,在开发者无感知地侵入式的插如自己的业务逻辑,比如我最近做的一个埋点统计的一些场景,在开发者无感知情况下,将生命周期上报执行逻辑代码植入到我们现有的APP的某些页面的Class里面,将用户事件的逻辑代码植入到对应的事件响应方法里面。这里我们就引出了字节码操作框架ASM,通过ASM修改编译过程生成的java字节码,植入埋点上报的业务逻辑方法,从而不需要开发者在每个页面生命周期或者基类生命周期,事件响应方法里面做大量的埋点代码重复工作。
介绍
ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
ASM效果展示
这是一个简单的埋点工程,在工程里面的页面MainActivity里面添加事件
通过ASM插件的加入,最终编译出来的Class字节码文件
但是在编译出来的class字节码文件里面多出了一行代码,这就是植入的一行埋点统计方法
...
this.findViewById(2131427418).setOnClickListener(new OnClickListener() {
public void onClick(View var1) {
//多出来的代码,即工具植入的埋点方法代码
Monitor.INSTANCE.onViewClick(var1);
Log.d(MainActivity.TAG, "onclick btn_test1 ");
}
});
...
看到这,想必大家就见识到ASM黑科技的威力了,通过ASM可以对java字节码进行各种修改,达到我们所需要的目的。(ps:很多流氓sdk的广告植入大部分都是采用类似的方案)
ASM初步使用
- 1.工程引入
dependencies {
...
compile 'org.ow2.asm:asm:5.2'
compile 'org.ow2.asm:asm-commons:5.2'
}
- 2.调用ASM的类读写器
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new ClickEventVisitor(classWriter)
classReader.accept(cv, ClassReader.EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(file.absolutePath)
fos.write(code)
fos.close()
- 3.实现ClassVisitor植入埋点代码
class ClickEventVisitor extends ClassVisitor {
ClickEventVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv)
}
@Override
MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
def methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions)
methodVisitor = new AdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, desc) {
boolean isInject() {
return name == "onClick"
}
@Override
protected void onMethodEnter() {
super.onMethodEnter()
//判断是否需要植入代码
if (!isInject()) {
return
}
//这里就是我们植入的一段埋点代码 即调用Monitor.INSTANCE.onViewClick方法
mv.visitFieldInsn(GETSTATIC, "com/zgkxzx/malfurion/Monitor", "INSTANCE", "Lcom/zgkxzx/malfurion/Monitor;")
mv.visitVarInsn(ALOAD, 1)
mv.visitMethodInsn(INVOKEVIRTUAL, "com/zgkxzx/malfurion/Monitor", "onViewClick",
"(Landroid/view/View;)V", false)
}
}
return methodVisitor
}
}
在 ASM 中,提供了一个 ClassReader类,这个类可以直接由字节数组或由 class 文件间接的获得字节码数据,它能正确的分析字节码,构建出抽象的树在内存中表示字节码。它会调用 accept方法,这个方法接受一个实现了 ClassVisitor接口的对象实例作为参数,然后依次调用 ClassVisitor接口的各个方法。字节码空间上的偏移被转换成 visit 事件时间上调用的先后,所谓 visit 事件是指对各种不同 visit 函数的调用,ClassReader知道如何调用各种 visit 函数。在这个过程中用户无法对操作进行干涉,所以遍历的算法是确定的,用户可以做的是提供不同的 Visitor 来对字节码树进行不同的修改。ClassVisitor会产生一些子过程,比如 visitMethod会返回一个实现 MethordVisitor接口的实例,visitField会返回一个实现 FieldVisitor接口的实例,完成子过程后控制返回到父过程,继续访问下一节点。因此对于 ClassReader来说,其内部顺序访问是有一定要求的。实际上用户还可以不通过 ClassReader类,自行手工控制这个流程,只要按照一定的顺序,各个 visit 事件被先后正确的调用,最后就能生成可以被正确加载的字节码。当然获得更大灵活性的同时也加大了调整字节码的复杂度。
初步使用小节只是ASM的初步引入基本使用,后续章节会深入字节码,对ASM的几个重要的接口
1.ClassVisitor 类访问器
2.FieldVisitor fang 变量访问器
3.MethodVisitor 方法访问器
4.AnnomatioinVisitor 注解访问器
进行深入讲解和使用
ASM植入过程分析
正常的java IDE在对java文件编译后运行的流程(ps:这里的图用的经典的HelloWorld做编译分析,更容易理解)
在使用我们自己的插件后,整个IDE工程的编译流程
可以看出,在javac将程序员编写的java文件编译成class字节码文件后,myPlugin插件工具对生成的class文件进行植入式修改变成新的class文件,最后通过java指令执行class运行起来(Android通过ART虚拟机执行Dex文件,dex也是有这些class打包生成的),从而达到植入的目的。这里我们对比下刚才工程的植入的代码的字节码文件二进制对比
可以看出,文件的二进制基本上一样,就是对文件下面的二进制进行了植入。我们在对比下字节码文件
对比可以很容易看出,注入的字节码文件对常量池进行添加,对方法的code属性进行了添加调用埋点统计的方法,从而无感知的实现了埋点的功能。
总结
ASM字节码操纵框架的出现为我们开发人员编写各种框架各种SDK工具实现更多的可能,本章只是简单的介绍和引入(内容比较多一章写不完)。后面会分小章进行ASM重要类方法的精讲。谢谢-