Android应用嵌入腾讯Tinker热修复详解

1,373 阅读14分钟

背景

简单来说,热修复是一种动态加载技术,比如线上某个产品出现了bug:

  • 传统流程:debug->测试->发布新版 ->用户安装(各平台审核时间不一,而且用户需要手动下载或者更新)
  • 集成热修复情况下:dubug->测试->推送补丁->自动下载补丁修复 (用户不知情况,自动下载补丁并修复)

对比下来,我们不难发现,传统流程存在这几大弊端:

  • 发版代价大
  • 用户下载安装的成本过高
  • bug修复不及时,取决于各平台的审核时间等等

基于以上原因热修复技术就此诞生。

现状

目前市面上主流热修复框架主要有HotFix、AndFix、Tinker、Robust等。热修复框架按照原理大致可以分为三类:

  • 基于 multidex机制 干预 ClassLoader 加载dex
  • native 替换方法结构体
  • instant-run 插桩方案

HotFix和Tinker都是使用的方案一;阿里的AndFix使用的是方案二;美团的Robust使用的是方案三。

下面对这几个框架做一下简单的对比:

  • github受欢迎程度对比

  • 优缺点对比

  • 实现原理对比

    1. QQ空间补丁原理 把补丁类生成 patch.dex,在app启动时,使用反射获取当前应用的ClassLoader,也就是 BaseDexClassLoader,反射获取其中的pathList,类型为DexPathList, 反射获取其中的Element[] dexElements, 记为elements1;然后使用当前应用的ClassLoader作为父ClassLoader,构造出 patch.dex 的 DexClassLoader,通用通过反射可以获取到对应的Element[] dexElements,记为elements2。将elements2拼在elements1前面,然后再去调用加载类的方法loadClass。

    2. Tinker 原理 对于Tinker,修复前和修复后的apk分别定义为apk1和apk2,tinker自研了一套dex文件差分合并算法,在生成补丁包时,生成一个差分包 patch.dex,后端下发patch.dex到客户端时,tinker会开一个线程把旧apk的class.dex和patch.dex合并,生成新的class.dex并存放在本地目录上,重新启动时,会使用本地新生成的class.dex对应的elements替换原有的elements数组。

    3. AndFix 原理 AndFix的修复原理是替换方法的结构体。在native层获取修复前类和修复后类的指针,然后将旧方法的属性指针指向新方法。由于不同系统版本下的方法结构体不同,而且davilk与art虚拟机处理方式也不一样,所以需要针对不同系统针对性的替换方法结构体。

    4. Robust原理 Robust 的实现可以分成三个部分:插桩、生成补丁包、加载补丁包。利用编译插桩、Instant-run实现。

探索实践(原理)

经过上述调研和对比决定先从tinker入手开始进行尝试。下面先从通用Android热修复实现原理来分析:

Java类加载机制

双亲委派模型

在加载一个字节码文件时,会询问当前的classLoader是否已经加载过此字节码文件。如果加载过,则直接返回,不再重复加载。如果没有加载过,则会询问它的Parent是否已经加载过此字节码文件,同样的,如果已经加载过,就直接返回parent加载过的字节码文件,而如果整个继承线路上的classLoader都没有加载过,才由child类加载器(即,当前的子classLoader)执行类的加载工作。

1)特点: 如果一个类被classLoader继承线路上的任意一个加载过,那么在以后整个系统的生命周期中,这个类都不会再被加载,大大提高了类的加载效率。

2)作用:

类加载的共享功能:

一些Framework层级的类一旦被顶层classLoader加载过,会缓存到内存中,以后在任何地方用到,都不会去重新加载。

类加载的隔离功能:

共同继承线程上的classLoader加载的类,肯定不是同一个类,这样可以避免某些开发者自己去写一些代码冒充核心类库,来访问核心类库中可见的成员变量。如java.lang.String在应用程序启动前就已经被系统加载好了,如果在一个应用中能够简单的用自定义的String类把系统中的String类替换掉的话,会有严重的安全问题。

验证多个类是同一个类的成立条件:

  • 相同的className
  • 相同的packageName
  • 被相同的classLoader加载

3)loadClass()

通过loadClass()这个方法来验证双亲委派模型

找到ClassLoader这个类中的loadClass()方法,它调用的是另一个2个参数的重载loadClass()方法。

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

