【笔记四】编译插桩操纵字节码,实现不可能完成的任务

根据之前学习的class 字节码文件的解析,接下来在此基础上进行一些更深入的操作,来加深对 class 字节码文件的理解。

相信做过 Android 开发的工程师大多都遇到过这种需求:

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

面对这样的需求,一般人都会想到,这其实就是在每一个 Activity 的 onCreate 和 onDestroy 方法中,分别添加页面打开和页面关闭的逻辑。常见的做法有以下三种:

  1. 修改项目中现有的每一个 Activity,这样显然不够高大上,并且如果项目以后需要添加新的页面,这套逻辑需要重新拷贝一遍,非常容易遗漏。
  2. 将项目中所有的 Activity 继承自 BaseActivity,将页面打开和关闭的逻辑添加在 BaseActivity中,这种方案看起来比第 1 种方案高级得多,并且后续项目中有新的 Activity,直接继承 BaseActivity 即可。但是这种方案对第三方依赖库中的界面则无能为力,因为我们没有第三方依赖库的源码。
  3. 在 Application 中调用registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback) 方法来注册一个 Activity 生命周期的监听回调,所有的 Activity 都会走该回调,我们可以在该回调中通过生命周期的方法来记录页面打开和关闭事件。

除了这三种方案,还有一种更加优雅更加完整的方案应运而生:编译插桩

1. 编译插桩是什么

顾名思义,所谓编译插桩就是在代码编译期间修改已有的代码或者生成新代码。实际上,我们项目中经常用到的 Dagger、ButterKnife 甚至是 Kotlin 语言,它们都用到了编译插桩的技术。

理解编译插桩之前,需要先回顾一下 Android 项目中 .java 文件的编译过程:

img

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

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

主要介绍第 2 种实现方式,用一张图来描述如下过程

img

一般情况下,我们经常会使用编译插桩实现如下几种功能:

  • 日志埋点;
  • 性能监控;
  • 动态权限控制;
  • 业务逻辑跳转时,校验是否已经登录;
  • 甚至是代码调试等。

2. 插桩工具介绍

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

AspectJ

AspectJ 是老牌 AOP(Aspect-Oriented Programming)框架,如果你做过 J2EE 开发可能对这个框架更加熟悉,经常会拿这个框架跟 Spring AOP 进行比较。其主要优势是成熟稳定,使用者也不需要对字节码文件有深入的理解。

ASM

目前另一种编译插桩的方式 ASM 越来越受到广大工程师的喜爱。通过 ASM 可以修改现有的字节码文件,也可以动态生成字节码文件,并且它是一款完全以字节码层面来操纵字节码并分析字节码的框架(此处可以联想一下写汇编代码时的酸爽)。

举个例子,在 Java 中如果实现两个数相加操作,可以如下实现:

public int add(){
  int a = 10;
  int b = 20;
  return a + b;
}

但是如果使用 ASM 直接编写字节码指令,则有可能是如下几个字节码指令:

img

虽然上面的代码看起来很恐怖,但是没必要太过担心,因为有各种工具帮我们生成这些字节码指令。

接下来就使用 ASM 来实现简单的编译插桩效果,通过插桩实现在每一个 Activity 打开时输出相应的 log 日志。

3. 插桩实现

3.1 实现思路

过程主要包含两步:

  1. 遍历项目中所有的 .class 文件
    如何找到项目中编译生成的所有 .class 文件,是我们需要解决的第一个问题。众所周知,Android Studio 使用 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 日志字节码

3.2 具体实现

  1. 创建 ASMLifeCycleDemo 项目
    创建主项目 ASMLifeCycleDemo,当前项目中只有一个 MainActivity

  2. 创建自定义 Gradle 插件
    首先在 ASMLifeCycleDemo 项目中创建一个新的 module,并选择 Android Library 类型,命名为 asm_lifecycle_plugin。
    将 asm_lifecycle_plugin module 中除了 build.gradle 和 main 文件夹之外的所有内容都删除。然后在 main 目录下分别创建 groovy 和 java 目录

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

