基于QZone dex分包技术的热修复插件详解

1,105 阅读14分钟

1 QZone 热修复简介

  • 关键词重启生效、反射、类加载
  • 简介:QQ空间基于的是 dex 分包方案。把 Bug 方法修复以后,放到一个单独的 dex 补丁文件,让程序运行期间加载 dex 补丁,执行修复后的方法。
  • 原理:如何做到简介中的描述?在 Android 中所有我们运行期间需要的类都是由 ClassLoader (类加载器)进行加载。因此让 ClassLoader 加载全新的类替换掉出现 Bug 的类即可完成热修复。

QZone dex分包修复原理

2 QZone dex 分包实现

2.1 Android 类加载机制

我们知道 Java 的类加载采用双亲委托的加载机制,有效防止类的重复加载和保护核心类的加载。Android 的加载机制与 Java 的类加载机制类似,但也有区别。Android 中的各个类加载器之间的关系如下图所示。

Android类加载机制

2.2 寻找 Element 数组

当系统通过 PathClassLoader 去加载应用程序的 dex 文件中 Java 类时,PathClassLoader 并没有重写 loadClass() 方法,所以接下来由 PathClassLoader 的父类 BaseDexClassLoader 尝试执行加载任务,然而 BaseDexClassLoader 也没有重写 loadClass() 方法,则依次向上调用父类加载器的 loadClass() 方法,父类加载器(由最顶级父类加载器开始尝试 loadClass )在各自的加载范围内尝试加载需要的类,失败之后依次向下调用子类加载器的 findClass() 方法(也就是双亲委托机制那一套)。最终会调用到 BaseDexClassLoader 的 findClass() 方法

// PathClassLoader 去掉注释后的全部代码,只有两个构造函数
package dalvik.system;
public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}
public class BaseDexClassLoader extends ClassLoader {
    // ...
	private final DexPathList pathList;
	// ...
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
    	List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    	Class c = pathList.findClass(name, suppressedExceptions);
    	if (c == null) {
        	ClassNotFoundException cnfe = new ClassNotFoundException(
            	"Didn't find class \"" + name + "\" on path: " + pathList);
        	for (Throwable t : suppressedExceptions) {
            	cnfe.addSuppressed(t);
        	}
        	throw cnfe;
    	}
    	return c;
	}
    // ...
}

在该方法内,程序会调用 BaseDexClassLoader 类中私有成员属性 DexPathList pathList 的 findClass() 方法

final class DexPathList {
    // ...
    private Element[] dexElements;
    // ...
    public Class<?> findClass(String name, List<Throwable> suppressed) {
    	for (Element element : dexElements) {
        	Class<?> clazz = element.findClass(name, definingContext, suppressed);
        	if (clazz != null) {
            	return clazz;
        	}
    	}
    	if (dexElementsSuppressedExceptions != null) {
        	suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    	}
    	return null;
    }
    // ...
}

在这段代码中,用增强 for 循环从前往后去遍历 dexElements 数组,该数组即为上述图片中的提及的 Element 数组。

Android类查找流程

2.3 反射插入 patch.dex

找到了该 Element 数组,接下来我们思考如何将 patch.dex 插入到该数组的最前面,实现对待修复类的覆盖。**

注意!因为 dexElements 是数组,所以不能直接插入,更不能直接把第一个元素替换掉。QZone 给出的解决方案是直接反射得到 pathList ,再反射替换掉 dexElements 属性的值。

  • 第一步,反射得到 BaseDexClassLoader 的 pathList 字段的值;

在 Android 的环境中,我们通过 Context.getClassLoader() 得到的是 PathClassLoader 类加载器,而 pathList 是其父类的私有属性,所以直接 getField() 或者 getDeclaredField() 无法达到目的。

复习:getField() 方法能获取自己和父类的所有 public 字段;getDeclaredField() 方法能获取自己的所有字段

public static Field findField(Object instance, String name) throws NoSuchFieldException {
    for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
        try {
            Field field = clazz.getDeclaredField(name);
            if (!field.isAccessible()) {
                field.setAccessible(true);
            }
            return field;
        } catch (NoSuchFieldException e) {
            // ...
        }
    }
    throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
}

