1.什么是编译插桩?
顾名思义,所谓的编译插桩就是在代码编译期间修改已有的代码或者生成新代码。
如图,这是Java代码的编译流程。从图中可以看出,编译插桩可以从两个方面着手
- Java 文件。类似 APT、AndroidAnnotation 这些代码生成的场景,它们生成的都是 Java 文件,是在编译的最开始介入。
- 字节码(Bytecode)。这是一般情况下讨论最多的操作字节码的方式。可以操作“.class”的 Java 字节码,也可以操作“.dex”的 Dalvik 字节码,这取决于我们使用的插桩方法。
相对于 Java 文件方式,字节码操作方式功能更加强大,应用场景也更广,但是它的使用复杂度更高
说到这里,有必要说一下Java字节码和Dalvik 字节码
java字节码可以参考这篇文章作为了解一文让你明白Java字节码,里面演示了如何将字节码反过来解析出对应的class文件。我自己也按照他的方法解析了一次class文件字节码解析
Dalvik 字节码可以看这篇dex文件字节码解析这篇文章开头还有对应的参考链接,本人按照这两个链接做的
总的来说,dex文件比class文件要复杂得多.一方面是小端排列,另一方面需要寻址.最重要的一点是,class文件的类索引里面所有的信息都是直接排进去的,但是dex文件里面的类都是存的索引,dex文件更为紧凑.也就是意味着,如果需要修改dex文件,那么他的成本会比修改class文件难得多
2.编译插桩的两种方法
(1)AspectJ
AspectJ是 Java 中流行的 AOP(aspect-oriented programming)编程扩展框架,它内部也是通过字节码处理技术实现的代码注入。本人也做了大略的了解,具体文章在这里简单聊聊AspectJ。
从使用上来看,AspectJ 的框架的确有自己的一些优势,比如成熟稳定,使用简单。但它也有劣势,比如切入点固定等。如果想要做更细致的修改,它就无能为力了。
具体的实现可以参看《AspectJ程序设计指南》这本书
(2)ASM
如果说 AspectJ 只能满足 50% 的字节码处理场景,那ASM就是一个可以实现 100% 场景的 Java 字节码操作框架,它的功能也非常强大。
使用 ASM 操作字节码主要的特点有:操作灵活,可以根据需求自定义修改、插入、删除。上手比较难,需要对 Java 字节码有比较深入的了解。
下面简单介绍一下ASM
ASM 库提供了两个用于生成和转换已编译类的 API,一个是核心 API,以基于事件的形式来表示类,另一个是树 API,以基于对象的形式来表示类。
- 核心 API
在采用基于事件的模型时,类是用一系列事件来表示的,每个事件表示类的一个元素,比如它的一个标头、一个字段、一个方法声明、一条指令,等等。基于事件的 API 定义了一组可能事件,以及这些事件必须遵循的发生顺序,还提供了一个类分析器,为每个被分析元素生成一个事件,还提供一个类写入器,由这些事件的序列生成经过编译的类。核心的类是ClassVisitor和ClassReader。
特点:使用者可以不用关心字节码的格式,只需要在每个 Visitor 的位置关心自己所修改的结构即可。但是这种模式的缺点是,一般只能在一些简单场景里实现字节码的处理。 - 树 API
而在采用基于对象的模型时,类用一个对象树表示,每个对象表示类的一部分,比如类本身、一个字段、一个方法、一条指令,等等,每个对象都有一些引用,指向表示其组成部分的对象。基于对象的 API 提供了一种方法,可以将表示一个类的事件序列转换为表示同一个类的对象树,也可以反过来,将对象树表示为等价的事件序列。换言之,基于对象的 API 构建在基于事件的 API 之上。核心是ClassNode,可以在此基础上增加或者删除属性,成员之类的,当然还有更高级的。缺点是:如果使用者对字节码不熟悉的话不好操作
3.掌握插桩应该具备的基础知识
- (1)熟练掌握字节码相关技术。之前提到过
- (2)groovy语言和Gradle自定义插件,可以直接参考官网
- (3)如果你想运用在Android项目中,那么还需要掌握Transform API
这是android在将class转成dex之前给我们预留的一个接口,在该接口中我们可以通过插件形式来修改class文件。共两步,第一步继承Transform接口,第二步register - (4)字节码修改工具。如AspectJ,ASM
4.插桩实践
字节码插桩--你也可以轻松掌握,Android字节码插桩——详细讲解 附带Demo
其实这两篇文章我只是大致理解了,并没有运行。第一篇是因为一直没要到demo。第二篇是因为gradle要5.1.1,据说AS必须要3.4.1.升级了我的其他的程序大概率受影响,就没决定升级.但是gradle也没办法降下去(会提示最低支持5.1.1),所以就放弃了。不过也大致理解了具体的流程
- (1)继承Transform实现字节码的修改(其中就有asm的应用,对ASM有所了解的话基本能看懂第一篇文章里面的modifyClass这个函数是干嘛的。文章中用的是对象API,其实用核心API应该也可以)
- (2)注册并且对外使用插件
5.总结
要想实现编译插桩,就必须要了解字节码,了解ASM,了解groovy。(本人对后面两个都不熟,只算基本了解。看来还有很长的路要走。这篇文章仅仅作为入门的了解)