然后,在 groovy 中创建目录 cn.onestravel.plugin,并在此目录中创建类 LifeCyclePlugin.groovy 文件。在 LifeCyclePlugin 中重写 apply 方法,实现插件逻辑,因为是 demo 演示,所以我只是简单的打印 log 日志。

package cn.onestravel.plugin

import org.gradle.api.Plugin
import org.gradle.api.Project

public class LifeCyclePlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        System.out.println("==LifeCyclePlugin ====")

    }
}

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

接下来,将 asm_lifecycle_plugin module 的 build.gradle 中的内容全部删掉,改为如下内容:

apply plugin: 'groovy'
apply plugin: 'maven'


dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation gradleApi()
    implementation localGroovy()

    implementation 'com.android.tools.build:gradle:3.4.2'

}

group='cn.onestravel.plugin'
version='1.0.0'

uploadArchives{
    repositories {
        mavenDeployer{
           repository(url:uri('../ams_lifecycle_repo'))
        }
    }
}

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

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

最后一步,创建 properties 文件。

在 plugin/src/main 目录下新建目录 resources/META-INF/gradle-plugins,然后在此目录下新建一个文件:ones.asm.lifecycle.properties,其中文件名 ones.asm.lifecycle 就是我们自定义插件的名称,稍后我们在 app module 中会使用到此名称。

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

implementation-class=cn.onestravel.plugin.LifeCyclePlugin

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

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

image-20200821150609739

  1. 测试 asm_lifecycle_plugin

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

    apply plugin: 'ones.asm.lifecycle'// 1. 自定义 Gradle 插件中 properties 的文件名
    buildscript{
        repositories {
            google()
            jcenter()
            maven {url '../ams_lifecycle_repo'}
        }
        dependencies {
            classpath 'cn.onestravel.plugin:asm_lifecycle_plugin:1.0.0' //2. group 值 + module 名 + version
        }
    }
    android {
      ...
    }
    dependencies{
      ...
    }

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

➜  asmlifecycledemo ./gradlew clean  assembleDebug
Starting a Gradle Daemon (subsequent builds will be faster)

> Configure project :asmlifecycledemo
==LifeCyclePlugin ====
------------------register  LifeCycleTransform


BUILD SUCCESSFUL in 31s
26 actionable tasks: 26 executed
  1. 自定义 Transform,实现遍历 .class 文件

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

什么是 Transform ?

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

自定义 Transform

在 cn.onestravel.plugin 目录中,新建 LifeCycleTransform.groovy,并继承 Transform 类。LifeCycleTransform 需要实现抽象类 Transform 中的抽象方法,具体有如下几个方法需要实现:

package cn.onestravel.plugin

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager

public class LifeCycleTransform extends Transform {

    @Override
    String getName() {
        return "LifeCycleTransform"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.PROJECT_ONLY
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
    }
}

getName

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

getInputType

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

ContentType 有以下 2 种取值。

/**
    * A content type that is requested through the transform API.
    */
   interface ContentType {

       /**
        * Content type name, readable by humans.
        * @return the string content type name
        */
       String name();

       /**
        * A unique value for a content type.
        */
       int getValue();
   }

   /**
    * The type of of the content.
    */
   enum DefaultContentType implements ContentType {
       /**
        * The content is compiled Java code. This can be in a Jar file or in a folder. If
        * in a folder, it is expected to in sub-folders matching package names.
        */
       CLASSES(0x01),

       /** The content is standard Java resources. */
       RESOURCES(0x02);

       private final int value;

       DefaultContentType(int value) {
           this.value = value;
       }

       @Override
       public int getValue() {
           return value;
       }
   }
  • CLASSES:代表只检索 .class 文件;
  • RESOURCES:代表检索 java 标准资源文件。

getScopes()

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

enum Scope implements ScopeType {
        /** Only the project (module) content */
        PROJECT(0x01),
        /** Only the sub-projects (other modules) */
        SUB_PROJECTS(0x04),
        /** Only the external libraries */
        EXTERNAL_LIBRARIES(0x10),
        /** Code that is being tested by the current variant, including dependencies */
        TESTED_CODE(0x20),
        /** Local or remote dependencies that are provided-only */
        PROVIDED_ONLY(0x40),