该方法中,我们首先获取到 PathClassLoader 类加载器的运行时类,然后尝试获取 pathList 属性,如果获取成功,直接返回该 pathList ;否则查找其父类的 pathList 属性,直到查找完顶级类加载器,还没有找到则抛出异常 NoSuchFieldException() 。

如下调用将获得 pathList 字段,并获得 BaseDexClassLoader 实例的 DexPathList pathList 的值。

Field pathListField = ShareReflectUtil.findField(loader, "pathList");//获得字段
Object dexPathList = pathListField.get(loader);//获得具体 BaseClassLoader 实例的该字段的值
  • 第二步,构造包含补丁 patch.dex 的 Element 数组;

Element 是 DexPathList 类的内部类,要得到 Element[] ,DexPathList 提供了具体的工厂方法;需要注意的是,不同的 Android 版本,该工厂方法有些不同,具体如下表。显然,我们可以分成三段进行版本兼容处理:

API 构造 Element[] 形参列表
28 (9.0 Pie) makePathElements List<File> files,
File optimizedDirectory,
List<IOException> suppressedExceptions
26 (8.0 Oreo) makePathElements List<File> files,
File optimizedDirectory,
List<IOException> suppressedExceptions
24 (7.0 Nougat) makePathElements List<File> files,
File optimizedDirectory,
List<IOException> suppressedExceptions
23 (6.0 Marshmallow) makePathElements List<File> files,
File optimizedDirectory,
List<IOException> suppressedExceptions
22 (5.1 Lollipop) makeDexElements ArrayList<File> files,
File optimizedDirectory,
ArrayList<IOException> suppressedExceptions
19 (4.4 KitKat) makeDexElements ArrayList<File> files,
File optimizedDirectory,
ArrayList<IOException> suppressedExceptions
18 (4.3 JellyBean) makeDexElements ArrayList<File> files,
File optimizedDirectory
14 (4.0 ICS) makeDexElements ArrayList<File> files,
File optimizedDirectory

★API Level 属于 [23 , ?]

final class DexPathList {
    // ...
    @SuppressWarnings("unused")
	private static Element[] makePathElements(
    	List<File> files,
    	File optimizedDirectory,
    	List<IOException> suppressedExceptions
	)
	{
        return makeDexElements(files, optimizedDirectory, suppressedExceptions, null);
	}// Dart Style
    // ...
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    //准备构造 Element[] 的传入参数
    File hackFile = initHack(context);
    List<File> files = new ArrayList<>();
    if (patch.exists()) {
        files.add(patch);
    }
    files.add(hackFile);
    File dexOptDir = context.getCacheDir();
    ArrayList<IOException> suppressedExceptions = new ArrayList<>();
    //反射执行私有的 makePathElements() 方法,得到 Element[] fixs
    try {
        Class clz = dexPathList.getClass();
        Method makePathElements_Mtd = clz.getDeclaredMethod(
            "makePathElements",
            List.class,
            File.class,
            List.class
        );// Dart Style
        makePathElements_Mtd.setAccessible(true);// 因为方法是私有的
        Object[] fixs = (Object[])makePathElements_Mtd.invoke(
            dexPathList,
            files,
            dexOptDir,
            suppressedExceptions
        )// Dart Style
    } catch (Exception e) {
        e.printStackTrace();
    }
}

★API Level 属于 [19 , 22]

final class DexPathList {
    // ...
    private static Element[] makeDexElements(
        ArrayList<File> files,
        File optimizedDirectory,
        ArrayList<IOException> suppressedExceptions
    ) 
    {
        // ...
    }// Dart Style
    // ...
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    //准备构造 Element[] 的传入参数
    File hackFile = initHack(context);
    ArrayList<File> files = new ArrayList<>();
    if (patch.exists()) {
        files.add(patch);
    }
    files.add(hackFile);
    File dexOptDir = context.getCacheDir();
    ArrayList<IOException> suppressedExceptions = new ArrayList<>();
    //反射执行私有的 makeDexElements() 方法,得到 Element[] fixs
    try {
        Class clz = dexPathList.getClass();
        Method makeDexElements_Mtd = clz.getDeclaredMethod(
            "makeDexElements",
            ArrayList.class,
            File.class,
            ArrayList.class
        );// Dart Style
        makeDexElements_Mtd.setAccessible(true);// 因为方法是私有的
        Object[] fixs = (Object[])makeDexElements_Mtd.invoke(
            dexPathList,
            files,
            dexOptDir,
            suppressedExceptions
        )// Dart Style
    } catch (Exception e) {
        e.printStackTrace();
    }
}