找到最终这个真正的loadClass()方法,下面便是该方法的源码:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    // First, check if the class has already been loaded
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // ClassNotFoundException thrown if class not found
            // from the non-null parent class loader
        }

        if (c == null) {
            // If still not found, then invoke findClass in order
            // to find the class.
            c = findClass(name);
        }
    }
    return c;
}

可以看到,如前面所说,加载一个类时,会有如下3步:

  1. 检查当前的classLoader是否已经加载琮这个class,有则直接返回,没有则进行第2步。
  2. 调用父classLoader的loadClass()方法,检查父classLoader是否有加载过这个class,有则直接返回,没有就继续检查上上个父classLoader,直到顶层classLoader。
  3. 如果所有的父classLoader都没有加载过这个class,则最终由当前classLoader调用findClass()方法,去dex文件中找出并加载这个class。

Android中的ClassLoader

1、类加载器类型

Android跟java有很大的渊源,基于jvm的java应用是通过ClassLoader来加载应用中的class的,Android对jvm优化过,使用的是dalvik虚拟机,且class文件会被打包进一个dex文件中,底层虚拟机有所不同,那么它们的类加载器当然也是会有所区别。

Android中最主要的类加载器有如下4个:

  • BootClassLoader:加载Android Framework层中的class字节码文件(类似java的Bootstrap ClassLoader)
  • PathClassLoader:加载已经安装到系统中的Apk的class字节码文件(类似java的App ClassLoader)
  • DexClassLoader:加载制定目录的class字节码文件(类似java中的Custom ClassLoader)
  • BaseDexClassLoader:PathClassLoader和DexClassLoader的父类

一个app一定会用到BootClassLoader、PathClassLoader这2个类加载器,可通过如下代码进行验证:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ClassLoader classLoader = getClassLoader();
    if (classLoader != null) {
        Log.e(TAG, "classLoader = " + classLoader);
        while (classLoader.getParent() != null) {
            classLoader = classLoader.getParent();
            Log.e(TAG, "classLoader = " + classLoader);
        }
    }
}

上面代码中可以通过上下文拿到当前类的类加载器(PathClassLoader),然后通过getParent()得到父类加载器(BootClassLoader),这是由于Android中的类加载器和java类加载器一样使用的是双亲委派模型。

2、PathClassLoader与DexClassLoader的区别

一般的源码在Android Studio中可以查到,但 PathClassLoader 和 DexClassLoader 的源码是属于系统级源码,所以无法在Android Studio中直接查看。可以到androidxref.com这个网站上直接查看,下面会列出之后要分析的几个类的源码地址。

以下是Android 5.0中的部分源码:

  • PathClassLoader.java
  • DexClassLoader.java
  • BaseDexClassLoader.java
  • DexPathList.java

1)使用场景

先来介绍一下这两种Classloader在使用场景上的区别

  • PathClassLoader:只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。
  • DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更灵活,是实现热修复的重点。

2)代码差异

下面来看一下PathClassLoader与DexClassLoader的源码的差别,都非常简单

// PathClassLoader
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);
    }
}
// DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}

通过比对,可以得出2个结论:

  • PathClassLoader与DexClassLoader都继承于BaseDexClassLoader。
  • PathClassLoader与DexClassLoader在构造函数中都调用了父类的构造函数,但DexClassLoader多传了一个optimizedDirectory。

3、BaseDexClassLoader

通过观察PathClassLoader与DexClassLoader的源码我们就可以确定,真正有意义的处理逻辑肯定在BaseDexClassLoader中,所以下面着重分析BaseDexClassLoader源码。

1)构造函数

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;
    ...
    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent){
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    ...
}
  • dexPath:要加载的程序文件(一般是dex文件,也可以是jar/apk/zip文件)所在目录。
  • optimizedDirectory:dex文件的输出目录(因为在加载jar/apk/zip等压缩格式的程序文件时会解压出其中的dex文件,该目录就是专门用于存放这些被解压出来的dex文件的)。
  • libraryPath:加载程序文件时需要用到的库路径。
  • parent:父加载器 从一个完整App的角度来说,程序文件指定的就是apk包中的classes.dex文件;但从热修复的角度来看,程序文件指的是补丁。

因为PathClassLoader只会加载已安装包中的dex文件,而DexClassLoader不仅仅可以加载dex文件,还可以加载jar、apk、zip文件中的dex。jar、apk、zip其实就是一些压缩格式,要拿到压缩包里面的dex文件就需要解压,所以,DexClassLoader在调用父类构造函数时会指定一个解压的目录。

2)findClass()

