1 什么是插桩?
听到关于“插桩”的词语,第一眼觉得会很高深,那到底什么是插桩呢?用通俗的话来讲,插桩就是将一段代码通过某种策略插入到另一段代码,或替换另一段代码。这里的代码可以分为源码和字节码,而我们所说的插桩一般指字节码插桩。
图1是Android开发者常见的一张图,我们编写的源码(.java)通过javac编译成字节码(.class),然后通过dx/d8编译成dex文件。

我们下面要讲的插桩,就是在.class转为.dex之前,修改.class文件从而达到修改或替换代码的目的。
那有人肯定会有这样的疑问?既然插桩是插入或替换代码,那为何我不自己直接插入或替换呢?为何还要用这么“复杂”的工具?别着急,第二个问题将会给你答案。
2 插桩的应用场景有哪些?
技术是服务于业务的,一个无法推进业务进步的技术并不值得我们学习。在上面,我们对插桩的理解是:插入,替换代码。那么,结合这个核心主线我们来挖掘插桩能被应用的场景有哪些?
- 代码插入
我们所熟悉的ButterKnife,Dagger这些常用的框架,也是在编译期间生成了代码,简化了程序员的操作。假设有这么一个需求,要监控某些或者所有方法的执行耗时?你会怎么做呢?如果你监控的方法只有十几个或者几十个,那么也许通过程序员自身的编码就能轻松解决;但是如果监控的方法达到百千甚至万级别,你还通过编码来解决?那么程序员存在的价值在哪里?面对这样的重复劳动问题,最先想到的就应该是自动化,也就是我们今天所讲的插桩。通过插桩,我们扫描每一个class文件,并针对特定规则进行字节码修改从而达到监控每个方法耗时的目的。关于如何实现这样的需求,后面我会详细讲述。
- 代码替换
如果遇到这么一个需求,需要将项目中所有使用某个方法(如Dialog.show())的地方替换成自己包装的方法(MyDialog.show()),那么你该如何解决呢?有人会说,直接使用快捷键就能全局替换。那么有两个问题
1 如果有其他类定义了show()方法,并被调用了,直接使用快捷键是否会被错误替换?
2 如果其他引用包使用了该方法,你怎么替换呢?
没关系,插桩同样可以解决你的问题。
综合上面所说的两点,其实很多业务场景都使用了插桩技术,比如无痕埋点,性能监控等。
3 掌握插桩应该具备的基础知识有哪些?
上面讲了插桩的应用场景,是否现在想跃跃欲试呢?别着急,想掌握好插桩技术,练就扎实的插桩功底,我们是需要具备一些基础知识的。
- 熟练掌握字节码相关技术。可参考 一文让你明白Java字节码
- Gradle自定义插件,直接参考官网 Writing Custom plugins
- 如果你想运用在Android项目中,那么还需要掌握Transform API,
- 这是android在将class转成dex之前给我们预留的一个接口,在该接口中我们可以通过插件形式来修改class文件。
- 字节码修改工具。如AspectJ,ASM,javasisst。这里我推荐使用ASM,关于ASM相关知识,在下一章我给大家简单介绍。同样大家可以参考 Asm官方文档
- groovy语言基础
- 如果你具备了上面5块知识,那么恭喜你,会很顺利的完成字节码插桩技术了。下面,我通过实战一个很简单的例子,带领大家一起领略插桩的风采。
4 使用ASM进行字节码插桩
1 什么是ASM?
ASM是生成和转换已编译的Java类工具,就是我们插桩需要使用的工具。
2 两种API?
ASM提供了两种API来生成和转换已编译类,一个是核心API,以基于事件形式来表示类;另一个是树API,以基于对象形式来表示类。
3 基于事件形式
我们通过上面的基础知识,了解到类的结构,类包含字段,方法,指令等;基于事件的API把类看作是一系列事件来表示,每一个类的事件表示一个类的元素。类似解析XML的SAX
4 基于对象形式
基于对象的API将类表示成一棵对象树,每个对象表示类的一部分。类似解析XML的DOM
5 优缺点比较
事件形式 对象形式 内存占用 少 多 实现难度 难 易 通过上面表格,我们清楚的了解到:
- 事件API内存占用少于对象API,因为事件API不需要在内存中创建和存储对象树
- 事件API实现难度比对象API大,因为事件API在任意时刻类中只有一个元素可使用,但是对象API能获得整个类。
- 那么接下来,我们就通过比较容易实现的对象API入手,一起完成上面的需求。
- 我们Android的构建工具是Gradle,因此我们结合transform和Gradle插件方式来完成该需求,接下来我们来看看gradle官方提供的3种插件形式
- 6 Gradle插件的3种形式
插件形式 说明 Build script 直接在build script中写插件代码,不可复用 buildSrc 独立项目结构,只能在本构建体系中复用,无法提供给其他项目 Standalone 独立项目结构,发布到仓库,可以复用 由于我们是demo,并不需要共享给其他项目,因此采用buildSrc方式即可,但是正常项目中都采用Standalone形式。
5 插桩实践
目标 : 删除所有以test开头的方法
接下来我们来完成一个非常小的需求,删除所有以test开头的方法。为什么说这是一个小需求,因为这并不涉及指令的操作,所有操作通过方法名完成即可。通过完成这个demo,只是抛砖引玉。如若后期需要,可以逐步深入到指令级别替换。
接下来的步骤就是创建demo的过程
- 1 新建buildSrc目录,用来存放源代码位置。针对不同语言可以新建不同目录。

如上图所示的是buildSrc的结构。
- 2 在buildSrc的gradle文件中我们需要配置如下代码
apply plugin: 'groovy'dependencies { compile gradleApi()//在使用自定义插件时候,一定要引用org.gradle.api.Plugin compile 'com.android.tools.build:gradle:3.3.2'//使用自定义transform时候,需要引用com.android.build.api.transform.Transform compile 'org.ow2.asm:asm:6.0' compile 'commons-io:commons-io:2.6'}repositories { mavenCentral() jcenter() google()}
- 3 重写Transform API
在groovy目录下新建一个groovy类并继承Transform,注意导包com.android.build.api.transform,并实现抽象方法和transform方法,如下
class MyTransform extends Transform { Project project MyTransform(Project project) { this.project = project } @Override String getName() { return "MyTransform" } //设置输入类型,我们是针对class文件处理 @Override Set getInputTypes() { return TransformManager.CONTENT_CLASS } //设置输入范围,我们选择整个项目 @Override Set super QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT } @Override boolean isIncremental() { return true } //重点就是该方法,我们需要将修改字节码的逻辑就从这里开始 @Override void transform(Context context, Collection inputs, Collection referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException { inputs.each { TransformInput input -> input.getJarInputs().each { //处理jar文件,代码太多,这里暂时不贴 } input.getDirectoryInputs().each { //处理目录文件,这里的ASMHelper.transformClass()是修改字节码逻辑 def destDir = transformInvocation.outputProvider.getContentLocation( "${dir.name}_transformed