热修复之自定义热修复框架雏形(三)

在上篇最后提出了两个问题:

  1. 在 Android Studio 中,.java 文件 —-> .class 文件 —-> .dex 文件都是自动化的过程,如何在 .class 文件 —-> .dex 文件的过程中修改 .class 文件呢,可以借助
    gradle 构建脚本实现
  2. 如何修改类的字节码文件,在字节码层面的类中的构造方法中插入对其他 dex 文件中的类的引用,可以使用 javaassist 工具实现

好,接下来就通过实例介绍如何解决上面两个问题



1. 自定义 Gradle Plugin

1.1 Gradle Plugin 概述

说起 Gradle Plugin 插件,其实在 Android Studio 中很常见。创建完一个新工程之后,在 /app/build.gradle 中就有对 Gradle Plugin 的引用,如下图所示:



Gradle Plugin 是采用 groovy 语言编写的,groovy 是基于 JVM 的敏捷脚本语言,groovy 最后会被编译为标准的 java 类字节码文件,然后通过 JVM 加载这个字节码文件并执行。groovy 最核心的应该是闭包,Java8 中引入的 Lambda 表达式即是一个闭包,更多关于 groovy 语言的知识请参考相关资料 Groovy 使用完全解析

共有三种方法自定义的 Gradle Plugin:

  1. 直接在构建文件 build.gralde 中编写 Plugin,使用这种方式自定义的 Plugin,无法在其他构建文件中使用
  2. 单独编写 Gradle Plugin 文件,放在 rootProjectPath/buildSrc/src/main/groovy/ 目录下,同一工程中其他所有的构建文件即可使用这个 Plugin,但是其他工程无法使用此 Plugin
  3. 单独的工程中自定义的 Gradle Plugin 文件,可以上传到远程的 maven 仓库,其他工程通过添加对这个 Plugin 的依赖即可使用这个插件

1.2 自定义 Gradle Plugin 步骤详解

在本篇文章中使用第二种方式创建 Gradle Plugin 插件,创建步骤有以下几步:

  1. 创建一个 Module,Module 类型随便选一个即可(无论是 Android Library 或者 Java Library 都行),并将该 Module 命名为 buildSrc

    注意: 该 Module 必须命名为 buidlSrc

  2. buildSrc 目录下的所有文件删除,除了 src 目录下的文件和 build.gradle 文件

  3. buildSrc/src/main/ 目录下创建 groovy 文件,并将 buildSrc/src/main/ 目录下的 java 文件夹删除
  4. buildSrc/src/main/groovy/ 文件下创建包文件,如 com.lijiankun24.plugin,这样在 buildSrc/src/main/groovy/com/lijiankun24/plugin/ 目录下创建 groovy 文件了
  5. 修改 buildSrc/build.gradle 文件中的内容,如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    apply plugin: 'groovy'
    repositories {
    jcenter()
    }
    dependencies {
    compile gradleApi()
    compile 'com.android.tools.build:gradle:2.3.3'
    compile 'org.javassist:javassist:3.20.0-GA'
    }
  6. buildSrc/src/main/groovy/com/lijiankun24/plugin/ 目录下,创建 groovy 文件,如下图所示:



    注意创建 groovy 时,填写文件名称时,需要带有 .groovy 扩展名,比如创建一个 Hotfix.groovy 文件

  7. 创建 Hotfix.groovy 文件,并添加如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package com.lijiankun24.plugin
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    import org.gradle.api.logging.LogLevel
    /**
    * Hotfix.java
    * <p>
    * Created by lijiankun on 18/4/14.
    */
    public class Hotfix implements Plugin<Project> {
    @Override
    public void apply(Project project) {
    project.logger.error("====== Hello Gradle Plugin =======")
    }
    }
  8. app/build.gradle 文件中添加对此插件的依赖,并 Sync Project with Gradle Files,在 Gradle Console 中即可即可看到输出的信息,如下图所示:



这样,一个自定义的 Gradle Plugin 即完成了

1.3 Gradle Plugin 的使用

在上面创建的 Hotfix.groovy 文件中的内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.lijiankun24.plugin
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* Hotfix.java
* <p>
* Created by lijiankun on 18/4/14.
*/
public class Hotfix implements Plugin<Project> {
@Override
public void apply(Project project) {
project.logger.error("====== Hello Gradle Plugin =======")
}
}

Hotfix 插件是实现了 org.gradle.api.Plugin 接口,并且重写了其中的 apply(Project project) 方法,那 org.gradle.api.Project 接口是什么呢?

根据 Gradle官网 的介绍,Project 是构建文件和 Gradle 文件交互的主要 API,通过 Project 接口的对象,可以动态地获取 Gradle 的属性。一句话就是,在构建文件中是通过 Project API 获取 Gradle 属性的。

比如,app/build.gradle 的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
android {
compileSdkVersion 24
buildToolsVersion "24.0.0"
defaultConfig {
applicationId "com.hc.hcplugin"
minSdkVersion 15
targetSdkVersion 24
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

其中的 compileSdkVersiondefaultConfig 等属性就是通过 Project 接口的 Extension 获取得到的。

下面,通过自定义一个 Extension 更深入的理解一下 Project

  1. buildSrc/src/main/groovy/com/lijiankun24/plugin/ 目录下定义两个类,如下所示:

    1
    2
    3
    class SExtension {
    String myName = null;
    }
    1
    2
    3
    4
    class Student {
    String name=null
    String phone=null
    }
  2. 修改 Hotfix.groovy 中的内容,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    package com.lijiankun24.plugin
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    /**
    * Hotfix.java
    * <p>
    * Created by lijiankun on 18/4/14.
    */
    public class Hotfix implements Plugin<Project> {
    @Override
    public void apply(Project project) {
    project.logger.error("================ Hello Gradle Plugin ==========")
    // 创建一个 SExtension 的对象
    project.extensions.create('se', SExtension)
    // 创建一个 Student 的对象
    project.extensions.create('student', Student)
    // 定义一个 task 读取 build.gradle 文件中 'se' 和 'studeng' 的属性的内容
    project.task('readExtension') << {
    def se = project['se']
    def student = project['student']
    println "The SExtension's myName is " + se.myName
    println "The student's name is " + student.name
    println "The student's phone is " + student.phone
    }
    }
    }
  3. app/build.gradle 文件中添加对 com.lijiankun24.plugin.Hotfix 引用,并添加对 sesdutent 属性的定义,如下所示:



通过上面这个例子,相信对自定义 Gradle Plugin 有了更加深刻的理解。

2. 自定义 Transform

2.1 Transform 概述

从 Android Gradle 1.5.0-beta1 开始,引入了 com.android.build.api.transform.Transform,其实 Transform 也是一个 task,每新建一个 Transform,都有一个新的 task 和它对应。

在 .java 文件 —-> .class 文件 —-> .dex 文件的过程中,是通过一个一个的 task 执行完成其中的每一个步骤的。Trasnform 注册之后,其执行的时机是在项目被打包成 dex 文件之前,正是操纵字节码的时机,多个 Transform 之间是串行执行的,执行流程如下图所示:



图片来源:通过自定义Gradle插件修改编译后的class文件

每一个 Transform 都一个输入(input) 和一个输出(output),上一个 Transform 的输出(output)是下一个 Transform 的输入(input)。比如:一个 Transform 的作用是将 .java 文件编译成 .class 文件,那么该 Transform 的输入就是 .java 文件的保存目录,而它的输出就是 .class 文件的保存目录,该 Transform 的下一个 Transform 的输入就是 .class 文件的保存目录。

2.2 自定义 Transform

接下来,我们则自定义一个 Transform

  1. Transformcom.android.build.api.transform.Transform 包下的,所以需要导入 com.android.tools.build:gradle 包才可以

    注意:Transformcom.android.tools.build:gradle:1.5.0-beta1 之后才引入的,所以 com.android.tools.build:gradle 版本需要 1.5.0-beta1 之上才可以

  2. 定义一个 PreDex,代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    package com.lijiankun24.plugin
    import com.android.build.api.transform.*
    import com.android.build.gradle.internal.pipeline.TransformManager
    import org.gradle.api.Project
    /**
    * PreDex.java
    * <p>
    * Created by lijiankun on 18/4/14.
    */
    public class PreDex extends Transform {
    Project project
    // 添加构造,为了方便从plugin中拿到project对象,待会有用
    public PreDexTransform(Project project) {
    this.project = project
    }
    // Transfrom在Task列表中的名字
    // TransfromClassesWithPreDexForXXXX
    @Override
    String getName() {
    return "preDex"
    }
    // 指定input的类型
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
    return TransformManager.CONTENT_CLASS
    }
    // 指定Transfrom的作用范围
    @Override
    Set<QualifiedContent.Scope> getScopes() {
    return TransformManager.SCOPE_FULL_PROJECT
    }
    @Override
    boolean isIncremental() {
    return false
    }
    @Override
    void transform(Context context, Collection<TransformInput> inputs,
    Collection<TransformInput> referencedInputs,
    TransformOutputProvider outputProvider, boolean isIncremental)
    throws IOException, TransformException, InterruptedException {
    // inputs就是输入文件的集合
    // outputProvider可以获取outputs的路径
    }
    }
  3. Hotfix.groovy 中注册 PreDex.groovy Transform,代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    public class Hotfix implements Plugin<Project> {
    @Override
    public void apply(Project project) {
    project.logger.error("================ Hello Gradle Plugin ==========")
    def android = project.extensions.findByType(AppExtension.class)
    android.registerTransform(new PreDex(project))
    }
    }
  4. 此时运行一下项目,会报如下的错误:



    原因是:PreDex 中的方法 void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) 还没有实现,PreDex 的下一个 Transform 接收到的 input 是空的。

    实现 void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) 方法,将 input 的文件复制到 output 的路径下即可,代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // Transfrom的inputs有两种类型,一种是目录,一种是jar包,要分开遍历
    inputs.each {TransformInput input ->
    input.directoryInputs.each {DirectoryInput directoryInput->
    //TODO 这里可以对input的文件做处理,比如代码注入!
    // 获取output目录
    def dest = outputProvider.getContentLocation(directoryInput.name,
    directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
    // 将input的目录复制到output指定目录
    FileUtils.copyDirectory(directoryInput.file, dest)
    }
    }
  5. 查看 Transform 的输入(input)和输出(output)。在 app module 的 build.gradle 文件中的 android{} 下添加如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    applicationVariants.all { variant->
    def dexTask = project.tasks.findByName("transformClassesWithDexForDebug")
    def preDexTask = project.tasks.findByName("transformClassesWithPreDexForDebug")
    if(preDexTask) {
    project.logger.error "======preDexTask======"
    preDexTask.inputs.files.files.each {file ->
    project.logger.error "inputs =$file.absolutePath"
    }
    preDexTask.outputs.files.files.each {file ->
    project.logger.error "outputs =$file.absolutePath"
    }
    }
    if(dexTask) {
    project.logger.error "======dexTask======"
    dexTask.inputs.files.files.each {file ->
    project.logger.error "inputs =$file.absolutePath"
    }
    dexTask.outputs.files.files.each {file ->
    project.logger.error "outputs =$file.absolutePath"
    }
    }
    }

    输出如下所示:



3. 修改 .class 字节码文件

按照上一篇博客最后的结论,需要在一个类的构造方法中引用另外一个 .dex 文件中的类,这个类则不会被打上 CLASS_ISPREVERIFIED 的标志,这个类就可以通过之前介绍的热修复方式修复了,接下来就介绍如何通过 Gradle Plugin 和 Transform 实现修改 .class 字节码文件。

假设在 classes.dex 中的每一个类的构造方法中添加一句打印语句 System.out.println(AntilazyLoad.class);,而 AntilazyLoad.class 则是另一个 hack.dex 文件中的类,这样 classes.dex 文件中的类就不会被打上 CLASS_ISPREVERIFIED 标志了。

3.1 定义 Gradle Plugin

按照第一节中的方式定义一个 Hotfix.groovy 插件,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.lijiankun24.plugin
import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* Hotfix.java
* <p>
* Created by lijiankun on 18/4/14.
*/
public class Hotfix implements Plugin<Project> {
@Override
public void apply(Project project) {
project.logger.error("================ Hello Gradle Plugin ==========")
}
}

3.2 定义 Transform

按照第二节介绍的方式定义一个名为 PreDex 的 Transform,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package com.lijiankun24.plugin
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.gradle.api.Project
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
/**
* PreDex.java
* <p>
* Created by lijiankun on 18/4/14.
*/
public class PreDex extends Transform {
Project project
// 添加构造,为了方便从plugin中拿到project对象,待会有用
public PreDex(Project project) {
this.project = project
}
// Transfrom在Task列表中的名字
// TransfromClassesWithPreDexForXXXX
@Override
String getName() {
return "preDex"
}
// 指定input的类型
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
// 指定Transfrom的作用范围
@Override
Set<QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(Context context, Collection<TransformInput> inputs,
Collection<TransformInput> referencedInputs,
TransformOutputProvider outputProvider, boolean isIncremental)
throws IOException, TransformException, InterruptedException {
// inputs就是输入文件的集合
// outputProvider可以获取outputs的路径
// Transfrom的inputs有两种类型,一种是目录,一种是jar包,要分开遍历
inputs.each {TransformInput input ->
input.directoryInputs.each {DirectoryInput directoryInput->
//TODO 这里可以对input的文件做处理,比如代码注入!
// 获取output目录
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
// 将input的目录复制到output指定目录
FileUtils.copyDirectory(directoryInput.file, dest)
}
}
}
}

3.3 制作 hack.jar

  1. 创建一个名为 hack 的 Module,并创建一个 AntilazyLoad.java,如下所示:



  2. AntilazyLoad.class 拷贝到同包名的文件下,并通过如下命令生成 hack.jar 文件

    1
    2
    jar -cvf hackTemp.jar com
    dx --dex --output=hack.jar hackTemp.jar


  3. hack.jar 包放到 app module 中的 assets 文件夹中

3.4 加载 hack.jar

在应用启动的时候,首先需要将 hack.jar 加载到 ClassLoader 中,否则在 classes.dex 中的类的构造方法中引用的 AntilazyLoad.class 就会找不到,加载 hack.jar 的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 将 /assets/hack.jar 拷贝至 App 私有目录下
File hackDexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hack.jar");
Utils.prepareDex(this.getApplicationContext(), hackDexPath, "hack.jar");
// 加载 hack.jar,避免 CLASS_ISPREVERIFIED 异常
if (hackDexPath.exists()) {
inject(hackDexPath.getAbsolutePath());
}
// 获取补丁,如果存在就执行注入操作
String dexPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/patch_dex.jar");
File file = new File(dexPath);
if (file.exists()) {
inject(dexPath);
} else {
Log.e("BugFixApplication", dexPath + "不存在");
}
}
......
}