类加载器肯定会提供有一个方法来供外界找到它所加载到的class,该方法就是findClass(),不过在PathClassLoader和DexClassLoader源码中都没有重写父类的findClass()方法,但它们的父类BaseDexClassLoader就有重写findClass(),所以来看看BaseDexClassLoader的findClass()方法都做了哪些操作,代码如下:

private final DexPathList pathList;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    // 实质是通过pathList的对象findClass()方法来获取class
    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的findClass()方法实际上是通过DexPathList对象(pathList)的findClass()方法来获取class的,而这个DexPathList对象恰好在之前的BaseDexClassLoader构造函数中就已经被创建好了。所以,下面就来看看DexPathList类中都做了什么。

DexPathList

1)构造函数

private final Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
    ...
    this.definingContext = definingContext;
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
    ...
}

这个构造函数中,保存了当前的类加载器definingContext,并调用了makeDexElements()得到Element集合。

通过对splitDexPath(dexPath)的源码追溯,发现该方法的作用其实就是将dexPath目录下的所有程序文件转变成一个File集合。而且还发现,dexPath是一个用冒号(":")作为分隔符把多个程序文件目录拼接起来的字符串(如:/data/dexdir1:/data/dexdir2:...)。

接下来分析makeDexElements()方法:

private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {
    // 1.创建Element集合
    ArrayList<Element> elements = new ArrayList<Element>();
    // 2.遍历所有dex文件(也可能是jar、apk或zip文件)
    for (File file : files) {
        ZipFile zip = null;
        DexFile dex = null;
        String name = file.getName();
        ...
        // 如果是dex文件
        if (name.endsWith(DEX_SUFFIX)) {
            dex = loadDexFile(file, optimizedDirectory);

        // 如果是apk、jar、zip文件(这部分在不同的Android版本中,处理方式有细微差别)
        } else {
            zip = file;
            dex = loadDexFile(file, optimizedDirectory);
        }
        ...
        // 3.将dex文件或压缩文件包装成Element对象,并添加到Element集合中
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }
    // 4.将Element集合转成Element数组返回
    return elements.toArray(new Element[elements.size()]);
}

在这个方法中,DexPathList的构造函数是将一个个的程序文件(可能是dex、apk、jar、zip)封装成一个个Element对象,最后添加到Element集合中。

其实,Android的类加载器(不管是PathClassLoader,还是DexClassLoader),它们最后只认dex文件,而loadDexFile()是加载dex文件的核心方法,可以从jar、apk、zip中提取出dex,但这里先不分析了,因为第1个目标已经完成,等到后面再来分析吧。

2)findClass()

再来看DexPathList的findClass()方法:

