安卓 编译插桩:AspectJ、ASM、ReDex
应用场景
- 代码生成:Butterknife、各种 APT 插件、热更新
- 代码监控:方便监控第三方 SDK 代码
- 代码修改:无痕埋点
- 代码分析:持续集成,里面的自定义代码检查就可以使用编译插桩技术实现。例如检查代码中的 new Thread() 调用、检查代码中的一些敏感权限使用等。比如:Findbugs
编译插桩的介入时机
- Java 文件。类似 APT、AndroidAnnotation 这些代码生成的场景,它们生成的都是 Java 文件,是在编译的最开始介入。
- 字节码。对于代码监控、代码修改以及代码分析这三个场景,一般采用操作字节码的方式。可以操作“.class”的 Java 字节码,也可以操作“.dex”的 Dalvik 字节码,这取决于我们使用的插桩方法。
Java 字节码和 Dalvik 字节码主要区别
- 体系结构。Java 虚拟机是基于栈实现,而 Android 虚拟机是基于寄存器实现。在 ARM 平台,寄存器实现性能会高于栈实现。
- 格式结构。对于 Class 文件,每个文件都会有自己单独的常量池以及其他一些公共字段。对于 Dex 文件,整个 Dex 中的所有 Class 共用同一个常量池和公共字段,所以整体结构更加紧凑,因此也大大减少了体积。
- 指令优化。Dalvik 字节码对大量的指令专门做了精简和优化,如下图所示,相同的代码 Java 字节码需要 100 多条,而 Dalvik 字节码只需要几条。
编译插桩实现方式
AspectJ
AspectJ Java 中流行的 AOP(aspect-oriented programming)
编程扩展框架,网上很多文章说它处理的是 Java 文件,其实并不正确,它内部也是通过字节码处理技术实现的代码注入。从底层实现上来看,AspectJ 内部使用的是 BCEL 框架来完成的,不过这个库在最近几年没有更多的开发进展,官方也建议切换到 ObjectWeb 的 ASM 框架。
优势:
成熟稳定 :从字节码的格式和各种指令规则来看,字节码处理不是那么简单,如果处理出错,就会导致程序编译或者运行过程出问题。而 AspectJ 作为从 2001 年发展至今的框架,它已经很成熟,一般不用考虑插入的字节码正确性的问题。
使用简单 :AspectJ 功能强大而且使用非常简单,使用者完全不需要理解任何 Java 字节码相关的知识,就可以使用自如。它可以在方法(包括构造方法)被调用的位置、在方法体(包括构造方法)的内部、在读写变量的位置、在静态代码块内部、在异常处理的位置等前后,插入自定义的代码,或者直接将原位置的代码替换为自定义的代码。
劣势
切入点固定 :AspectJ 只能在一些固定的切入点来进行操作,如果想要进行更细致的操作则无法完成,它不能针对一些特定规则的字节码序列做操作。
正则表达式 :AspectJ 的匹配规则是类似正则表达式的规则,比如匹配 Activity 生命周期的 onXXX 方法,如果有自定义的其他以 on 开头的方法也会匹配到。
性能低 :AspectJ 在实现时会包装自己的一些类,逻辑比较复杂,不仅生成的字节码比较大,而且对原函数的性能也会有所影响。它并不是把我们的代码直接插桩,而是经过一系列自己的封装。如果想针对所有的函数都做插桩,AspectJ 会带来不少的性能影响。
不过大部分情况,我们可能只会插桩某一小部分函数,这样 AspectJ 带来的性能影响就可以忽略不计了。如果想在 Android 中直接使用 AspectJ。这里推荐你直接使用沪江的 AspectJX 框架,它不仅使用更加简便一些,而且还扩展了排除某些类和 JAR 包的能力。如果你想通过 Annotation 注解方式接入,推荐使用 Jake Wharton 大神写的 Hugo
ASM
如果说 AspectJ 只能满足 50% 的字节码处理场景,那 ASM 就是一个可以实现 100% 场景的 Java 字节码操作框架,它的功能也非常强大。使用 ASM 操作字节码主要的特点有:
- 操作灵活。操作起来很灵活,可以根据需求自定义修改、插入、删除。
- 上手比较难,需要对 Java 字节码有比较深入的了解。
为了使用简单,相比于 BCEL 框架,ASM 的优势是提供了一个 Visitor 模式的访问接口(Core API),使用者可以不用关心字节码的格式,只需要在每个 Visitor 的位置关心自己所修改的结构即可。但是这种模式的缺点是,一般只能在一些简单场景里实现字节码的处理。
还需要 JVM 虚拟机栈的背景知识
比如 线程栈(每一条 Java 虚拟机线程都有自己私有的 Java 虚拟机栈,这个栈与线程同时创建,用于存储栈帧(Stack Frame))。它里面的栈帧:本地变量表、操作数栈、常量池引用。
在具体的字节码处理过程中,特别需要注意的是本地变量表和操作数栈的数据交换和 try catch blcok 的处理。如果想在一个方法执行完成后增加代码,ASM 相对也要简单很多,可以在字节码中出现的每一条 RETURN 系或者 ATHROW 的指令前,增加处理的逻辑即可。
Redex
ReDex 不仅只是作为一款 Dex 优化工具,它也提供了很多的小工具和文档里没有提到的一些新奇功能。比如在 ReDex 里提供了一个简单的 Method Tracing 和 Block Tracing 工具,这个工具可以在所有方法或者指定方法前面插入一段跟踪代码。
ReDex 的这个功能并不是完整的 AOP 工具,但它提供了一系列指令生成 API 和 Opcode 插入 API,我们可以参照这个功能实现自己的字节码注入工具,这个功能的代码在 Instrument.cpp
这个类已经将各种字节码特殊情况处理得相对比较完善,我们可以直接构造一段 Opcode 调用其提供的 Insert 接口即可完成代码的插入,而不用过多考虑可能会出现的异常情况。不过这个类提供的功能依然耦合了 ReDex 的业务,所以我们需要提取有用的代码加以使用。
参考
- ASMDEX ,开发者是 ASM 库的开发者,但很久未更新了。
- Dexter ,Google 官方开发的 Dex 操作库,更新很频繁,但使用起来很复杂。
- Dexmaker ,用来生成 Dalvik 字节码的代码。
- Soot ,修改 Dex 的方法很另类,是先将 Dalvik 字节码转成一种 Jimple three-address code,然后插入 Jimple Opcode 后再转回 Dalvik 字节码。
- 一起玩转 Android 项目中的字节码
- 字节码操纵技术探秘
- 基于 ASM 的字节码处理工具: Hunter 、 Hibeaver
- 基于 Javassist 的字节码处理工具: DroidAssist
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 安卓 编译过程分析
下一篇: MyBatis 介绍和使用
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论