关于热更新目前的各种方案原理看我之前这篇文章的介绍热补丁动态修复技术调研
这篇文章将选择Trinker集成体验一下热更新。
1.什么是Tinker?
Tinker 是一个开源项目(Github链接),它是微信官方的 Android 热补丁解决方案,它支持动态下发代码、So 库以及资源,让应用能够在不需要重新安装的情况下实现更新。但需要注意的是需要重启Activity或重启Application才能达到更新效果。
2.对比其他以及选择Tinker的理由?
将Tinker与其他原理的热补丁方案做对比:阿里的 AndFix、美团的 Robust 以及 QZone 的超级补丁方案,如果不了解这三类原理,就看这篇文章的介绍热补丁动态修复技术调研
总的来说:
-
AndFix作为native解决方案,首先面临的是稳定性与兼容性问题,更重要的是它无法实现类替换,它是需要大量额外的开发成本的;
-
Robust兼容性与成功率较高,但是它与AndFix一样,无法新增变量与类只能用做的bugFix方案;
-
Qzone方案可以做到发布产品功能,但是它主要问题是插桩带来Dalvik的性能问题,以及为了解决Art下内存地址问题而导致补丁包急速增大的。
Tinker热补丁方案不仅支持类、So 以及资源的替换,它还是2.X-7.X的全平台支持。利用Tinker我们不仅可以用做 bugfix,甚至可以替代功能的发布。Tinker 已运行在微信的数亿 Android 设备上,那么为什么你不使用 Tinker 呢?
3.接入TinkerPatch 平台
- Tinker 需要使用者有一个后台可以下发和管理补丁包,并且需要处理传输安全等部署工作,TinkerPatch 平台帮你做了这些工作,提供了补丁后台托管,版本管理,保证传输安全等功能,让你无需搭建一个后台,无需关心部署操作,只需引入一个 SDK 即可立即使用 Tinker。
- 接入平台,无需理解复杂的热修复原理,一行代码即可接入热修复。实现了自动反射 Appliction 与 Library,使用者无需对自己的项目做任何的改动;
- 当然如果我们要更好的理解tinker,需要研究其原理,源码,若不使用TinkerPatch平台,也可以根据需求自己搭建后台,根据开源的tinker自行接入。
4.【通过接入TinkerPatch 平台,开启Tinker之旅】
第一步:注册 TinkerPatch 平台账号点击前往注册
第二步 添加 APP
名字只是一个标识,没有其他要求
第三步 记录 AppKey
添加完APP,会生成一个AppKey,这个将在后续代码中使用
第四步 客户端SDK接入 【重点】
1.添加 gradle 插件依赖 :位于【工程】的build.gradle下,gradle 远程仓库依赖 jcenter。
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
// 添加TinkerPatch 插件
//无需再单独引用tinker的其他库
classpath "com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:1.2.2"
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
2.集成 TinkerPatch SDK, 位于【app】的build.gradle下
dependencies {
...
compile 'com.android.support:multidex:1.0.1'
// 若使用annotation需要单独引用,对于tinker的其他库都无需再引用
provided("com.tinkerpatch.tinker:tinker-android-anno:1.9.2")
compile "com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.2.2"
}
对于有的使用的as3.0,引入语法有变化,如下配置
dependencies {
...
implementation 'com.android.support:multidex:1.0.1'
// 若使用annotation需要单独引用,对于tinker的其他库都无需再引用
compileOnly("com.tinkerpatch.tinker:tinker-android-anno:1.9.2")
implementation "com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.2.2"
}
注意,若使用 annotation 自动生成 Application, 需要单独引入 Tinker 的 tinker-android-anno 库。除此之外,我们无需再单独引入 tinker 的其他库。
同时在【app】目录下需要将 TinkerPatch 相关的配置都放于 tinkerpatch.gradle 中,所以新建一个file,命名为tinkerpatch.gradle
所以我们需要在app下的build.gradle引入
dependencies {
...
}
//添加
apply from: 'tinkerpatch.gradle'
3.配置 tinkerpatchSupport 参数 ,在tinkerpatch.gradle下
apply plugin: 'tinkerpatch-support'
//这里需要说明bakPath定义了基准包的输出位置
def bakPath = file("${buildDir}/bakApk/")
//baseInfo和variantName 对于基准包我们可以忽略不填,
//因为tinker也不会对这个做处理
//它主要的作用是在打补丁包是,我们要根据基准包输出的路径下的信息做修改。主要是给补丁包配置
//这里我们先默认为"",等到基准包完成后再修改。
def baseInfo = "xxx"
def variantName = "xxx"
tinkerpatchSupport {
tinkerEnable = true
reflectApplication = true
protectedApp = true
supportComponent = false
autoBackupApkPath = "${bakPath}"
appKey = "d8de698f2ac404aa"
appVersion = "1.0.0"
def pathPrefix = "${bakPath}/${baseInfo}/${variantName}"
def name = "${project.name}-${variantName}"
baseApkFile = "${pathPrefix}/${name}.apk"
baseProguardMappingFile = "${pathPrefix}/${name}-mapping.txt"
baseResourceRFile = "${pathPrefix}/${name}-R.txt"
}
android {
defaultConfig {
buildConfigField "boolean", "TINKER_ENABLE", "${tinkerpatchSupport.tinkerEnable}"
}
}
tinkerPatch {
ignoreWarning = false
useSign = true
dex {
dexMode = "jar"
pattern = ["classes*.dex"]
loader = []
}
lib {
pattern = ["lib/*/*.so"]
}
res {
pattern = ["res/*", "r/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
ignoreChange = []
largeModSize = 100
}
packageConfig {
}
sevenZip {
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
}
buildConfig {
keepDexApply = false
}
}
重要参数描述
我们将原apk包称为基准apk包,tinkerPatch直接使用基准apk包与新编译出来的apk包做差异,得到最终的补丁包。
参数 | 默认值 | 描述 | 其他说明 |
---|---|---|---|
tinkerEnable | true | 是否开启 tinkerpatchSupport 插件功能。 | 在开启时将不能instant run,所以若想调试程序,在debug的时候关闭false |
reflectApplication | false | 是否反射 Application 实现一键接入 | 一般来说,接入 Tinker 我们需要改造我们的 Application, 若这里为 true, 即我们无需对应用做任何改造即可接入。 |
protectedApp | false | 是否开启支持加固 | 若我们的程序需要加固,则开启此选项 |
supportComponent | false | 是否开启支持在补丁包中动态增加Activity | 注意:新增Activity的Exported属性必须为false� |
autoBackupApkPath | "" | 指定每次编译产生的 apk/mapping.txt/R.txt 归档存储的位置 | 无 |
appKey | "" | 在 TinkerPatch 平台 申请的 appkey | 就是我们在上面提到的appkey |
appVersion | "" | 和 TinkerPatch 平台 的版本号需要对应,若这里我们输入1.0.0,之后在TinkerPatch平台上我们也需要输入1.0.0 | 注意,我们使用 appVersion 作为 TinkerId, 我们需要保证每个发布出去的基础安装包的 appVersion 都不一样。 |
baseApkFile | "" | 基准包的文件路径,对应 tinker 插件中的 oldApk 参数 | 编译补丁包时,必需指定基准版本的 apk(路径以及名字,如果不对应,将不到找到对应的基准包进行打补丁操作),默认值为空,则表示不是进行补丁包的编译。所以这个参数很重要。 |
baseProguardMappingFile | "" | 基准包的 Proguard mapping.txt 文件路径, 对应 tinker 插件 applyMapping 参数 | 在编译新的 apk 时候,我们希望通过保持基准 apk 的 proguard 混淆方式,从而减少补丁包的大小。这是强烈推荐的,编译补丁包时,我们推荐输入基准 apk 生成的 mapping.txt 文件。 |
baseResourceRFile | "" | 基准包的资源 R.txt 文件路径, 对应 tinker 插件 applyResourceMapping 参数 | 在编译新的apk时候,我们希望通基准 apk 的 R.txt 文件来保持 Resource Id 的分配,这样不仅可以减少补丁包的大小,同时也避免由于 Resource Id 改变导致 remote view 异常。 |
tinkerPatch | 全局信息相关的配置项 | 一般来说,我们无需对其中的参数做任何的修改 | |
outputFolder | 设置编译输出路径,也就是补丁包生成的位置,默认在build/outputs/tinkerPatch中 | 一般我们不做指定,也不用配置,只需要知道补丁生成的位置在哪里 | |
ignoreWarning | false | 如果出现如右侧的五种情况,并且ignoreWarning为false,程序将中断编译。因为这些情况可能会导致编译出来的patch包带来风险 | 1. minSdkVersion小于14,但是dexMode的值为"raw";2. 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);3. 定义在dex.loader用于加载补丁的类不在main dex中;4. 定义在dex.loader用于加载补丁的类出现修改;5. resources.arsc改变,但没有使用applyResourceMapping编译。 |
useSign | ture | 在运行过程中,我们需要验证基准apk包与补丁包的签名是否一致,我们是否需要为你签名。 | 做好不要做修改 |
4.初始化 TinkerPatch SDK
最后在我们的代码中,只需简单的初始化 TinkerPatch 的 SDK 即可,我们无需考虑 Tinker 是如何下载/合成/应用补丁包, 也无需引入各种各样 Tinker 的相关类。
对于初始化操作我们分为两种情况:
-
第一种是我们的 reflectApplication = true,即一键接入,此时我们无需为接入 Tinker 而改造我们的 Application 类。
public class MyApplication extends Application {
...
@Override
public void onCreate() {
super.onCreate();
// 我们可以从这里获得Tinker加载过程的信息
ApplicationLike tinkerApplicationLike = TinkerPatchApplicationLike.getTinkerPatchApplicationLike();
// 初始化TinkerPatch SDK, 更多配置可参照API章节中的,初始化SDK
TinkerPatch.init(tinkerApplicationLike)
.reflectPatchLibrary()
.setPatchRollbackOnScreenOff(true)
.setPatchRestartOnSrceenOff(true)
.setFetchPatchIntervalByHours(3);
// 每隔3个小时(通过setFetchPatchIntervalByHours设置)去访问后台时候有更新,通过handler实现轮训的效果
TinkerPatch.with().fetchPatchUpdateAndPollWithInterval();
}
...
-
第二种是我们的 reflectApplication = false,就是根据自己的需求自己改造应用的 Application。参考 tinkerpatch-sample 中的 SampleApplicationLike 类这里不做更多说明。
5.使用步骤
到此为止,我们的配置已经完成,就剩下实践操作了:
1.获得基准包
- 确保更新tinkerpatchSupport中的appVersion
- 运行 assembleRelease task 构建基准包。也就是我们通常通过命令./gradlew assembleRelease 出的apk包。
- tinkerPatch会基于你填入的autoBackupApkPath自动备份基础包信息到相应的文件夹,包含:apk文件、R.txt文件和mapping.txt文件 (注:mapping.txt是proguard的产物,如果你没有开启proguard则不会有这个文件�)
-
根据我们的配置,此时构建完成基准包的位置即为如图
- tinker按照时间戳给我们命名
- 你要发布的apk就是该路径下的apk,如果需要加固也是用该apk。
2.获得补丁包
- 构建之前:将自动保存下来的文件分别填到tinkerpatchSupport中的baseApkFile、baseProguardMappingFile和baseResourceRFile 参数中,因为我们需要根据这些参数路径对应的apk和当前工程对比出不同,然后打出补丁。
-
修改配置文件【重点】:
- 所以你应该理解了,每次打补丁只需要改根据基准apk修改这两处就可以了,baseApkFile、baseProguardMappingFile和baseResourceRFile 也就自动改变了
def pathPrefix = "${bakPath}/${baseInfo}/${variantName}"
def name = "${project.name}-${variantName}"
baseApkFile = "${pathPrefix}/${name}.apk"
baseProguardMappingFile = "${pathPrefix}/${name}-mapping.txt"
baseResourceRFile = "${pathPrefix}/${name}-R.txt"
-
最后运行 tinkerPatchRelease task 构建补丁包,若无渠道,可通过./gradlew tinkerPatchRelease构建,我这个有一个小米渠道,可以通过./gradlew tinkerPatchXiaomiRelease构建。或者最简单的方式如图:
-
完成后,补丁包将位于 build/outputs/tinkerPatch下。
3.发布补丁包
- 将刚所得的补丁包patch_signed_7zip.apk重命名,去掉.apk后缀,我将其命名为patch_signed。
-
然后回到tinker平台上,找到我们的app,然后新建版本,我们代码中的appVersion为1.0.0
-
上传补丁包并下发:
4.重新安装我们的基准包,然后刷新tinker后台
5.杀掉进程,重新启动。补丁加载成功
成功接入了~给个小心心鼓励一下吧