public Class findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        // 遍历出一个dex文件
        DexFile dex = element.dexFile;

        if (dex != null) {
            // 在dex文件中查找类名与name相同的类
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

结合DexPathList的构造函数,其实DexPathList的findClass()方法很简单,就只是对Element数组进行遍历,一旦找到类名与name相同的类时,就直接返回这个class,找不到则返回null。

为什么是调用DexFile的loadClassBinaryName()方法来加载class?这是因为一个Element对象对应一个dex文件,而一个dex文件则包含多个class。也就是说Element数组中存放的是一个个的dex文件,而不是class文件。这可以从Element这个类的源码和dex文件的内部结构看出。

热修复的实现原理

经过对PathClassLoader、DexClassLoader、BaseDexClassLoader、DexPathList的分析,我们知道,安卓的类加载器在加载一个类时会先从自身DexPathList对象中的Element数组中获取(Element[] dexElements)到对应的类,之后再加载。采用的是数组遍历的方式,不过注意,遍历出来的是一个个的dex文件。在for循环中,首先遍历出来的是dex文件,然后再是从dex文件中获取class,所以,我们只要让修复好的class打包成一个dex文件,放于Element数组的第一个元素,这样就能保证获取到的class是最新修复好的class了(当然,有bug的class也是存在的,不过是放在了Element数组的最后一个元素中,所以没有机会被拿到而已。

在了解了热修复的基本原理后再来看一下tinker基于这个原理做了哪些改进,下面是tinker官方给出的原理图:

Tinker的核心思想是利用DexDiff算法对比差异生成Patch 补丁包,然后将 Patch 包里面的 Dex与基础包的 dexElements 进行合并,全量合成新dex,进行整体替换。

探索实践(集成)

在了解完原理后开始集成到项目中进行测试

1、首先在gradle中引入tinker核心库:

dependencies {
    //tinker's main Android lib
    implementation('com.tencent.tinker:tinker-android-lib:1.9.13.2')
}
...
...
apply plugin: 'com.tencent.tinker.patch'

2、在项目根目录的gradle中引入tinker插件

buildscript {
    dependencies {
        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.13.2')
    }
}

3、在项目的gradle文件中加入相关配置(其中的相关参数已经研究过配置好,这里直接加进去就可以)

//====================================Tinker配置与任务开始=========================================================
def bakPath = file("${buildDir}/bakApk/")
ext {
    // 是否使用Tinker(当你的项目处于开发调试阶段时,可以改为false)
 tinkerEnabled = true
 // 基础包路径(名字这里写死为old-app.apk。用于比较新旧app生成补丁包,不管是debug还是release编译)
 tinkerOldApkPath = "${bakPath}/old-app.apk"
 // 基础包的mapping.txt文件路径(用于辅助混淆补丁包的生成,一般在生成release版app时会使用到混淆,所以这个mapping.txt文件一般也是用于release安装包补丁的生成)
 tinkerApplyMappingPath = "${bakPath}/old-app-mapping.txt"
 // 基础包的R.txt文件路径(如果你的安装包中资源文件有改动,则需要使用该R.txt文件来辅助生成补丁包)
 tinkerApplyResourcePath = "${bakPath}/old-app-R.txt"
 //only use for build all flavor, if not, just ignore this field
 tinkerBuildFlavorDirectory = "${bakPath}/flavor"
}

def getOldApkPath() {
    return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}

def getApplyMappingPath() {
    return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}

def getApplyResourceMappingPath() {
    return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}

def getTinkerIdValue() {
    return hasProperty("TINKER_ID") ? TINKER_ID : android.defaultConfig.versionName
}

def buildWithTinker() {
    return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}

if (buildWithTinker()) {
    //apply tinker插件
 apply plugin: 'com.tencent.tinker.patch'

 // 全局信息相关的配置项
 tinkerPatch {
        tinkerEnable = buildWithTinker()// 是否打开tinker的功能。
 oldApk = getOldApkPath()        // 基准apk包的路径,必须输入,否则会报错。
 ignoreWarning = true // 是否忽略有风险的补丁包。这里选择不忽略,当补丁包风险时会中断编译。
 useSign = true // 在运行过程中,我们需要验证基准apk包与补丁包的签名是否一致,我们是否需要为你签名。
 // 编译相关的配置项
 buildConfig {
            applyMapping = getApplyMappingPath()
            // 可选参数;在编译新的apk时候,我们希望通过保持旧apk的proguard混淆方式,从而减少补丁包的大小。这个只是推荐设置,不设置applyMapping也不会影响任何的assemble编译。
 applyResourceMapping = getApplyResourceMappingPath()
            // 可选参数;在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配,这样不仅可以减少补丁包的大小,同时也避免由于ResId改变导致remote view异常。
 tinkerId = getTinkerIdValue()
            // 在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等。
 keepDexApply = false
 // 如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。
 isProtectedApp = true // 是否使用加固模式,仅仅将变更的类合成补丁。注意,这种模式仅仅可以用于加固应用中。
 supportHotplugComponent = false // 是否支持新增非export的Activity(1.9.0版本开始才有的新功能)
 }
        // dex相关的配置项
 dex {
            dexMode = "jar"
 // 只能是'raw'或者'jar'。 对于'raw'模式,我们将会保持输入dex的格式。对于'jar'模式,我们将会把输入dex重新压缩封装到jar。如果你的minSdkVersion小于14,你必须选择‘jar’模式,而且它更省存储空间,但是验证md5时比'raw'模式耗时。默认我们并不会去校验md5,一般情况下选择jar模式即可。
 pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            // 需要处理dex路径,支持*、?通配符,必须使用'/'分割。路径是相对安装包的,例如assets/...
 loader = [
                    // 定义哪些类在加载补丁包的时候会用到。这些类是通过Tinker无法修改的类,也是一定要放在main dex的类。
 // 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中;
 // 其他一些你不希望被更改的类,例如Sample中的BaseBuildInfo类。这里需要注意的是,这些类的直接引用类也需要加入到loader中。或者你需要将这个类变成非preverify。
 ]
        }
        // lib相关的配置项
 lib {
            pattern = ["lib/*/*.so", "src/main/jniLibs/*/*.so"]
            // 需要处理lib路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如assets/...
 }
        // res相关的配置项
 res {
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
            // 需要处理res路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如assets/...,务必注意的是,只有满足pattern的资源才会放到合成后的资源包。
 ignoreChange = [
                    // 支持*、?通配符,必须使用'/'分割。若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改。 最极端的情况,ignoreChange与上面的pattern一致,即会完全忽略所有资源的修改。
 "assets/sample_meta.txt"
 ]
            largeModSize = 100
 // 对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。这可以降低补丁包的大小,但是会增加合成时的复杂度。默认大小为100kb
 }
        // 用于生成补丁包中的'package_meta.txt'文件
 packageConfig {
            // configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动写入configField。
 // 在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。
 // 但是建议直接通过修改代码来实现,例如BuildConfig。
 configField("platform", "all")
            configField("patchVersion", "1.0")
// configField("patchMessage", "tinker is sample to use")
 }
        // 7zip路径配置项,执行前提是useSign为true
 sevenZip {
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
 }
    }
    List<String> flavors = new ArrayList<>();
    project.android.productFlavors.each { flavor ->
        flavors.add(flavor.name)
    }
    boolean hasFlavors = flavors.size() > 0
 def date = new Date().format("MMdd-HH-mm-ss")

    /**
 * bak apk and mapping
 */
 android.applicationVariants.all { variant ->
        /**
 * task type, you want to bak
 */
 def taskName = variant.name

        tasks.all {
            if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {

                it.doLast {
                    copy {
                        def fileNamePrefix = "${project.name}-${variant.baseName}"
 def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"

 def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
                        from variant.outputs.first().outputFile
                        into destPath
                        rename { String fileName ->
                            fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
                        }

                        from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
 into destPath
                        rename { String fileName ->
                            fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
                        }

                        from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
 into destPath
                        rename { String fileName ->
                            fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
                        }
                    }
                }
            }
        }
    }
    project.afterEvaluate {
        //sample use for build all flavor for one time
 if (hasFlavors) {
            task(tinkerPatchAllFlavorRelease) {
                group = 'tinker'
 def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}ReleaseManifest")
                    preAssembleTask.doFirst {
                        String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release.apk"
 project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-mapping.txt"
 project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-R.txt"

 }

                }
            }

            task(tinkerPatchAllFlavorDebug) {
                group = 'tinker'
 def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Debug")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}DebugManifest")
                    preAssembleTask.doFirst {
                        String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk"
 project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-mapping.txt"
 project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-R.txt"
 }

                }
            }
        }
    }
}
//====================================Tinker配置与任务结束=========================================================