★API Level 属于 [14 , 18]

final class DexPathList {
    // ...
    private static Element[] makeDexElements(
        ArrayList<File> files,
        File optimizedDirectory
    ) 
    {
        // ...
    }//Dart Style
    // ...
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
    //准备构造 Element[] 的传入参数
    File hackFile = initHack(context);
    ArrayList<File> files = new ArrayList<>();
    if (patch.exists()) {
        files.add(patch);
    }
    files.add(hackFile);
    File dexOptDir = context.getCacheDir();
    //反射执行私有的 makeDexElements() 方法,得到 Element[] fixs
    try {
        Class clz = dexPathList.getClass();
        Method makeDexElements_Mtd = clz.getDeclaredMethod(
            "makeDexElements",
            ArrayList.class,
            File.class,
        );// Dart Style
        makeDexElements_Mtd.setAccessible(true);// 因为方法是私有的
        Object[] fixs = (Object[])makeDexElements_Mtd.invoke(
            dexPathList,
            files,
            dexOptDir
        )// Dart Style
    } catch (Exception e) {
        e.printStackTrace();
    }
}

★API Level 小于 14 的,该方法不适用

经过版本兼容处理,得到的 Object[] fixs 数组即为构造完成的包含补丁文件的 Element 数组

  • 第三步,获取到 DexPathList 的实例 dexPathList 后,反射获取 Element 数组字段 dexElements 和字段的值;

  • 第四步,并新建一个长度为 old.length + fixs.length 的 Element 数组,用于进行合并;

  • 第五步,先拷贝新数组,即包含 patch.dex 的数组,需要放数组前面,实现覆盖;

  • 第六步,再拷贝原有的数组元素;

  • 第七步,反射设置 dexElements 的值为新和成的数组,完成。

//@param
//Object instance 是第一步获得的 BaseDexClassLoader 实例的 pathList 字段的值,即 DexPathList 实例
//String fieldName 需要修改的 Element[] 字段的名字 —— "dexElements"
//Object[] fixs 已经构造好的、包含 patch.dex 的 Element 数组
public static void expandFieldArray(Object instance, String fieldName, Object[] fixs)
            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
    //第三步:调用上面的 findField() 方法,得到中的 dexElements 数组
    Field jlrField = findField(instance, fieldName);
    //第三步:old Element[]
    Object[] old = (Object[]) jlrField.get(instance);
    //第四步:构建合并后的数组
    Object[] newElements = (Object[]) Array.newInstance(old.getClass().getComponentType(),
                                                        old.length + fixs.length);
    //第五步:先拷贝新数组,即包含 patch.dex 的数组,需要放数组前面,实现覆盖
    System.arraycopy(fixs, 0, newElements, 0, fixs.length);
    //第六步:再拷贝原有的数组元素
    System.arraycopy(old, 0, newElements, fixs.length, old.length);
    //第七步:反射修改 pathList 的 dexElements 为新和成的 Element 数组
    jlrField.set(instance, newElements);
}

3 兼容处理

3.1 兼容 Android 19 (4.4)

3.1.1 Dalvik 虚拟机 dex 包校验

虚拟机在安装期间,会判断 Java 类的所有引用是否与自己处于同一个 dex 包内。如果某个类的所有引用与该类处于同一个 dex 包内,则该类会被打上 CLASS_ISPREVERIFIED 标记。倘若被标记的类引用了出现 Bug 的类,将无法直接对该类进行 QZone dex 分包热修复:

CLASS_ISPREVERIFIED = class is pre-verified

CLASS_ISPREVERIFIED

否则将会报错:

报错

原因在于 APK 在安装的时候图中 MainActivity.class 经过校验,被打上了 CLASS_ISPREVERIFIED 标记,这告诉虚拟机 MainActivity.class 不会引用其他 dex 包中的类;现又用另一个 patch.dex 包内的修复类覆盖了原 classes.dex 包中的 Bug 类,MainActivity.class 必须引用 patch.dex 包中的类,出现矛盾,报错。

因此,为了保证 QZone dex 分包热修复的可行性,对于使用 Dalvik 虚拟机的版本(API <= 19 (4.4)),我们需要进行兼容处理,防止类被打上 CLASS_ISPREVERIFIED 标记。

