<
编译插桩操作字节码
>
上一篇

理解ClassLoader加载机制
下一篇

GC回收机制与分代回收策略

编译插桩操作字节码

首先举个例子,加入有一个需求:

记录每一个页面的打开和关闭事件,并通过各种DataTracking的框架上传到服务器,用来日后做数据分析

我们一般会使用,在每个Activity的onCreate和onDestroy方法中,分别添加页面打开和页面关闭的逻辑。常见做法有两种,如下:

  1. 修改项目每个Activity,这样显然非常捞而且容易遗漏
  2. 在BaseActivity中进行修改,这样能高级一点,也是常用的,但是有一点,这样对于第三方依赖库中的界面无能为力 = =!

这种环境下,有一种更优雅的方法: 编译插桩

编译插桩是啥

从名字来看,就是在代码编译期间修改已有的代码或生成新的代码 。 我们项目中经常用到的Dagger,ButterKnife甚至Kotlin语言,都用到了编译插桩(Kotlin想学没时间啊,一定要了解的)

首先看下Android项目中.java文件的编译过程

1

从上图可以看出,我们可以在1,2两处对代码进行改造

  1. 在.java文件编译成.class文件时,APT,AndroidAnnotation等就是在此触发代码生成
  2. 在.class文件进一步优化成.dex文件时,也就是直接操作字节码文件,这也是主要要了解的。这种方式功能强大,应用场景更广,但需要对字节码有一定理解

我们主要了解第2种方式,用一张图来描述如下过程,其中红色虚框包含了此次学习的内容。

2

一般使用编译插桩实现如下几个功能:

插桩工具介绍

目前主要流行两种实现编译插桩的方式:

AspectJ

优点是成熟稳定,使用人不需要对字节码文件有深入理解

ASM

通过ASM可以修改现有的字节码文件,也可以动态生成字节码文件,并且它是一款完全以字节码层面来操纵字节码并分析字节码的框架。

我们这次就使用ASM来实现简单的编译插桩效果,通过插桩实现我们上面说的需求,在每一个Activity打开时输出相应的log日志

实现思路

过程主要包含2步:

1.遍历项目中的所有.class文件

如何找到项目中编译生成的所有.class文件 ,是需要解决的第一个问题。众所周知AS使用Gradle编译项目中的.java文件,并且从Gradle1.5.0之后,我们可以自定义Transform ,来获取所有.class文件引用 。但是Transform的使用需要依赖Gradle Plugin

所以第一步需要创建一个单独的Gradle Plugin,并在Gradle Plugin中使用自定义Transform找出所有的.class文件。

2.遍历到目标.class文件(Activity)之后,通过ASM动态注入需要被插入的字节码

如果第一步顺利,我们可以找到所有的.class文件。接下来就需要过滤出目标Activity文件,并在目标Activity文件的onCreate方法中,通过ASM插入相应的log日志字节码

具体实现

1.创建ASMLifeCycleDemo项目

3

2.创建自定义Gradle插件

首先在ASMLifeCycleDemo项目中创建一个新的module,并选择Android Library类型,命名为asm_lifecycle_plugin。

将asm_lifecycle_plugin module中除了build.gradle和main文件夹之外的所有内容都删除。然后在main目录下分别创建groovy和java目录的

4

因为Gradle插件时使用groovy语言编写的,所以需要新建一个groovy目录,用来存放插件相关的.groovy类。但ASM是Java层面的框架,所以在Java目录里存放ASM相关的类。

然后在groovy中创建目录对应目录,并在此目录中创建类LifeCyclePlugin.groovy文件。在LifeCyclePlugin中重写apply方法,实现插件逻辑,这里只打印log日志。

5

可以看出LifeCyclePlugin实现了gradle api中的Plugin接口。当我们在app module的build.gradle文件中使用此插件时,其LifeCyclePlugin的apply方法会被自动调用。

接下来将asm_lifecycle_plugin module的build.gradle中内容都删除,改为如下内容:

6

group和version都需要在app module引入此插件时使用

所有的插件都需要被部署到maven库中,我们可以选择部署到远程或本地。这里demo只部署到本地目录中。具体地址通过repository属性配置,如图我们只是将其配置在项目根目录下的asm_lifecycle_repo目录下

最后一步,创建properties文件

在plugin/src/main目录下新建目录resource/META-INF/gradle-plugins,然后在此目录下新建一个文件,这个文件名就是我们自定义插件的名称,我们会在app module中使用到此名称

在.properties文件中,需要指定我们自定义的插件类名,如下:

7

至此,自定义Gradle插件就已经写完了,现在可以在AS右边栏找到Gradle中点击uploadArchives,执行plugin的部署任务:

8

可以看到构建成功之后,在Project的根目录下将会出现一个repo目录,里面存放的就是我们的插件目标文件。

3.测试asm_lifecycle_plugin

为了测试自定义的Gradle插件是否可用,可以在app module中的build.gradle中引用此插件。

9

图1处就是在自定义Gradle插件中properties的文件名

