安卓 编译插桩:AspectJ、ASM、ReDex

发布于 2024-11-12 10:08:52 字数 4381 浏览 2 评论 0

应用场景

  1. 代码生成:Butterknife、各种 APT 插件、热更新
  2. 代码监控:方便监控第三方 SDK 代码
  3. 代码修改:无痕埋点
  4. 代码分析:持续集成,里面的自定义代码检查就可以使用编译插桩技术实现。例如检查代码中的 new Thread() 调用、检查代码中的一些敏感权限使用等。比如:Findbugs

编译插桩的介入时机

  1. Java 文件。类似 APT、AndroidAnnotation 这些代码生成的场景,它们生成的都是 Java 文件,是在编译的最开始介入。
  2. 字节码。对于代码监控、代码修改以及代码分析这三个场景,一般采用操作字节码的方式。可以操作“.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 的业务,所以我们需要提取有用的代码加以使用。

参考

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

0 文章
0 评论
23 人气
更多

推荐作者

玍銹的英雄夢

文章 0 评论 0

我不会写诗

文章 0 评论 0

十六岁半

文章 0 评论 0

浸婚纱

文章 0 评论 0

qq_kJ6XkX

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文