引自 安卓 App 热补丁动态修复技术介绍 by johncz from QQ空间开发团队

虚拟机在安装期间为类打上CLASS_ISPREVERIFIED标志是为了提高性能的,我们强制防止类被打上标志是否会影响性能?这里我们会做一下更加详细的性能测试.但是在大项目中拆分dex的问题已经比较严重,很多类都没有被打上这个标志。

3.1.2 ★字节码插桩★

显然,只要 Java 类引用了另外一个 dex 包的类,就可以防止该类被打上 CLASS_ISPREVERIFIED 标记。接下来我们分步骤来完成这个工作。

  • 第一步,AS 新建一个模块 patch-hack ,在 src/main/java/ 中写一个空类 packageName/AntilazyLoad.java ,专门用于被引用;
public class AntilazyLoad {
    
}
  • 第二步,Make Module ‘patch-hack’ 生成 AntilazyLoad.class;
  • 第三步,终端执行 dx 命令 ,将 AntilazyLoad.class 文件封装进 hack.jar 文件;
dx --dex --output=hack.jar packageName/AntilazyLoad.class
# dx --dex --output=hack.dex packageName/AntilazyLoad.class

dx.bat 命令位于 .../AndroidSDK/build-tools/29.0.2/dx.bat ,需要提前配置环境变量。

  • 第四步,将 hack.jar 文件放入项目 assets 资源目录;

  • 第五步字节码插桩操作业务 .class 文件,经过前面四步,hack.dex 文件成功进入 APK 中,我们接下来需要借助字节码插桩技术让所有的业务类都引用该 AntilazyLoad 类,这样一来,所有的业务类都不会被标记,都能被 QZone dex 分包热修复;接下来,解决两个问题:

Q1:什么时候进行字节码插桩?

A1:顾名思义,字节码插桩技术操作的是字节码,而 Java 编译执行的过程中,.class 文件即为字节码文件,所以,字节码插桩的操作应该在 gradle 自动构建得到 .class 文件之后,同时要在 .class 文件被封装进 dex 文件之前

:app:compileDebugJavaWithJavac
//保证在两个任务之间执行字节码插桩操作
:app:transformClassesWithDexBuilderForDebug

Q2:如何得到所有的 .class 文件?

回答这个问题,我们先认识一下 gradle 中的 Task 。Task 可以看作 gradle 中的基本执行单元,每一个 Task 都有输入和输出,取决于该 Task 所执行的任务内容,同时还有 doFirst() 和 doLast() 回调供开发者使用,分别用于定义执行 Task 之前的操作和执行 Task 之后的操作。

gradle Task 图解

AS 的编译过程中,我们可以在 Build.Build Output 窗口查看 gradle Task 执行情况:

gradle Task

现在我们可以回答第二个问题了。

A2:方法 1 ,我们可以使用 transformClassesWithDexBuilderForDebug Task 的 doFirst() 回调,同时拦截该 Task 的输入,拿到所有的 .class 文件;方法 2 ,我们可以使用 compileDebugJavaWithJavac Task 的 doLast() 回调,同时拦截该 Task 的输出,拿到所有的 .class 文件。以下我们以方法 1 为例。

**两个问题回答完毕,接下来执行字节码插桩的操作。**插桩操作是通过编写 gradle 脚本完成的,使用 groovy 语法。

注意看代码注释

  • 插入 build.gradle 文件的脚本:
//gradle 执行会解析 build.gradle 文件,afterEvaluate 表示在解析完成之后再执行我们的代码
afterEvaluate ({
    android.getApplicationVariants.all {
        variant->
        //获得 debug/release
        String variantName = variant.getName()
        //首字母大写
        String capitalizedName = variantName.capitalize()
        //通过任务名字找到打包 dex 的任务
        Task dexTask = project.getTasks().findByName("transformClassesWithDexBuilderFor" + capitalizedName)
        //定义 doFirst()
        dexTask.doFirst {
            //获取 .class 文件集合
            Set<File> files = dexTask.getInputs().getFiles().getFiles()
            //遍历
            for (File file : files) {
                String filePath = file.getAbsolutePath()
                //依赖的库会以 .jar 包形式传过来,对依赖库也执行插桩
                if (filePath.endsWith(".jar")) {
                    processJar(file)
                //主要是我们的业务字节码 .class 文件
                } else if (filePath.endsWith(".class")) {
                    processClass(variant.getDirName(), file)
                }
            }
        }
    }
})
  • void processClass(String dirName, File file) 方法