3.5 修改 .class 文件

在 Android Studio 编译过程中,app module 编译后的 .class 文件保存在 debug 目录下,直接遍历这个目录,使用 javassist 注入 System.out.println(AntilazyLoad.class); 代码即可。

  1. 专门写一个使用 javassist 操作 .class 文件的 Inject.groovy

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    package com.lijiankun24.plugin
    import javassist.ClassPool
    import javassist.CtClass
    import javassist.CtConstructor
    import org.apache.commons.io.FileUtils
    /**
    * Created by AItsuki on 2016/4/7.
    * 注入代码需要遍历里面的 .class 进行注入
    */
    public class Inject {
    private static ClassPool pool= ClassPool.getDefault()
    /**
    * 添加classPath到ClassPool
    * @param libPath
    */
    public static void appendClassPath(String libPath) {
    pool.appendClassPath(libPath)
    }
    /**
    * 遍历该目录下的所有 .class,对所有 .class 进行代码注入。
    * 其中以下class是不需要注入代码的:
    * --- 1. R文件相关
    * --- 2. 配置文件相关(BuildConfig)
    * --- 3. Application
    * @param path 目录的路径
    */
    public static void injectDir(String path) {
    pool.appendClassPath(path)
    File dir = new File(path)
    if (dir.isDirectory()) {
    dir.eachFileRecurse { File file ->
    String filePath = file.absolutePath
    if (filePath.endsWith(".class")
    && !filePath.contains('R$')
    && !filePath.contains('R.class')
    && !filePath.contains("BuildConfig.class")
    // 这里是application的名字,可以通过解析清单文件获得,先写死了
    && !filePath.contains("HotPatchApplication.class")) {
    // 这里是应用包名,也能从清单文件中获取,先写死
    int index = filePath.indexOf("com\\aitsuki\\hotpatchdemo")
    if (index != -1) {
    int end = filePath.length() - 6 // .class = 6
    String className = filePath.substring(index, end).replace('\\', '.').replace('/', '.')
    injectClass(className, path)
    }
    }
    }
    }
    }
    private static void injectClass(String className, String path) {
    CtClass c = pool.getCtClass(className)
    if (c.isFrozen()) {
    c.defrost()
    }
    CtConstructor[] cts = c.getDeclaredConstructors()
    if (cts == null || cts.length == 0) {
    insertNewConstructor(c)
    } else {
    cts[0].insertBeforeBody("System.out.println(com.lijiankun24.hack.AntilazyLoad.class);")
    }
    c.writeFile(path)
    c.detach()
    }
    private static void insertNewConstructor(CtClass c) {
    CtConstructor constructor = new CtConstructor(new CtClass[0], c)
    constructor.insertBeforeBody("System.out.println(com.lijiankun24.hack.AntilazyLoad.class);")
    c.addConstructor(constructor)
    }
    }
  2. PreDex.groovy 中使用 Inject.groovy 修改 .class 字节码文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @Override
    void transform(Context context, Collection<TransformInput> inputs,
    Collection<TransformInput> referencedInputs,
    TransformOutputProvider outputProvider, boolean isIncremental)
    throws IOException, TransformException, InterruptedException {
    // inputs就是输入文件的集合
    // outputProvider可以获取outputs的路径
    inputs.each {TransformInput input ->
    input.directoryInputs.each {DirectoryInput directoryInput->
    // 注入代码
    Inject.injectDir(directoryInput.file.absolutePath)
    def dest = outputProvider.getContentLocation(directoryInput.name,
    directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
    // 将input的目录复制到output指定目录
    FileUtils.copyDirectory(directoryInput.file, dest)
    }
    }
    }

好了,这篇文章也算是完结了。经过这三篇文章的介绍,一个简单的自定义热修复框架已经基本完成,虽然还存在一些其他的问题,比如资源的热修复、so 库的热修复、混淆等还没有考虑,但是热修复的基本原理就是这样的。

接下来会分析一下 Tinker 的源码,看看 Tinker 是如何实现热修复的。


参考资料:

Android热补丁动态修复技术(三)—— 使用Javassist注入字节码,完成热补丁框架雏形(可使用)Altsuki的博客

Android 热补丁动态修复框架小结Hongyang

通过自定义Gradle插件修改编译后的class文件huachao1001

Transform 官方文档

Gradle自定义插件详解wangqiubo2010的博客

在AndroidStudio中自定义Gradle插件huachao1001

DexClassLoader热修复的入门到放弃cuieney