专栏:暴力突破 Android 编译插桩系列
一、前言
随着 Android 开发的发展,越来越多场景需要用到编译插桩了。日常开发中我们肯定也都接触过编译插桩,只是没有去深入挖掘它的实现原理,比如 ButterKnife、无痕埋点等,所以学习编译插桩不仅可以提升我们的竞争力,还可以让我们在开发中开拓视野、更好地解决问题。
我们经常使用 Gradle Plugin + 编译插桩的方式,因此在学习编译插桩前,建议先看一下我的 《Gradle 专栏》学习
现在让我们一起学习一些编译插桩的基础来入门。
二、编译插桩基础知识
顾名思义,所谓的编译插桩就是在代码编译期间修改已有的代码或者生成新代码。它主要有以下几种应用场景:
- 代码生成。除了 Dagger、ButterKnife 这些常用的注解生成框架,Protocol Buffers、数据库 ORM 框架也都会在编译过程生成代码。代码生成隔离了复杂的内部实现,让开发更加简单高效,而且也减少了手工重复的劳动量,降低了出错的可能性。
- 代码监控。除了网络监控和耗电监控,我们可以利用编译插桩技术实现各种各样的性能监控。为什么不直接在源码中实现监控功能呢?首先我们不一定有第三方 SDK 的源码,其次某些调用点可能会非常分散,例如想监控代码中所有 new Thread() 调用,通过源码的方式并不那么容易实现。
- 代码修改。我们在这个场景拥有无限的发挥空间,例如某些第三方 SDK 库没有源码,我们可以给它内部的一个崩溃函数增加 try catch,或者说替换它的图片库等。我们也可以通过代码修改实现无痕埋点,就像网易的HubbleData、51 信用卡的埋点实践。
- 代码分析。例如检查代码中的 new Thread() 调用、检查代码中的一些敏感权限使用等。事实上,Findbugs 这些第三方的代码检查工具也同样使用的是编译插桩技术实现。
编译插桩技术非常有趣,同样也很有价值,掌握它之后,可以完成一些其他技术很难实现或无法完成的任务。学会这项技术以后,我们就可以随心所欲地操控代码,满足不同场景的需求。
我们先来看看 java 的编译流程:
编译插桩是从代码编译的哪个流程介入的呢?我们可以把它分为两类:
- Java 文件。类似 APT、AndroidAnnotation 这些代码生成的场景,它们生成的都是 Java 文件,是在编译的最开始介入。
- 字节码。对于代码监控、代码修改以及代码分析这三个场景,一般采用操作字节码的方式。可以操作“.class”的 Java 字节码,也可以操作“.dex”的 Dalvik 字节码,这取决于我们使用的插桩方法。常用的字节码插桩方法有 AspectJ、ASM。
根据下图可以看出它们介入的流程:
需要注意的是,AspectJ 是 Java 中流行的 AOP(aspect-oriented programming)编程扩展框架,网上很多文章说它处理的是 Java 文件,其实并不正确,它内部也是通过字节码处理技术实现的代码注入。
相对于 Java 文件方式,字节码操作方式功能更加强大,应用场景也更广,但是它的使用复杂度更高。
三、APK 编译打包流程
我们来看一下详细的编译打包流程图:
从上面的流程图,我们可以看出apk打包流程可以分为以下七步
- 通过 AAPT 打包 res 资源文件,生成 R.java、resources.arsc 和 res 文件(二进制 & 非二进制如 res/raw 和 pic 保持原样)
- 通过 aidl 工具处理 .aidl 文件,生成对应的 Java 接口文件
- 通过 Java Compiler 编译 R.java、Java 接口文件、Java 源文件,生成 .class 文件
- 通过 dex 命令,将 .class 文件和第三方库中的 .class 文件处理生成 classes.dex
- 通过 apkbuilder 工具,将 aapt 生成的 resources.arsc 和 res 文件、assets 文件和 classes.dex 一起打包生成 apk
- 通过 Jarsigner 工具,对上面的 apk 进行 debug 或 release 签名
- 通过 zipalign 工具,将签名后的 apk 进行对齐处理。