static void processClass(String dirName, File file) {
    //获取文件的绝对路径
    String filePath = file.getAbsolutePath()
    //去掉目录名,保留包名+类名
    String className = filePath.split(dirName)[1].subString(1)
    //控制台打印
    println className
    if (className.startsWith("com\\hotfix\\MyApplication") || isAndroidClass(className)){
        return
    }
    try{
        //执行插桩,插桩之后的 .class 数据用 byte[] 保存
        FileInputStream is = new FileInputStream(filePath)
        byte[] byteCode = referHackWhenInit(is)
        is.close()
        //用插桩之后的字节码数据覆盖掉插桩之前的字节码数据
        FileOutputStream os = new FileOutputStream(filePath)
        os.write(byteCode)
        os.close()
    } catch (Exception e) {
        e.printStackTrace()
    }
}
  • byte[] referHackWhenInit(InputStream inputStream) 插桩核心代码
//执行插桩的核心代码
static byte[] referHackWhenInit(InputStream inputStream) throws IOException {
    //class 文件解析器
    ClassReader cr = new ClassReader(inputStream)
    //class 文件输出器
    ClassWriter cw = new ClassWirter(cr, 0)
    //class 文件访问者,相当于操作回调
    ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, cw) {
        @Overide
        public MethodVisitor visitMethod(int access, final String name, String desc,
                                         String signature, String[] exceptions) {
            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)
            mv = new MethodVisitor(Opcodes.ASM5, mv) {
                @Override
                public void visitInsn(int opcode) {
                    //若当前访问的方法是构造方法,则在构造方法 return 之前插入字节码
                    if ("<init>".equals(name) && opcode == Opcodes.RETURN) {
                        super.visitInsn(Type.getType("LpackageName/AntilazyLoad;"))
                    }
                    super.visitInsn(opcode)
                }
            }
            return mv
        }
    }
    //解析器启动解析
    cr.accept(cv, 0)
    return cw.toByteArray()
}

完成字节码插桩之后,所有的 build 之后的业务类都会在构造方法中引用上述 AntilazyLoad 类,例如:

  • build 之前形如:
public class AClass {
    // ...
}
  • build 之后形如 :
import ...packageName.AntilazyLoad;

public class AClass {
	public AClass(){
		AntilazyLoad var10000 = new AntilazyLoad();
	}
    // ...
}

3.2 兼容 Android N (7.0)

详情查看 Android N 混合编译与对热补丁影响解析 by shwenzhang from WeMobileDev

3.2.1 ART 混合编译

ART 是在 Android KitKat (4.0) 引入的,并在 Android Lollipop (5.0) 中被设为默认运行环境,可以看作 DVM 2.0。

ART 模式在 Android N (7.0) 之前安装 APK 时会采用 AOT (Ahead of time:提前编译、静态编译) 预编译 .class 文件为机器码。而在 Android N 使用混合模式的运行时。应用在安装时不做编译,而是运行时解释字节码,同时在 JIT 编译了一些热点代码后将这些代码信息记录至 Profile 文件,等到设备空闲的时候使用 AOT(All-Of-the-Time compilation:全时段编译) 编译生成称为app_imagebase.art (类对象映像) 文件,这个 art 文件会在 apk 启动时自动加载(相当于缓存)。根据类加载原理,类被加载了无法被替换,即无法修复

混合编译示意图

3.2.2 运行时替换 PathClassLoader

事实上,App image 中的 class 是插入到 PathClassloader 中的 ClassTable 中。假设我们完全废弃掉 PathClassloader ,而采用一个新建 Classloader 来加载后续的所有类,即可达到将 cache 无用化的效果。这种方式不会影响没有补丁时的性能,但在加载补丁后,会由于废弃了 App image 带来一定的性能损耗。

需要替换 PathClassLoader 的几个地方

  • Thread.currentThread().setContextClassLoader(ClassLoader classLoader);
    
  • public final class LoadedAPK {
        // ...
        private ClassLoader mClassLoader;
        // ...
    }
    
  • public class Resources {
        // ...
        private DrawableInflater mDrawableInflater;
        // ...
        final ClassLoader mClassLoader;
        // ...
    }
    
  • public final class DrawableInflater {
        // ...
        private final ClassLoader mClassLoader;
    }
    