4、自定义一些文件继承tinker中的相关回调类自己处理(这些文件直接拷贝到项目中就可以)

SampleApplicationLike.java

SampleLoadReporter.java

SamplePatchListener.java

SamplePatchReporter.java

SampleResultService.java

SampleTinkerReport.java

SampleUncaughtExceptionHandler.java

TinkerManager.java

TinkerUtils.java

5、修改项目Application,在项目自定义的MyApplication(名字随意)中构造方法中加入下面的代码(com.test.SampleApplicationLike这个根据自己放的位置而变)

public MyApplication() {
    super(
            //tinkerFlags, which types is supported
 //dex only, library only, all support
 ShareConstants.TINKER_ENABLE_ALL,
 // This is passed as a string so the shell application does not
 // have a binary dependency on your ApplicationLifeCycle class.
 "com.test.SampleApplicationLike");

6、在AndroidManifest中注册步骤4中的Service,如下(exported必须为false,路径根据自己放的位置而定):

<service
android:name="com.test.tinker.SampleResultService"
android:exported="false"/>

7、修复时机根据业务在代码里自由处理,如下示例:

 String filePath = Build.VERSION.SDK_INT >= 29 ? getExternalFilesDir(null).getAbsolutePath() : Environment.getExternalStorageDirectory().getAbsolutePath();
if (new File(filePath + "/patch_signed_7zip.apk").exists()) {
 TinkerInstaller.onReceiveUpgradePatch(this, filePath + "/patch_signed_7zip.apk");
}

探索实践(使用)

集成完成该如何打补丁包?看下面流程:

1、首先补丁包是基于基础包才能打出来的,所以我们先把基础包放在下面这个地方,并修改成如下名称(这里的位置可以配置,名称也能配置,下图是以上面gradle中的配置做示例的)

2、打开gradle窗口执行tinkerPatch命令

3、执行完命令后就可以在下面的位置看到补丁包

4、补丁包已经成功打出来,就可以根据业务做自己的处理了