图2处dependencies中的classpath时group值+module名+version

然后在命令行中使用gradlew执行构建命令,如果打印出我们自定义插件中的log,则说明自定义Gradle插件可以使用

10

其实现在已经有一些比较成熟的三方Gradle插件,比如hiBeaver。如果不喜欢重头创建Gradle插件,可以考虑尝试使用。

4.自定义Transform,实现遍历.class文件

自定义Gradle插件已经写好了,接下来就需要实现遍历所有.class的逻辑。这部分功能主要依赖Transform API

1.什么是Transform?

Transform可以被看作是Gradle在编译项目时的一个task, 在.class文件转化成.dex的流程中会执行这些task ,对所有的.class文件(可包括第三方库的.class)进行转化, 转化的逻辑定义在Transform的transform方法中 。实际上平常我们在build.gradle中常用的功能都是通过Transform实现的,比如混淆(proguard),分包(multidex),jar包合并(jarMerge)

2.自定义Transform

danny.jiang.plugin 目录中,新建LifeCycleTransform.groovy,并继承Transform类。

11

可以看到,LifeCycleTransform需要实现抽象类Transform中的抽象方法,具体有如下几个方法需要实现:

12

解释说明:Transform主要作用是检索项目编译过程中的所有文件。通过这几个方法,我们可以对自定义Transform设置一些遍历规则,具体如下:

  1. getName:

    设置我们自定义的Transform对应的Task名称。Gradle在编译的时候,会将这个名称显示在控制台上。比如:Task:app:transformClassesWithXXXForDebug

  2. getInputType:

    在项目中会有各种各样格式的文件,通过getInputType可以设置LifeCycleTransform接收的文件类型,此方法返回的类型是Set集合

    ContentType有以下2种取值

    13

    1. CLASSES:代表只检索.class文件
    2. RESOURCES:代表检索java标准资源文件
  3. getScopes()

    这个方法规定自定义Transform检索的范围,具体有以下几种取值

    14

  4. isIncremental()

    表示当前Transform是否支持增量编译,我们不需要增量编译,所以直接返回false即可。

  5. transform()

    在自定义Transform中最重要的方法就是transform()。在这个方法中,可以获取到两个数据的流向

    • inputs:inputs中是传过来的输入流,其中有两种格式,一种是jar包格式,一种是directory(目录格式)
    • outputProvider:outputProvider获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做,否则编译会报错

我们可以实现一个简单LifeCycleTransform,功能是打印出所有.class文件。

15

解释说明:

  1. 自定义的Transform名称为LifeCycleTransform
  2. 检索项目中.class类型的目录或文件
  3. 设置当前Transform检索范围为当前目录
  4. 设置过滤文件为.class文件(去除文件夹类型),并打印文件名称

3.将自定义LifeCycleTransform注册到Gradle插件中

在LifeCyclePlugin中添加如下代码

16

再次在命令行中执行build命令,可以看到LifeCycleTransform检索出的所有.class文件

17

从图中可以看出,Gradle编译时多了一个我们自定义的LifeCycleTransform类型的任务,并且将所有.class文件名打印出来,其中包含了我们需要的目标文件MainActivity.class

5.使用ASM,插入字节码到Activity文件

ASM是一套开源框架,其中几个常用的API如下:

1.添加ASM依赖

在asm_lifecycle_plugin的build.gradle中,添加对ASM的依赖,如下

18

2.创建自定义ASM Visitor类

在asm_lifecycle_plugin module中的src/main/java目录下创建包danny.jiang.asm,并分别创建LifecycleClassVisitor.java和LifecycleMethodVisitor.java

19

红框中,在visitMethod方法中,过滤出继承自AppCompatActivity的文件,并在LifecycleMethodVisitor.java中对onCreate进行改造

20

图中红框内是真正执行插入字节码的逻辑。可以看出ASM都是直接以字节码指令的方式进行操作的,所以如果想使用ASM需要对字节码有一定的理解。也可以借助第三方工具ASM Bytecode Outline来生成想要的字节码。

3.修改LifeCycleTransform的transform方法,使用ASM

各种visitor都定义好后,我们就可以修改LifeCycleTransform的transform方法,并将需要插桩的字节码插入到MainActivity.class文件中

21

6.重新部署自定义Gradle插件,并运行主项目

接下来在点击uploadArchives重新部署LifeCyclePlugin。

注意:重新部署时,需要先在app module的build.gradle中将插件依赖注释,否则报错

部署成功后,重新在app中依赖自定义插件并运行主项目,当Activity被打开时,会在logcat中看到对应日志

22

虽然我们在MainActivity中并没有添加任何log日志逻辑,但是在编译期间,自定义的LifeCyclePlugin会自动为每一个Activity的onCreate方法中添加log日志逻辑。

就算我们在项目中打开了混淆,注入的字节码还会正常工作,因为混淆也是一个Transform,叫做ProguardTransform,它是在自定义的Transform之后执行。

总结

主要操作演示了一遍编译插桩的流程。涉及以下几个知识点:

Top
Foot