自定义 ClassLoader ,反射获取以上字段,修改字段的内容即可。

代码详情见 Tencent/Tinker 源码片段 找以下代码段

if (Build.VERSION.SDK_INT >= 24 && !isProtectedApp) {
    classLoader = NewClassLoaderInjector.inject(application, loader);
}

4 插件化

4.1 基本的 gradle 插件开发

  • 第一步,在 project 下新建名为 buildSrc 的 Directory(不是新建 Module),该目录下的文件是给 app/build.gradle 使用的;
  • 第二步,build 编译项目,将在 buildSrc 下生成 .gradle 文件夹和 build 文件夹;
  • 第三步,拷贝一个 Module 的 build.gradle 文件放入 buildSrc 文件夹,并引入 gradleApi();
apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    //引入gradleApi
    implementation gradleApi()
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"
  • 第四步,在 buildSrc 下新建 src/main/java 目录,用于存放插件的 Java 源代码;接着在该 src/main/java 目录下新建一个 Java 包,比如 com.enjoy.plugin 包;接着创建一个 HotfixPlugin 类,HotfixPlugin 作为插件类须实现 Plugin<Project> 接口,并实现 apply() 方法;
package com.enjoy.plugin;

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

public class HotfixPlugin implements Plugin<Project> {
    @Oveerride
    public void apply(Project project) {
        //希望插件执行的任务
    }
}
  • 第五步,在 app/build.gradle 中添加 apply plugin:com.enjoy.plugin.HotfixPlugin ,gradle 编译时解析 build.gradle 脚本时就会执行插件的 apply() 方法;
  • 第六步如何让插件提供一些参数给用户配置?在 package com.enjoy.plugin 内新建第二个类叫 PatchExt ,将我们所有希望用户配置的参数都设置成该类的成员变量,并提供 getter/settter ,形成一个 JavaBean ;然后在 HotfixPlugin 的 apply 方法中调用 project.getExtentions().create("patch", PatchExt.class)其中 ”patch“ 字符串是 build.gradle 中设置参数时闭包的名字
package com.enjoy.plugin;

public class PatchExt {

    //三个我们希望用户配置的参数
    boolean debugOn;
    String output;
    String applicationName;

    //debugOn 参数有默认值,用户可以不用配置
    public PatchExtension() {
        debugOn = false;
    }
    
    public boolean getDebugOn(){
        return this.debugOn;
    }
    
    public String getOutput(){
        return this.output;
    }
    
    public String getApplicationName(){
        return this.applicationName;
    }

    public void setDebugOn(boolean debugOn) {
        this.debugOn = debugOn;
    }

    public void setOutput(String output) {
        this.output = output;
    }

    public void setApplicationName(String applicationName) {
        this.applicationName = applicationName;
    }
}
package com.enjoy.plugin;

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

public class HotfixPlugin implements Plugin<Project> {
    @Oveerride
    public void apply(Project project) {
        //希望插件执行的任务
        final PatchExt patch = project.getExtentions().create("patch", PatchExt.class);
    }
}
  • 第七步,在 app/build.gradle 中引用插件;
apply plugin: com.enjoy.plugin.HotfixPlugin

patch {
    debugOn true
    output 'xxx//xxx//xxx'
    applicationName 'com.enjoy.qzonefix.MyApplication'
}
  • 第八步,在 HotfixPlugin 中拿到用户设置的参数;
package com.enjoy.plugin;

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

public class HotfixPlugin implements Plugin<Project> {
    @Oveerride
    public void apply(Project project) {
        //希望插件执行的任务
        final PatchExt patch = project.getExtentions().create("patch", PatchExt.class);
        //获取用户设置的参数
        project.afterEvaluate(new Action<Project>() {
            @Override
            public void execute(Project project) {
                String applicationName = patch.applicationName;
                System.out.println(applicationName);//将打印 com.enjoy.qzonefix.MyApplication
            }
        });
        
    }
}

以上即为基本的 gradle 插件开发。

4.2 热修复插件开发

4.2.1 md5 值比较

//TODO

4.2.2 mapping 表缓存

//TODO