        /**
         * Only the project's local dependencies (local jars)
         *
         * @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}
         */
        @Deprecated
        PROJECT_LOCAL_DEPS(0x02),
        /**
         * Only the sub-projects's local dependencies (local jars).
         *
         * @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}
         */
        @Deprecated
        SUB_PROJECTS_LOCAL_DEPS(0x08);

        private final int value;

        Scope(int value) {
            this.value = value;
        }

        @Override
        public int getValue() {
            return value;
        }
    }

**isIncremental() **

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

transform()

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

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

我们可以实现一个简易 LifeCycleTransform,功能是打印出所有 .class 文件。代码如下:

package cn.onestravel.plugin

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager

public class LifeCycleTransform extends Transform {

    @Override
    String getName() {
      //1. 自定义的 Transform 名称为 LifeCycleTransform;
        return "LifeCycleTransform"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
      //2. 检索项目中 .class 类型的目录或者文件;
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
      //3. 设置当前 Transform 检索范围为当前项目;
        return TransformManager.PROJECT_ONLY
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
        Collection<TransformInput> inputs = transformInvocation.inputs
        inputs.each { TransformInput transformInput ->
            transformInput.directoryInputs.each { DirectoryInput directoryInput ->
                File dir = directoryInput.file
                if (dir) {
                  //4. 设置过滤文件为 .class 文件(去除文件夹类型),并打印文件名称。
                    Map<String, Object> options = new HashMap<>()
                    options.put("type", groovy.io.FileType.FILES)
                    options.put("nameFilter", ~/.*\.class/)
                    dir.traverse(options) { file ->
                        System.out.println(file.name)
                    }
                }
            }
        }
    }
}

解释说明:

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

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

在 LifeCyclePlugin 中添加如下代码:

package cn.onestravel.plugin

import com.android.build.gradle.AppExtension;
import org.gradle.api.Plugin
import org.gradle.api.Project

public class LifeCyclePlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        System.out.println("==LifeCyclePlugin ====")
        
        
        def android = project.extensions.getByType(AppExtension)
        println('------------------register  LifeCycleTransform')
        LifeCycleTransform transform = new LifeCycleTransform()
        android.registerTransform(transform)

    }
}

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

➜  asmlifecycledemo ./gradlew clean assembleDebug
Starting a Gradle Daemon (subsequent builds will be faster)

> Configure project :asmlifecycledemo
==LifeCyclePlugin ====
------------------register  LifeCycleTransform

> Task :asmlifecycledemo:transformClassesWithLifeCycleTransformForDebug
BuildConfig.class
MainActivity.class

BUILD SUCCESSFUL in 31s
26 actionable tasks: 26 executed

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

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

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

  • ClassReader:负责解析 .class 文件中的字节码,并将所有字节码传递给 ClassWriter。
  • ClassVisitor:负责访问 .class 文件中各个元素,还记得上一课时我们介绍的 .class 文件结构吗?ClassVisitor 就是用来解析这些文件结构的,当解析到某些特定结构时(比如类变量、方法),它会自动调用内部相应的 FieldVisitor 或者 MethodVisitor 的方法,进一步解析或者修改 .class 文件内容。
  • ClassWriter:继承自 ClassVisitor,它是生成字节码的工具类,负责将修改后的字节码输出为 byte 数组。

3.3.1 添加 ASM 依赖

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

apply plugin: 'groovy'
apply plugin: 'maven'


dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation gradleApi()
    implementation localGroovy()

    implementation 'com.android.tools.build:gradle:3.4.2'

    //ASM 相关依赖
    implementation 'org.ow2.asm:asm:7.1'
    implementation 'org.ow2.asm:asm-commons:7.1'

}

group='cn.onestravel.plugin'
version='1.0.0'

uploadArchives{
    repositories {
        mavenDeployer{
           repository(url:uri('../ams_lifecycle_repo'))
        }
    }
}