背景
简单来说,热修复是一种动态加载技术,比如线上某个产品出现了bug:
- 传统流程:debug->测试->发布新版 ->用户安装(各平台审核时间不一,而且用户需要手动下载或者更新)
- 集成热修复情况下:dubug->测试->推送补丁->自动下载补丁修复 (用户不知情况,自动下载补丁并修复)
对比下来,我们不难发现,传统流程存在这几大弊端:
- 发版代价大
- 用户下载安装的成本过高
- bug修复不及时,取决于各平台的审核时间等等
基于以上原因热修复技术就此诞生。
现状
目前市面上主流热修复框架主要有HotFix、AndFix、Tinker、Robust等。热修复框架按照原理大致可以分为三类:
- 基于 multidex机制 干预 ClassLoader 加载dex
- native 替换方法结构体
- instant-run 插桩方案
HotFix和Tinker都是使用的方案一;阿里的AndFix使用的是方案二;美团的Robust使用的是方案三。
下面对这几个框架做一下简单的对比:
-
github受欢迎程度对比

-
优缺点对比

-
实现原理对比
-
QQ空间补丁原理 把补丁类生成 patch.dex,在app启动时,使用反射获取当前应用的ClassLoader,也就是 BaseDexClassLoader,反射获取其中的pathList,类型为DexPathList, 反射获取其中的Element[] dexElements, 记为elements1;然后使用当前应用的ClassLoader作为父ClassLoader,构造出 patch.dex 的 DexClassLoader,通用通过反射可以获取到对应的Element[] dexElements,记为elements2。将elements2拼在elements1前面,然后再去调用加载类的方法loadClass。
-
Tinker 原理 对于Tinker,修复前和修复后的apk分别定义为apk1和apk2,tinker自研了一套dex文件差分合并算法,在生成补丁包时,生成一个差分包 patch.dex,后端下发patch.dex到客户端时,tinker会开一个线程把旧apk的class.dex和patch.dex合并,生成新的class.dex并存放在本地目录上,重新启动时,会使用本地新生成的class.dex对应的elements替换原有的elements数组。
-
AndFix 原理 AndFix的修复原理是替换方法的结构体。在native层获取修复前类和修复后类的指针,然后将旧方法的属性指针指向新方法。由于不同系统版本下的方法结构体不同,而且davilk与art虚拟机处理方式也不一样,所以需要针对不同系统针对性的替换方法结构体。
-
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步:
- 检查当前的classLoader是否已经加载琮这个class,有则直接返回,没有则进行第2步。
- 调用父classLoader的loadClass()方法,检查父classLoader是否有加载过这个class,有则直接返回,没有就继续检查上上个父classLoader,直到顶层classLoader。
- 如果所有的父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官方给出的原理图:


探索实践(集成)
在了解完原理后开始集成到项目中进行测试
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中的相关回调类自己处理(这些文件直接拷贝到项目中就可以)
SampleUncaughtExceptionHandler.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中的配置做示例的)


