作为 Android 开发, 日常写 Java 代码之余, 是否想过, 玩玩 class 文件? 直接对 class 文件的字节码下手, 我们可以做很多好玩的事情, 比如:
对全局所有 class 插桩, 做 UI, 内存, 网络等等方面的性能监控
发现某个第三方依赖, 用起来不爽, 但是不想拿它的源码修改再重新编译, 而想对它的 class 直接做点手脚
每次写打 log 时, 想让 TAG 自动生成, 让它默认就是当前类的名称, 甚至你想让 log 里自动加上当前代码所在的行数, 更方便定位日志位置
Java 自带的动态代理太弱了, 只能对接口类做动态代理, 而我们想对任何类做动态代理
为了实现上面这些想法, 可能我们最开始的第一反应, 都是能否通过代码生成技术, APT, 抑或反射, 抑或动态代理来实现, 但是想来想去, 貌似这些方案都不能很好满足上面的需求, 而且, 有些问题不能从 Java 文件入手, 而应该从 class 文件寻找突破. 而从 class 文件入手, 我们就不得不来近距离接触一下字节码!
JVM 平台上, 修改, 生成字节码无处不在, 从 ORM 框架 (如 Hibernate, MyBatis) 到 Mock 框架(如 Mockio), 再到 Java Web 中的常青树 Spring 框架, 再到新兴的 JVM 语言 Kotlin 的编译器, 还有大名鼎鼎的 https://github.com/cglib/cglib 项目, 都有字节码的身影.
字节码相关技术的强大之处自然不用多说, 而且在 Android 开发中, 无论是使用 Java 开发和 Kotlin 开发, 都是 JVM 平台的语言, 所以如果我们在 Android 开发中, 使用字节码技术做一下 hack, 还可以天然地兼容 Java 和 Kotlin 语言.
今天写这篇文章, 分享自己摸索相关技术过程中的一些鸡肋.
这个项目主要使用的技术是 Android gradle 插件, Transform,ASM 与字节码基础. 这篇文章将主要围绕以下几个技术点展开:
Transform 的应用, 原理, 优化
ASM 的应用, 开发流, 以及与 Android 工程的适配
几个具体应用案例
所以阅读这篇文章, 读者最好有 Android 开发以及编写简单 Gradle 插件的背景知识.
话不多说, 让我们开始吧.
一, Transform
引入 Transform
Transform 是 Android gradle plugin 1.5 开始引入的概念.
我们先从如何引入 Transform 依赖说起, 首先我们需要编写一个自定义插件, 然后在插件中注册一个自定义 Transform. 这其中我们需要先通过 gradle 引入 Transform 的依赖, 这里有一个坑, Transform 的库最开始是独立的, 后来从 2.0.0 版本开始, 被归入了 Android 编译系统依赖的 gradle-API 中, 让我们看看 Transform 在 jcenter 上的历个版本.
所以, 很久很久以前我引入 transform 依赖是这样
compile 'com.android.tools.build:transform-api:1.5.0'
现在是这样
- // 从 2.0.0 版本开始就是在 gradle-API 中了
- implementation 'com.android.tools.build:gradle-api:3.1.4'
然后, 让我们在自定义插件中注册一个自定义 Transform,gradle 插件可以使用 java,groovy,kotlin 编写, 我这里选择使用 java.
- public class CustomPlugin implements Plugin<Project> {
- @SuppressWarnings("NullableProblems")
- @Override
- public void apply(Project project) {
- AppExtension appExtension = (AppExtension)project.getProperties().get("android");
- appExtension.registerTransform(new CustomTransform(), Collections.EMPTY_LIST);
- }
- }
那么如何写一个自定义 Transform 呢?
Transform 的原理与应用
介绍如何应用 Transform 之前, 我们先介绍 Transform 的原理, 一图胜千言
每个 Transform 其实都是一个 gradle task,Android 编译器中的 TaskManager 将每个 Transform 串连起来, 第一个 Transform 接收来自 javac 编译的结果, 以及已经拉取到在本地的第三方依赖(jar. aar), 还有 resource 资源, 注意, 这里的 resource 并非 Android 项目中的 res 资源, 而是 asset 目录下的资源. 这些编译的中间产物, 在 Transform 组成的链条上流动, 每个 Transform 节点可以对 class 进行处理再传递给下一个 Transform. 我们常见的混淆, Desugar 等逻辑, 它们的实现如今都是封装在一个个 Transform 中, 而我们自定义的 Transform, 会插入到这个 Transform 链条的最前面.
但其实, 上面这幅图, 只是展示 Transform 的其中一种情况. 而 Transform 其实可以有两种输入, 一种是消费型的, 当前 Transform 需要将消费型型输出给下一个 Transform, 另一种是引用型的, 当前 Transform 可以读取这些输入, 而不需要输出给下一个 Transform, 比如 Instant Run 就是通过这种方式, 检查两次编译之间的 diff 的. 至于怎么在一个 Transform 中声明两种输入, 以及怎么处理两种输入, 后面将有示例代码.
为了印证 Transform 的工作原理和应用方式, 我们也可以从 Android gradle plugin 源码入手找出证据, 在 TaskManager 中, 有一个方法 createPostCompilationTasks. 为了避免贴篇幅太长的源码, 这里附上链接
TaskManager#createPostCompilationTasks
这个方法的脉络很清晰, 我们可以看到, Jacoco,Desugar,MergeJavaRes,AdvancedProfiling,Shrinker,Proguard, JarMergeTransform, MultiDex, Dex 都是通过 Transform 的形式一个个串联起来. 其中也有将我们自定义的 Transform 插进去.
讲完了 Transform 的数据流动的原理, 我们再来介绍一下 Transform 的输入数据的过滤机制, Transform 的数据输入, 可以通过 Scope 和 ContentType 两个维度进行过滤.
ContentType, 顾名思义, 就是数据类型, 在插件开发中, 我们一般只能使用 CLASSES 和 RESOURCES 两种类型, 注意, 其中的 CLASSES 已经包含了 class 文件和 jar 文件
从图中可以看到, 除了 CLASSES 和 RESOURCES, 还有一些我们开发过程无法使用的类型, 比如 DEX 文件, 这些隐藏类型在一个独立的枚举类中, 这些类型只能给 Android 编译器使用. 另外, 我们一般使用 TransformManager 中提供的几个常用的 ContentType 集合和 Scope 集合, 如果是要处理所有 class 和 jar 的字节码, ContentType 我们一般使用 TransformManager.CONTENT_CLASS.
Scope 相比 ContentType 则是另一个维度的过滤规则,
我们可以发现, 左边几个类型可供我们使用, 而我们一般都是组合使用这几个类型, TransformManager 有几个常用的 Scope 集合方便开发者使用.
如果是要处理所有 class 字节码, Scope 我们一般使用 TransformManager.SCOPE_FULL_PROJECT.
好, 目前为止, 我们介绍了 Transform 的数据流动的原理, 输入的类型和过滤机制, 我们再写一个简单的自定义 Transform, 让我们对 Transform 可以有一个更具体的认识
- public class CustomTransform extends Transform {
- public static final String TAG = "CustomTransform";
- public CustomTransform() {
- super();
- }
- @Override
- public String getName() {
- return "CustomTransform";
- }
- @Override
- public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
- super.transform(transformInvocation);
- // 当前是否是增量编译
- boolean isIncremental = transformInvocation.isIncremental();
- // 消费型输入, 可以从中获取 jar 包和 class 文件夹路径. 需要输出给下一个任务
- Collection<TransformInput> inputs = transformInvocation.getInputs();
- // 引用型输入, 无需输出.
- Collection<TransformInput> referencedInputs = transformInvocation.getReferencedInputs();
- //OutputProvider 管理输出路径, 如果消费型输入为空, 你会发现 OutputProvider == null
- TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
- for(TransformInput input : inputs) {
- for(JarInput jarInput : input.getJarInputs()) {
- File dest = outputProvider.getContentLocation(
- jarInput.getFile().getAbsolutePath(),
- jarInput.getContentTypes(),
- jarInput.getScopes(),
- Format.JAR);
- // 将修改过的字节码 copy 到 dest, 就可以实现编译期间干预字节码的目的了
- FileUtils.copyFile(jarInput.getFile(), dest);
- }
- for(DirectoryInput directoryInput : input.getDirectoryInputs()) {
- File dest = outputProvider.getContentLocation(directoryInput.getName(),
- directoryInput.getContentTypes(), directoryInput.getScopes(),
- Format.DIRECTORY);
- // 将修改过的字节码 copy 到 dest, 就可以实现编译期间干预字节码的目的了
- FileUtils.copyDirectory(directoryInput.getFile(), dest);
- }
- }
- }
- @Override
- public Set<QualifiedContent.ContentType> getInputTypes() {
- return TransformManager.CONTENT_CLASS;
- }
- @Override
- public Set<? super QualifiedContent.Scope> getScopes() {
- return TransformManager.SCOPE_FULL_PROJECT;
- }
- @Override
- public Set<QualifiedContent.ContentType> getOutputTypes() {
- return super.getOutputTypes();
- }
- @Override
- public Set<? super QualifiedContent.Scope> getReferencedScopes() {
- return TransformManager.EMPTY_SCOPES;
- }
- @Override
- public Map<String, Object> getParameterInputs() {
- return super.getParameterInputs();
- }
- @Override
- public boolean isCacheable() {
- return true;
- }
- @Override
- public boolean isIncremental() {
- return true; // 是否开启增量编译
- }
- }
可以看到, 在 transform 方法中, 我们将每个 jar 包和 class 文件复制到 dest 路径, 这个 dest 路径就是下一个 Transform 的输入数据, 而在复制时, 我们就可以做一些狸猫换太子, 偷天换日的事情了, 先将 jar 包和 class 文件的字节码做一些修改, 再进行复制即可, 至于怎么修改字节码, 就要借助我们后面介绍的 ASM 了. 而如果开发过程要看你当前 transform 处理之后的 class/jar 包, 可以到
/build/intermediates/transforms/CustomTransform / 下查看, 你会发现所有 jar 包命名都是 123456 递增, 这是正常的, 这里的命名规则可以在 OutputProvider.getContentLocation 的具体实现中找到
- public synchronized File getContentLocation(
- @NonNull String name,
- @NonNull Set<ContentType> types,
- @NonNull Set<? super Scope> scopes,
- @NonNull Format format) {
- // runtime check these since it's (indirectly) called by 3rd party transforms.
- checkNotNull(name);
- checkNotNull(types);
- checkNotNull(scopes);
- checkNotNull(format);
- checkState(!name.isEmpty());
- checkState(!types.isEmpty());
- checkState(!scopes.isEmpty());
- // search for an existing matching substream.
- for (SubStream subStream : subStreams) {
- // look for an existing match. This means same name, types, scopes, and format.
- if (name.equals(subStream.getName())
- && types.equals(subStream.getTypes())
- && scopes.equals(subStream.getScopes())
- && format == subStream.getFormat()) {
- return new File(rootFolder, subStream.getFilename());
- }
- }
- // 按位置递增!!
- // didn't find a matching output. create the new output
- SubStream newSubStream = new SubStream(name, nextIndex++, scopes, types, format, true);
- subStreams.add(newSubStream);
- return new File(rootFolder, newSubStream.getFilename());
- }
Transform 的优化: 增量与并发
到此为止, 看起来 Transform 用起来也不难, 但是, 如果直接这样使用, 会大大拖慢编译时间, 为了解决这个问题, 摸索了一段时间后, 也借鉴了 Android 编译器中 Desugar 等几个 Transform 的实现, 发现我们可以使用增量编译, 并且上面 transform 方法遍历处理每个 jar/class 的流程, 其实可以并发处理, 加上一般编译流程都是在 PC 上, 所以我们可以尽量敲诈机器的资源.
想要开启增量编译, 我们需要重写 Transform 的这个接口, 返回 true.
- @Override
- public boolean isIncremental() {
- return true;
- }
虽然开启了增量编译, 但也并非每次编译过程都是支持增量的, 毕竟一次 clean build 完全没有增量的基础, 所以, 我们需要检查当前编译是否是增量编译.
如果不是增量编译, 则清空 output 目录, 然后按照前面的方式, 逐个 class/jar 处理
如果是增量编译, 则要检查每个文件的 Status,Status 分四种, 并且对这四种文件的操作也不尽相同
NOTCHANGED: 当前文件不需处理, 甚至复制操作都不用;
ADDED,CHANGED: 正常处理, 输出给下一个任务;
REMOVED: 移除 outputProvider 获取路径对应的文件.
大概实现可以一起看看下面的代码
- @Override
- public void transform(TransformInvocation transformInvocation){
- Collection<TransformInput> inputs = transformInvocation.getInputs();
- TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
- boolean isIncremental = transformInvocation.isIncremental();
- // 如果非增量, 则清空旧的输出内容
- if(!isIncremental) {
- outputProvider.deleteAll();
- }
- for(TransformInput input : inputs) {
- for(JarInput jarInput : input.getJarInputs()) {
- Status status = jarInput.getStatus();
- File dest = outputProvider.getContentLocation(
- jarInput.getName(),
- jarInput.getContentTypes(),
- jarInput.getScopes(),
- Format.JAR);
- if(isIncremental && !emptyRun) {
- switch(status) {
- case NOTCHANGED:
- continue;
- case ADDED:
- case CHANGED:
- transformJar(jarInput.getFile(), dest, status);
- break;
- case REMOVED:
- if (dest.exists()) {
- FileUtils.forceDelete(dest);
- }
- break;
- }
- } else {
- transformJar(jarInput.getFile(), dest, status);
- }
- }
- for(DirectoryInput directoryInput : input.getDirectoryInputs()) {
- File dest = outputProvider.getContentLocation(directoryInput.getName(),
- directoryInput.getContentTypes(), directoryInput.getScopes(),
- Format.DIRECTORY);
- FileUtils.forceMkdir(dest);
- if(isIncremental && !emptyRun) {
- String srcDirPath = directoryInput.getFile().getAbsolutePath();
- String destDirPath = dest.getAbsolutePath();
- Map<File, Status> fileStatusMap = directoryInput.getChangedFiles();
- for (Map.Entry<File, Status> changedFile : fileStatusMap.entrySet()) {
- Status status = changedFile.getValue();
- File inputFile = changedFile.getKey();
- String destFilePath = inputFile.getAbsolutePath().replace(srcDirPath, destDirPath);
- File destFile = new File(destFilePath);
- switch (status) {
- case NOTCHANGED:
- break;
- case REMOVED:
- if(destFile.exists()) {
- FileUtils.forceDelete(destFile);
- }
- break;
- case ADDED:
- case CHANGED:
- FileUtils.touch(destFile);
- transformSingleFile(inputFile, destFile, srcDirPath);
- break;
- }
- }
- } else {
- transformDir(directoryInput.getFile(), dest);
- }
- }
- }
- }
这就能为我们的编译插件提供增量的特性.
实现了增量编译后, 我们最好也支持并发编译, 并发编译的实现并不复杂, 只需要将上面处理单个 jar/class 的逻辑, 并发处理, 最后阻塞等待所有任务结束即可.
- private WaitableExecutor waitableExecutor = WaitableExecutor.useGlobalSharedThreadPool();
- // 异步并发处理 jar/class
- waitableExecutor.execute(() -> {
- bytecodeWeaver.weaveJar(srcJar, destJar);
- return null;
- });
- waitableExecutor.execute(() -> {
- bytecodeWeaver.weaveSingleClassToFile(file, outputFile, inputDirPath);
- return null;
- });
- // 等待所有任务结束
- waitableExecutor.waitForTasksWithQuickFail(true);
接下来我们对编译速度做一个对比, 每个实验都是 5 次同种条件下编译 10 次, 去除最大大小值, 取平均时间
首先, 在 QQ 邮箱 Android 客户端工程中, 我们先做一次 cleanbuild
./gradlew clean assembleDebug --profile
给项目中添加 UI 耗时统计, 全局每个方法 (包括普通 class 文件和第三方 jar 包中的所有 class) 的第一行和最后一行都进行插桩, 实现方式就是 Transform+ASM, 对比一下并发 Transform 和非并发 Transform 下, Tranform 这一步的耗时
可以发现, 并发编译, 基本比非并发编译速度提高了 80%. 效果很显著.
然后, 让我们再做另一个试验, 我们在项目中模拟日常修改某个 class 文件的一行代码, 这时是符合增量编译的环境的. 然后在刚才基础上还是做同样的插桩逻辑, 对比增量 Transform 和全量 Transform 的差异.
./gradlew assembleDebug --profile
可以发现, 增量的速度比全量的速度提升了 3 倍多, 而且这个速度优化会随着工程的变大而更加显著.
数据表明, 增量和并发对编译速度的影响是很大的. 而我在查看 Android gradle plugin 自身的十几个 Transform 时, 发现它们实现方式也有一些区别, 有些用 kotlin 写, 有些用 java 写, 有些支持增量, 有些不支持, 而且是代码注释写了一个大大的 FIXME, To support incremental build. 所以, 讲道理, 现阶段的 Android 编译速度, 还是有提升空间的.
上面我们介绍了 Transform, 以及如何高效地在编译期间处理所有字节码, 那么具体怎么处理字节码呢? 接下来让我们一起看看 JVM 平台上的处理字节码神兵利器, ASM!
二, ASM
ASM 的官网在这里 https://asm.ow2.io/ , 贴一下它的主页介绍, 一起感受下它的强大
[图片上传失败...(image-c59976-1544358625290)]
JVM 平台上, 处理字节码的框架最常见的就三个, ASM,Javasist,AspectJ. 我尝试过 Javasist, 而 AspectJ 也稍有了解, 最终选择 ASM, 因为使用它可以更底层地处理字节码的每条命令, 处理速度, 内存占用, 也优于其他两个框架.
我们可以来做一个对比, 上面我们所做的计算编译时间实验的基础上, 做如下试验, 分别用 ASM 和 Javasist 全量处理工程所有 class, 并且都不开启并发处理的情况下, 一次 clean build 中, transform 的耗时对比如下
ASM 相比 Javasist 的优势非常显著, ASM 相比其他字节码操作库的效率和性能优势应该毋庸置疑的, 毕竟是诸多 JVM 语言钦定的字节码生成库.
我们这部分将来介绍 ASM, 但是由于篇幅问题, 不会从字节码的基础展开介绍, 会通过几个实例的实现介绍一些字节码的相关知识, 另外还会介绍 ASM 的使用, 以及 ASM 解析 class 文件结构的原理, 还有应用于 Android 插件开发时, 遇到的问题, 及其解决方案.
ASM 的引入
下面是一份完整的 gradle 自定义 plugin + transform + asm 所需依赖, 注意一下, 此处两个 gradleApi 的区别
- dependencies {
- // 使用项目中指定的 gradle wrapper 版本, 插件中使用的 Project 对象等等就来自这里
- implementation gradleApi()
- // 使用本地的 groovy
- implementation localGroovy()
- //Android 编译的大部分 gradle 源码, 比如上面讲到的 TaskManager
- implementation 'com.android.tools.build:gradle:3.1.4'
- // 这个依赖里其实主要存了 transform 的依赖, 注意, 这个依赖不同于上面的 gradleApi()
- implementation 'com.android.tools.build:gradle-api:3.1.4'
- //ASM 相关
- implementation 'org.ow2.asm:asm:5.1'
- implementation 'org.ow2.asm:asm-util:5.1'
- implementation 'org.ow2.asm:asm-commons:5.1'
- }
ASM 的应用
ASM 设计了两种 API 类型, 一种是 Tree API, 一种是基于 Visitor API(visitor pattern),
Tree API 将 class 的结构读取到内存, 构建一个树形结构, 然后需要处理 Method,Field 等元素时, 到树形结构中定位到某个元素, 进行操作, 然后把操作再写入新的 class 文件.
Visitor API 则将通过接口的方式, 分离读 class 和写 class 的逻辑, 一般通过一个 ClassReader 负责读取 class 字节码, 然后 ClassReader 通过一个 ClassVisitor 接口, 将字节码的每个细节按顺序通过接口的方式, 传递给 ClassVisitor(你会发现 ClassVisitor 中有多个 visitXXXX 接口), 这个过程就像 ClassReader 带着 ClassVisitor 游览了 class 字节码的每一个指令.
上面这两种解析文件结构的方式在很多处理结构化数据时都常见, 一般得看需求背景选择合适的方案, 而我们的需求是这样的, 出于某个目的, 寻找 class 文件中的一个 hook 点, 进行字节码修改, 这种背景下, 我们选择 Visitor API 的方式比较合适.
让我们来写一个简单的 demo, 这段代码很简单, 通过 Visitor API 读取一个 class 的内容, 保存到另一个文件
- private void copy(String inputPath, String outputPath) {
- try {
- FileInputStream is = new FileInputStream(inputPath);
- ClassReader cr = new ClassReader(is);
- ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
- cr.accept(cw, 0);
- FileOutputStream fos = new FileOutputStream(outputPath);
- fos.write(cw.toByteArray());
- fos.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
首先, 我们通过 ClassReader 读取某个 class 文件, 然后定义一个 ClassWriter, 这个 ClassWriter 我们可以看它源码, 其实就是一个 ClassVisitor 的实现, 负责将 ClassReader 传递过来的数据写到一个字节流中, 而真正触发这个逻辑就是通过 ClassWriter 的 accept 方式.
- public void accept(ClassVisitor classVisitor, Attribute[] attributePrototypes, int parsingOptions) {
- // 读取当前 class 的字节码信息
- int accessFlags = this.readUnsignedShort(currentOffset);
- String thisClass = this.readClass(currentOffset + 2, charBuffer);
- String superClass = this.readClass(currentOffset + 4, charBuffer);
- String[] interfaces = new String[this.readUnsignedShort(currentOffset + 6)];
- //classVisitor 就是刚才 accept 方法传进来的 ClassWriter, 每次 visitXXX 都负责将字节码的信息存储起来
- classVisitor.visit(this.readInt(this.cpInfoOffsets[1] - 7), accessFlags, thisClass, signature, superClass, interfaces);
- /**
- 略去很多 visit 逻辑
- */
- //visit Attribute
- while(attributes != null) {
- Attribute nextAttribute = attributes.nextAttribute;
- attributes.nextAttribute = null;
- classVisitor.visitAttribute(attributes);
- attributes = nextAttribute;
- }
- /**
- 略去很多 visit 逻辑
- */
- classVisitor.visitEnd();
- }
最后, 我们通过 ClassWriter 的 toByteArray(), 将从 ClassReader 传递到 ClassWriter 的字节码导出, 写入新的文件即可. 这就完成了 class 文件的复制, 这个 demo 虽然很简单, 但是涵盖了 ASM 使用 Visitor API 修改字节码最底层的原理, 大致流程如图
我们来分析一下, 不难发现, 如果我们要修改字节码, 就是要从 ClassWriter 入手, 上面我们提到 ClassWriter 中每个 visitXXX(这些接口实现自 ClassVisitor)都会保存字节码信息并最终可以导出, 那么如果我们可以代理 ClassWriter 的接口, 就可以干预最终字节码的生成了.
那么上面的图就应该是这样
我们只要稍微看一下 ClassVisitor 的代码, 发现它的构造函数, 是可以接收另一个 ClassVisitor 的, 从而通过这个 ClassVisitor 代理所有的方法. 让我们来看一个例子, 为 class 中的每个方法调用语句的开头和结尾插入一行代码
修改前的方法是这样
- private static void printTwo() {
- printOne();
- printOne();
- }
被修改后的方法是这样
- private static void printTwo() {
- System.out.println("CALL printOne");
- printOne();
- System.out.println("RETURN printOne");
- System.out.println("CALL printOne");
- printOne();
- System.out.println("RETURN printOne");
- }
让我们来看一下如何用 ASM 实现
- private static void weave(String inputPath, String outputPath) {
- try {
- FileInputStream is = new FileInputStream(inputPath);
- ClassReader cr = new ClassReader(is);
- ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
- CallClassAdapter adapter = new CallClassAdapter(cw);
- cr.accept(adapter, 0);
- FileOutputStream fos = new FileOutputStream(outputPath);
- fos.write(cw.toByteArray());
- fos.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
这段代码和上面的实现复制 class 的代码唯一区别就是, 使用了 CallClassAdapter, 它是一个自定义的 ClassVisitor, 我们将 ClassWriter 传递给 CallClassAdapter 的构造函数. 来看看它的实现
- //CallClassAdapter.java
- public class CallClassAdapter extends ClassVisitor implements Opcodes {
- public CallClassAdapter(final ClassVisitor cv) {
- super(ASM5, cv);
- }
- @Override
- public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
- super.visit(version, access, name, signature, superName, interfaces);
- }
- @Override
- public MethodVisitor visitMethod(final int access, final String name,
- final String desc, final String signature, final String[] exceptions) {
- MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
- return mv == null ? null : new CallMethodAdapter(name, mv);
- }
- }
- //CallMethodAdapter.java
- class CallMethodAdapter extends MethodVisitor implements Opcodes {
- public CallMethodAdapter(final MethodVisitor mv) {
- super(ASM5, mv);
- }
- @Override
- public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
- mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
- mv.visitLdcInsn("CALL" + name);
- mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
- mv.visitMethodInsn(opcode, owner, name, desc, itf);
- mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
- mv.visitLdcInsn("RETURN" + name);
- mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
- }
- }
CallClassAdapter 中的 visitMethod 使用了一个自定义的 MethodVisitor--CallMethodAdapter, 它也是代理了原来的 MethodVisitor, 原理和 ClassVisitor 的代理一样.
看到这里, 貌似使用 ASM 修改字节码的大概套路都走完了, 那么如何写出上面 visitMethodInsn 方法中插入打印方法名的逻辑, 这就需要一些字节码的基础知识了, 我们说过这里不会展开介绍字节码, 但是我们可以介绍一些快速学习字节码的方式, 同时也是开发字节码相关工程一些实用的工具.
在这之前, 我们先讲讲行号的问题
如何验证行号
上面我们给每一句方法调用的前后都插入了一行日志打印, 那么有没有想过, 这样岂不是打乱了代码的行数, 这样, 万一 crash 了, 定位堆栈岂不是乱套了. 其实并不然, 在上面 visitMethodInsn 中做的东西, 其实都是在同一行中插入的代码, 上面我们贴出来的代码是这样
- private static void printTwo() {
- System.out.println("CALL printOne");
- printOne();
- System.out.println("RETURN printOne");
- System.out.println("CALL printOne");
- printOne();
- System.out.println("RETURN printOne");
- }
无论你用 idea 还是 eclipse 打开上面的 class 文件, 都是一行行展示的, 但是其实 class 内部真实的行数应该是这样
- private static void printTwo() {
- System.out.println("CALL printOne"); printOne(); System.out.println("RETURN printOne");
- System.out.println("CALL printOne"); printOne(); System.out.println("RETURN printOne");
- }
idea 下可以开启一个选项, 让查看 class 内容时, 保留真正的行数
开启后, 你看到的是这样
我们可以发现, 17 行和 18 行, 分别包含了三句代码.
而开启选项之前是这样
那么如何开启这个选项呢? Mac 下 cmd + shift + A 输入 Registry, 勾选这两个选项
其实无论字节码和 ASM 的代码上看, class 中的所有代码, 都是先声明行号 X, 然后开始几条字节码指令, 这几条字节码对应的代码都在行号 X 中, 直到声明下一个新的行号.
ASM code
解析来介绍, 如何写出上面生成代码的逻辑. 首先, 我们设想一下, 如果要对某个 class 进行修改, 那需要对字节码具体做什么修改呢? 最直观的方法就是, 先编译生成目标 class, 然后看它的字节码和原来 class 的字节码有什么区别(查看字节码可以使用 javap 工具), 但是这样还不够, 其实我们最终并不是读写字节码, 而是使用 ASM 来修改, 我们这里先做一个区别, bytecode vs ASM code, 前者就是 JVM 意义的字节码, 而后者是用 ASM 描述的 bytecode, 其实二者非常的接近, 只是 ASM code 用 Java 代码来描述. 所以, 我们应该是对比 ASM code, 而不是对比 bytecode. 对比 ASM code 的 diff, 基本就是我们要做的修改.
而 ASM 也提供了一个这样的类: ASMifier, 它可以生成 ASM code, 但是, 其实还有更快捷的工具, Intellij IDEA 有一个插件
Asm Bytecode Outline, 可以查看一个 class 文件的 bytecode 和 ASM code.
到此为止, 貌似使用对比 ASM code 的方式, 来实现字节码修改也不难, 但是, 这种方式只是可以实现一些修改字节码的基础场景, 还有很多场景是需要对字节码有一些基础知识才能做到, 而且, 要阅读懂 ASM code, 也是需要一定字节码的的知识. 所以, 如果要开发字节码工程, 还是需要学习一番字节码.
ClassWriter 在 Android 上的坑
如果我们直接按上面的套路, 将 ASM 应用到 Android 编译插件中, 会踩到一个坑, 这个坑来自于 ClassWriter, 具体是因为 ClassWriter 其中的一个逻辑, 寻找两个类的共同父类. 可以看看 ClassWriter 中的这个方法 getCommonSuperClass,
- /**
- * Returns the common super type of the two given types. The default
- * implementation of this method <i>loads</i> the two given classes and uses
- * the java.lang.Class methods to find the common super class. It can be
- * overridden to compute this common super type in other ways, in particular
- * without actually loading any class, or to take into account the class
- * that is currently being generated by this ClassWriter, which can of
- * course not be loaded since it is under construction.
- *
- * @param type1
- * the internal name of a class.
- * @param type2
- * the internal name of another class.
- * @return the internal name of the common super class of the two given
- * classes.
- */
- protected String getCommonSuperClass(final String type1, final String type2) {
- Class<?> c, d;
- ClassLoader classLoader = getClass().getClassLoader();
- try {
- c = Class.forName(type1.replace('/', '.'), false, classLoader);
- d = Class.forName(type2.replace('/', '.'), false, classLoader);
- } catch (Exception e) {
- throw new RuntimeException(e.toString());
- }
- if (c.isAssignableFrom(d)) {
- return type1;
- }
- if (d.isAssignableFrom(c)) {
- return type2;
- }
- if (c.isInterface() || d.isInterface()) {
- return "java/lang/Object";
- } else {
- do {
- c = c.getSuperclass();
- } while (!c.isAssignableFrom(d));
- return c.getName().replace('.', '/');
- }
- }
这个方法用于寻找两个类的共同父类, 我们可以看到它是获取当前 class 的 classLoader 加载两个输入的类型, 而编译期间使用的 classloader 并没有加载 Android 项目中的代码, 所以我们需要一个自定义的 ClassLoader, 将前面提到的 Transform 中接收到的所有 jar 以及 class, 还有 Android.jar 都添加到自定义 ClassLoader 中.(其实上面这个方法注释中已经暗示了这个方法存在的一些问题)
如下
- public static URLClassLoader getClassLoader(Collection<TransformInput> inputs,
- Collection<TransformInput> referencedInputs,
- Project project) throws MalformedURLException {
- ImmutableList.Builder<URL> urls = new ImmutableList.Builder<>();
- String androidJarPath = getAndroidJarPath(project);
- File file = new File(androidJarPath);
- URL androidJarURL = file.toURI().toURL();
- urls.add(androidJarURL);
- for (TransformInput totalInputs : Iterables.concat(inputs, referencedInputs)) {
- for (DirectoryInput directoryInput : totalInputs.getDirectoryInputs()) {
- if (directoryInput.getFile().isDirectory()) {
- urls.add(directoryInput.getFile().toURI().toURL());
- }
- }
- for (JarInput jarInput : totalInputs.getJarInputs()) {
- if (jarInput.getFile().isFile()) {
- urls.add(jarInput.getFile().toURI().toURL());
- }
- }
- }
- ImmutableList<URL> allUrls = urls.build();
- URL[] classLoaderUrls = allUrls.toArray(new URL[allUrls.size()]);
- return new URLClassLoader(classLoaderUrls);
- }
但是, 如果只是替换了 getCommonSuperClass 中的 Classloader, 依然还有一个更深的坑, 我们可以看看前面 getCommonSuperClass 的实现, 它是如何寻找父类的呢? 它是通过 Class.forName 加载某个类, 然后再去寻找父类, 但是, 但是, Android.jar 中的类可不能随随便便加载的呀, Android.jar 对于 Android 工程来说只是编译时依赖, 运行时是用 Android 机器上自己的 Android.jar. 而且 Android.jar 所有方法包括构造函数都是空实现, 其中都只有一行代码
throw new RuntimeException("Stub!");
这样加载某个类时, 它的静态域就会被触发, 而如果有一个 static 的变量刚好在声明时被初始化, 而初始化中只有一个 RuntimeException, 此时就会抛异常.
所以, 我们不能通过这种方式来获取父类, 能否通过不需要加载 class 就能获取它的父类的方式呢? 谜底就在眼前, 父类其实也是一个 class 的字节码中的一项数据, 那么我们就从字节码中查询父类即可. 最终实现是这样.
- public class ExtendClassWriter extends ClassWriter {
- public static final String TAG = "ExtendClassWriter";
- private static final String OBJECT = "java/lang/Object";
- private ClassLoader urlClassLoader;
- public ExtendClassWriter(ClassLoader urlClassLoader, int flags) {
- super(flags);
- this.urlClassLoader = urlClassLoader;
- }
- @Override
- protected String getCommonSuperClass(final String type1, final String type2) {
- if (type1 == null || type1.equals(OBJECT) || type2 == null || type2.equals(OBJECT)) {
- return OBJECT;
- }
- if (type1.equals(type2)) {
- return type1;
- }
- ClassReader type1ClassReader = getClassReader(type1);
- ClassReader type2ClassReader = getClassReader(type2);
- if (type1ClassReader == null || type2ClassReader == null) {
- return OBJECT;
- }
- if (isInterface(type1ClassReader)) {
- String interfaceName = type1;
- if (isImplements(interfaceName, type2ClassReader)) {
- return interfaceName;
- }
- if (isInterface(type2ClassReader)) {
- interfaceName = type2;
- if (isImplements(interfaceName, type1ClassReader)) {
- return interfaceName;
- }
- }
- return OBJECT;
- }
- if (isInterface(type2ClassReader)) {
- String interfaceName = type2;
- if (isImplements(interfaceName, type1ClassReader)) {
- return interfaceName;
- }
- return OBJECT;
- }
- final Set<String> superClassNames = new HashSet<String>();
- superClassNames.add(type1);
- superClassNames.add(type2);
- String type1SuperClassName = type1ClassReader.getSuperName();
- if (!superClassNames.add(type1SuperClassName)) {
- return type1SuperClassName;
- }
- String type2SuperClassName = type2ClassReader.getSuperName();
- if (!superClassNames.add(type2SuperClassName)) {
- return type2SuperClassName;
- }
- while (type1SuperClassName != null || type2SuperClassName != null) {
- if (type1SuperClassName != null) {
- type1SuperClassName = getSuperClassName(type1SuperClassName);
- if (type1SuperClassName != null) {
- if (!superClassNames.add(type1SuperClassName)) {
- return type1SuperClassName;
- }
- }
- }
- if (type2SuperClassName != null) {
- type2SuperClassName = getSuperClassName(type2SuperClassName);
- if (type2SuperClassName != null) {
- if (!superClassNames.add(type2SuperClassName)) {
- return type2SuperClassName;
- }
- }
- }
- }
- return OBJECT;
- }
- private boolean isImplements(final String interfaceName, final ClassReader classReader) {
- ClassReader classInfo = classReader;
- while (classInfo != null) {
- final String[] interfaceNames = classInfo.getInterfaces();
- for (String name : interfaceNames) {
- if (name != null && name.equals(interfaceName)) {
- return true;
- }
- }
- for (String name : interfaceNames) {
- if(name != null) {
- final ClassReader interfaceInfo = getClassReader(name);
- if (interfaceInfo != null) {
- if (isImplements(interfaceName, interfaceInfo)) {
- return true;
- }
- }
- }
- }
- final String superClassName = classInfo.getSuperName();
- if (superClassName == null || superClassName.equals(OBJECT)) {
- break;
- }
- classInfo = getClassReader(superClassName);
- }
- return false;
- }
- private boolean isInterface(final ClassReader classReader) {
- return (classReader.getAccess() & Opcodes.ACC_INTERFACE) != 0;
- }
- private String getSuperClassName(final String className) {
- final ClassReader classReader = getClassReader(className);
- if (classReader == null) {
- return null;
- }
- return classReader.getSuperName();
- }
- private ClassReader getClassReader(final String className) {
- InputStream inputStream = urlClassLoader.getResourceAsStream(className + ".class");
- try {
- if (inputStream != null) {
- return new ClassReader(inputStream);
- }
- } catch (IOException ignored) {
- } finally {
- if (inputStream != null) {
- try {
- inputStream.close();
- } catch (IOException ignored) {
- }
- }
- }
- return null;
- }
- }
到此为止, 我们介绍了在 Android 上实现修改字节码的两个基础技术 Transform+ASM, 介绍了其原理和应用, 分析了性能优化以及在 Android 平台上的适配等. 在此基础上, 我抽象出一个轮子, 让开发者写字节码插件时, 只需要写少量的 ASM code 即可, 而不需关心 Transform 和 ASM 背后的很多细节. 详见
万事俱备, 只欠写一个插件来玩玩了, 让我们来看看几个应用案例.
应用案例
先抛结论, 修改字节码其实也有套路, 一种是 hack 代码调用, 一种是 hack 代码实现.
比如修改 Android Framework(Android.jar)的实现, 你是没办法在编译期间达到这个目的的, 因为最终 Android Framework 的 class 在 Android 设备上. 所以这种情况下你需要从 hack 代码调用入手, 比如 Log.i(TAG, "hello"), 你不可能 hack 其中的实现, 但是你可以把它 hack 成 HackLog.i(TAG, "seeyou").
而如果是要修改第三方依赖或者工程中写的代码, 则可以直接 hack 代码实现, 但是, 当如果你要插入的字节码比较多时, 也可以通过一定技巧减少写 ASM code 的量, 你可以将大部分可以抽象的逻辑抽象到某个写好的 class 中, 然后 ASM code 只需写调用这个写好的 class 的语句.
当然上面只是目前按照我的经验做的一点总结, 还是有一些更复杂的情况要具体情况具体分析, 比如在实现类似 JakeWharton 的 https://github.com/JakeWharton/hugo 的功能时, 在代码开头获取方法参数名时我就遇到棘手的问题(用了一种二次扫描的方式解决了这个问题, 可以移步项目主页参考具体实现).
我们这里挑选 OkHttp-Plugin 的实现进行分析, 演示如何使用 Huntet 框架开发一个字节码编译插件.
使用 OkHttp 的人知道, OkHttp 里每一个 OkHttp 都可以设置自己独立的 Intercepter/Dns/EventListener(EventListener 是 okhttp3.11 新增), 但是需要对全局所有 OkHttp 设置统一的 Intercepter/Dns/EventListener 就很麻烦, 需要一处处设置, 而且一些第三方依赖中的 OkHttp 很大可能无法设置. 曾经在官方 repo 提过这个问题的 https://github.com/square/okhttp/issues/4228 , 没有得到很好的回复, 作者之一觉得如果是他, 他会用依赖注入的方式来实现统一的 Okhttp 配置, 但是这种方式只能说可行但是不理想, 后台在 reddit 发 帖子安利自己 Hunter 这个轮子时, JakeWharton 大佬竟然亲自回答了, 虽然面对大佬, 不过还是要正面刚! 争论一波之后, 总结一下他的立场, 大概如下
他觉得我说的好像这是 okhttp 的锅, 然而这其实是 okhttp 的一个 feature, 他觉得全局状态是一种不好的编码, 所以在设计 okhttp 没有提供全局 Intercepter/Dns/EventListener 的接口. 而第三方依赖库不能设置自定义 Intercepter/Dns/EventListener 这是它们的锅.
但是, 他的观点我不完全同意, 虽然全局状态确实是一种不好的设计, 但是, 如果要做性能监控之类的功能, 这就很难避免或多或少的全局侵入.(不过我确实措辞不当, 说得这好像是 Okhttp 的锅一样)
言归正传, 来看看我们要怎么来对 OkHttp 动刀, 请看以下代码
- public Builder(){
- this.dispatcher = new Dispatcher();
- this.protocols = OkHttpClient.DEFAULT_PROTOCOLS;
- this.connectionSpecs = OkHttpClient.DEFAULT_CONNECTION_SPECS;
- this.eventListenerFactory = EventListener.factory(EventListener.NONE);
- this.proxySelector = ProxySelector.getDefault();
- this.cookieJar = CookieJar.NO_COOKIES;
- this.socketFactory = SocketFactory.getDefault();
- this.hostnameVerifier = OkHostnameVerifier.INSTANCE;
- this.certificatePinner = CertificatePinner.DEFAULT;
- this.proxyAuthenticator = Authenticator.NONE;
- this.authenticator = Authenticator.NONE;
- this.connectionPool = new ConnectionPool();
- this.dns = Dns.SYSTEM;
- this.followSslRedirects = true;
- this.followRedirects = true;
- this.retryOnConnectionFailure = true;
- this.connectTimeout = 10000;
- this.readTimeout = 10000;
- this.writeTimeout = 10000;
- this.pingInterval = 0;
- this.eventListenerFactory = OkHttpHooker.globalEventFactory;
- this.dns = OkHttpHooker.globalDns;
- this.interceptors.addAll(OkHttpHooker.globalInterceptors);
- this.networkInterceptors.addAll(OkHttpHooker.globalNetworkInterceptors);
- }
这是 OkhttpClient 中内部类 Builder 的构造函数, 我们的目标是在方法末尾加上四行代码, 这样一来, 所有的 OkHttpClient 都会拥有共同的 Intercepter/Dns/EventListener. 我们再来看看 OkHttpHooker 的实现
public class OkHttpHooker { public static EventListener.Factory globalEventFactory = new EventListener.Factory() { public EventListener create(Call call) { return EventListener.NONE; } };; public static Dns globalDns = Dns.SYSTEM; public static List<Interceptor> globalInterceptors = new ArrayList<>(); public static List<Interceptor> globalNetworkInterceptors = new ArrayList<>(); public static void installEventListenerFactory(EventListener.Factory factory) { globalEventFactory = factory; } public static void installDns(Dns dns) { globalDns = dns; } public static void installInterceptor(Interceptor interceptor) { if(interceptor != null) globalInterceptors.add(interceptor); } public static void installNetworkInterceptors(Interceptor networkInterceptor) { if(networkInterceptor != null) globalNetworkInterceptors.add(networkInterceptor); } }
这样, 只需要为 OkHttpHooker 预先 install 好几个全局的 Intercepter/Dns/EventListener 即可.
那么, 如何来实现上面 OkhttpClient 内部 Builder 中插入四行代码呢?
首先, 我们通过 Hunter 的框架, 可以隐藏掉 Transform 和 ASM 绝大部分细节, 我们只需把注意力放在写 ClassVisitor 以及 MethodVisitor 即可. 我们一共需要做以下几步
1, 新建一个自定义 transform, 添加到一个自定义 gradle plugin 中
2, 继承 HunterTransform 实现自定义 transform
3, 实现自定义的 ClassVisitor, 并依情况实现自定义 MethodVisitor
其中第一步文章讲解 transform 一部分有讲到, 基本是一样简短的写法, 我们从第二步讲起
继承 HunterTransform, 就可以让你的 transform 具备并发, 增量的功能.
final class OkHttpHunterTransform extends HunterTransform { private Project project; private OkHttpHunterExtension okHttpHunterExtension; public OkHttpHunterTransform(Project project) { super(project); this.project = project; // 依情况而定, 看看你需不需要有插件扩展 project.getExtensions().create("okHttpHunterExt", OkHttpHunterExtension.class); // 必须的一步, 继承 BaseWeaver, 帮你隐藏 ASM 细节 this.bytecodeWeaver = new OkHttpWeaver(); } @Override public void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException { okHttpHunterExtension = (OkHttpHunterExtension) project.getExtensions().getByName("okHttpHunterExt"); super.transform(context, inputs, referencedInputs, outputProvider, isIncremental); } // 用于控制修改字节码在哪些 debug 包还是 release 包下发挥作用, 或者完全打开 / 关闭 @Override protected RunVariant getRunVariant() { return okHttpHunterExtension.runVariant; } } //BaseWeaver 帮你隐藏了 ASM 的很多复杂逻辑 public final class OkHttpWeaver extends BaseWeaver { @Override protected ClassVisitor wrapClassWriter(ClassWriter classWriter) { return new OkHttpClassAdapter(classWriter); } } // 插件扩展 public class OkHttpHunterExtension { public RunVariant runVariant = RunVariant.ALWAYS; @Override public String toString() { return "OkHttpHunterExtension{" + "runVariant=" + runVariant + '}'; } }
好了, Transform 写起来就变得这么简单, 接下来看自定义 ClassVisitor, 它在 OkHttpWeaver 返回.
我们新建一个 ClassVisitor(自定义 ClassVisitor 是为了代理 ClassWriter, 前面讲过)
public final class OkHttpClassAdapter extends ClassVisitor{ private String className; OkHttpClassAdapter(final ClassVisitor cv) { super(Opcodes.ASM5, cv); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces); this.className = name; } @Override public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) { MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); if(className.equals("okhttp3/OkHttpClient$Builder")) { return mv == null ? null : new OkHttpMethodAdapter(className + File.separator + name, access, desc, mv); } else { return mv; } } }
我们寻找出 okhttp3/OkHttpClient$Builder 这个类, 其他类不管它, 那么其他类只会被普通的复制, 而 okhttp3/OkHttpClient$Builder 将会有自定义的 MethodVisitor 来处理
我们来看看这个 MethodVisitor 的实现
public final class OkHttpMethodAdapter extends LocalVariablesSorter implements Opcodes { private boolean defaultOkhttpClientBuilderInitMethod = false; OkHttpMethodAdapter(String name, int access, String desc, MethodVisitor mv) { super(Opcodes.ASM5, access, desc, mv); if ("okhttp3/OkHttpClient$Builder/<init>".equals(name) && "()V".equals(desc)) { defaultOkhttpClientBuilderInitMethod = true; } } @Override public void visitInsn(int opcode) { if(defaultOkhttpClientBuilderInitMethod) { if ((opcode>= IRETURN && opcode <= RETURN) || opcode == ATHROW) { //EventListenFactory mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETSTATIC, "com/hunter/library/okhttp/OkHttpHooker", "globalEventFactory", "Lokhttp3/EventListener$Factory;"); mv.visitFieldInsn(PUTFIELD, "okhttp3/OkHttpClient$Builder", "eventListenerFactory", "Lokhttp3/EventListener$Factory;"); //Dns mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETSTATIC, "com/hunter/library/okhttp/OkHttpHooker", "globalDns", "Lokhttp3/Dns;"); mv.visitFieldInsn(PUTFIELD, "okhttp3/OkHttpClient$Builder", "dns", "Lokhttp3/Dns;"); //Interceptor mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, "okhttp3/OkHttpClient$Builder", "interceptors", "Ljava/util/List;"); mv.visitFieldInsn(GETSTATIC, "com/hunter/library/okhttp/OkHttpHooker", "globalInterceptors", "Ljava/util/List;"); mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "addAll", "(Ljava/util/Collection;)Z", true); mv.visitInsn(POP); //NetworkInterceptor mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, "okhttp3/OkHttpClient$Builder", "networkInterceptors", "Ljava/util/List;"); mv.visitFieldInsn(GETSTATIC, "com/hunter/library/okhttp/OkHttpHooker", "globalNetworkInterceptors", "Ljava/util/List;"); mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "addAll", "(Ljava/util/Collection;)Z", true); mv.visitInsn(POP); } } super.visitInsn(opcode); } }
首先, 我们先找出 okhttp3/OkHttpClient$Builder 的构造函数, 然后在这个构造函数的末尾, 执行插入字节码的逻辑, 我们可以发现, 字节码的指令是符合逆波兰式的, 都是操作数在前, 操作符在后.
至此, 我们只需要发布插件, 然后 apply 到我们的项目中即可.
借助 Hunter 框架, 我们很轻松就成功 hack 了 Okhttp, 我们就可以用全局统一的 Intercepter/Dns/EventListener 来监控我们 App 的网络了.
讲到这里, 就完整得介绍了如何使用 Hunter 框架开发一个字节码编译插件, 对第三方依赖库为所欲为. 如果对于代码还有疑惑, 可以移步项目主页, 参考完整代码, 以及其他几个插件的实现.
来源: http://www.jianshu.com/p/996f16ec2157