TinkerPatch热更新

一、基础

这里使用TinkerPatch也是由于具有完善的后台管理和简洁性,作为小白是个不错的热更新入门sdk。
我们知道在打包后的apk修改为zip文件后可以看到内部的结构是这样的:
github
java代码会在编译期间转为.class文件,有使用dex工具将.class文件转为.dex文件,是一个可执行文件,也是主要的运行文件。在运行app后便会对其优化出现odex的缓存文件(Davlik)或oat文件(Art),Android安装的Apk文件实质就是zip结尾的压缩文件。在遍历dex时便会将所有的方法存放在一个数组里面,他的长度时65535,这也就是在大项目上打包常会出现的一个问题,方法数超过数组上限后无法打包。
Tinkder则是通过提供Dex差量包,整体替换Dex的方法。在打包时,都会根据基础包上进行打包,通过Dex查分算法生成差异包patch.dex,将差异包分发致客户端后,客户端会根据旧Dex与新patch.dex合并为新的dex,之后将这个新的dex文件插入数组前面使其最先执行该文件,从而修复了原有的bug。

二、代码

注意这里在集成sdk过程中最好紧跟官方文档:
http://tinkerpatch.com/Docs/SDK

1.添加 gradle 插件依赖

在项目根目录build.gradle中添加插件依赖

buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.2'
// TinkerPatch 插件
classpath "com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:1.2.8"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
}

2.集成TinkerPatch SDK

在主Module下build.gradle中添加依赖

apply plugin: 'tinkerpatch-support'
android {
defaultConfig {
...
multiDexEnabled true
}
...
dexOptions {
javaMaxHeapSize "2g"
preDexLibraries = false
jumboMode = true
}
}
dependencies {
...
// TinkerPatch依赖
provided("com.tinkerpatch.tinker:tinker-android-anno:1.9.8")
compile("com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.2.8")
// 包混合依赖
compile("com.android.support:multidex:1.0.1")
}


在官方文档中推荐独立编写tinker.gradle来管理tinkerpatch依赖管理,这里关系复杂就放在了build.gradle中
Project
|->app
| \—>build.gradle(tinker配置)
|
|->build.gradle(项目常数及基本配置)
->config.gradle

3.配置 tinkerpatchSupport 参数

apply plugin: 'tinkerpatch-support'
def bakPath = file("${buildDir}/bakApk/")
// 这个在打包Apk时可以不需要在意文件夹是否存在
// 但是在生成差异包时就需要修改,必须确定路径存在
def baseInfo = "app-1.0.0-1018-16-00-55"
def variantName = 'release'

tinkerpatchSupport {
/** 可以在debug的时候关闭 tinkerPatch **/
tinkerEnable = true

/** 是否使用一键接入功能 **/
reflectApplication = true

/** 是否开启加固模式,只有在使用加固时才能开启此开关 **/
protectedApp = false

/** 补丁是否支持新增 Activity (exported必须为false)**/
supportComponent = false

autoBackupApkPath = "${bakPath}"

/** 在tinkerpatch.com得到的appKey **/
appKey = "yourAppKey"
/** 注意: 若发布新的全量包, appVersion一定要更新 **/
appVersion = "1.0.0"

def pathPrefix = "${bakPath}/${baseInfo}/${variantName}/"
def name = "${project.name}-${variantName}"

// 这里的参数基本不变后期也可以自行修改基准包路径,路径以全局变量方式
// def bakPath = file("${buildDir}/bakApk/")
// def baseInfo = "app-1.0.0-1018-16-00-55"
baseApkFile = "${pathPrefix}/${name}.apk"
baseProguardMappingFile = "${pathPrefix}/${name}-mapping.txt"
baseResourceRFile = "${pathPrefix}/${name}-R.txt"
}
android {
...
}
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"
// path = "/usr/local/bin/7za"
}
buildConfig {
keepDexApply = false
}
}

在官方文档中有更全面的描述及使用
github

这里需要注意的几个:

reflectApplication:是否反射 Application 实现一键接入;一般来说,接入 Tinker 我们需要改造我们的 Application, 若这里为 true, 即我们无需对应用做任何改造即可接入。
appKey:在平台上注册app的key
appVersion:tinkerpatch便是以此code来判定是否更新的依据,以为具有与VersionName同样的性质,所以一般情况下我们会直接使用同一个数值来管理VersionName和Tinker的appVersion,但是在多渠道打包时就需要进行额外处理,以区分多渠道版本
baseApkFile:基准包的文件路径, 对应 tinker 插件中的 oldApk 参数,也是我们在打包时出现的位置。上述的路径可以经测试指到’project/build/bakApk’

github
这里由于使用多渠道,所以名称和单一渠道的文件名不同,不过基本相似。

4.初始化功能

这里由于我的reflectApplication=false,所以也是继承自己的Application

public class SampleApplication extends Application {

...

@Override
public void onCreate() {
super.onCreate();
// 我们可以从这里获得Tinker加载过程的信息
tinkerApplicationLike = TinkerPatchApplicationLike.getTinkerPatchApplicationLike();

// 初始化TinkerPatch SDK, 更多配置可参照API章节中的,初始化SDK
TinkerPatch.init(tinkerApplicationLike)
.reflectPatchLibrary()
.setPatchRollbackOnScreenOff(true)
.setPatchRestartOnSrceenOff(true);

TinkerPatch.with().fetchPatchUpdateAndPollWithInterval();
}

是的,只需要复制代码,即可完成傻瓜式的热更新功能,当然文档中也有详细描述API功能。
注意:初始化的代码建议紧跟 super.onCreate(),并且所有进程都需要初始化,已达到所有进程都可以被 patch 的目的
如果你确定只想在主进程中初始化 tinkerPatch,那也请至少在 :patch 进程中初始化,否则会有造成 :patch 进程crash,无法使补丁生效

5.使用步骤

是的,代码就上述这么多,完成好后就是后续的打包和生成差异包的工作了。
github
这幅图可以看到我们需要使用的工具,上面的方框用来打Apk包,下面的方框用来生成差异包。这里由于多渠道的关系,生成了许多不想干的功能,不需要在意。

首先我们双击assembleRelease构建最新的Apk

当然,为了测试看清出,clear-build是打包前的准备工作。此时就会在build文件夹中生成相应的apk
github
在文件夹中就会出现对应的apk了,不需要在意后续文件夹名称,这是我们打得基准包,就是说后续的差异包都需要在他,需要保存好。

修改文件夹名称

可以看到上述生成的app-1.0.7-1019-09-01-16,他的规则不难看出是app+tinkerVersion+时间,此时因为需要根据这个里面的app来生成差异包,所以在上面的baseInfo文件夹名称中修改他指向为该app的路径即可。
同步一下就可以看到是否指向成功,如果失败可以println打印日志看一下出错的位置。
github
最后一定要将自己的tinker中的appVersion变高,毕竟是需要提高版本来升级的。

生成差异包

上述准备成功后,我们就可以生成我们的差异包了。首先运行之前讲的下面方框中的tinkerPatchRelease生成差异包,稍等片刻后就好了。
github

可以清楚看到output中生成了我们需要的tinker文件夹,其中我们需要的就是patch_signed_7zip.apk,我们将它取出来,去除.apk的后缀名称,因为部分手机可能不会下载带.apk的文件,从而发生无法更新的问题。

上传差异包

首先创建版本,需要注意填写版本号一定是基础包的版本号,而不是这个差异包的版本号。
github
最后只需要上传文件夹和写相应的描述就可以上传啦。

1.在测试的时候首先清除安装在手机中基准包app的进程,
2.在重新进入app,刷新我们的版本管理工具就会看到app正在下载、合并,
3.合并成功后,再清除进程重新进入,就是一次完整的热更新了。

后续我们再更新的时候,

1.只需要将tinker的appVersion提高,
2.生成app差异包,
3.上传差异包就好了。

三、多渠道

上述是单需求的app,问题并不大。但是在多渠道打包时,就显得麻烦。
我们知道在热更新时首先判断是否有更高版本,再确认是否下载最新的差异包。但是在面对多渠道过程时,我们首先需要确定的是这个版本是否相同,其次才是确定是否有更高版本的差异包。

1.准备多渠道功能

在使用tinker多渠道前,我们需要为自己的项目增加多渠道功能。

android {
...
flavorDimensions "base","zone"
}
productFlavors {
main {
dimension "base"
}
x {
dimension "zone"
buildConfigField("String", "type", "\"x\"")
}
y {
dimension "zone"
buildConfigField("String", "type", "\"y\"")
}
}

这里我们使用多维度的方式生成两个渠道:mainxmainy。同步后就会发现在gradle中有给不同包的命令。这里在打包时只需要记住assembleRelease就可以了

2.准备tinker的多渠道适配

在文件中增加以下代码

tinkerpatchSupport {
...
productFlavors {
flavor {
flavorName = "mainx"
appVersion = "A${tinkerpatchSupport.appVersion}"
println("mainX====appVersion:"+appVersion)

pathPrefix = "${bakPath}/${baseInfo}/${flavorName}-${vative}"
name = "${project.name}-${flavorName}-${vative}"

baseApkFile = "${pathPrefix}/${name}.apk"
println("mainX=====baseApkFile:" + baseApkFile)

baseProguardMappingFile = "${pathPrefix}/${name}-mapping.txt"
println("mainX=====baseProguardMappingFile:" + baseProguardMappingFile)

baseResourceRFile = "${pathPrefix}/${name}-R.txt"
println("mainX=====baseResourceRFile:" + baseResourceRFile)
}
flavor {
flavorName = "mainy"
appVersion = "B${tinkerpatchSupport.appVersion}"
println("mainY====appVersion:"+appVersion)

pathPrefix = "${bakPath}/${baseInfo}/${flavorName}-${vative}"
name = "${project.name}-${flavorName}-${vative}"

baseApkFile = "${pathPrefix}/${name}.apk"
println("mainY=====baseApkFile:" + baseApkFile)

baseProguardMappingFile = "${pathPrefix}/${name}-mapping.txt"
println("mainY=====baseProguardMappingFile:" + baseProguardMappingFile)

baseResourceRFile = "${pathPrefix}/${name}-R.txt"
println("mainY=====baseResourceRFile:" + baseResourceRFile)
}
}
}

flavorName:渠道名称
appVersion:覆盖tinkerid版本号

可以看到这里的appVersion是A1.x.x,A是我们给这个版本的一个特殊标志,后面的则是我们之前写好的版本号。

这里增加了一个setAppChannel是用来指明渠道名称的,这里由于在gradle中增加了type,所以设置的字符串是 mainx 和 mainy。

TinkerPatch.init(TestApplication.tinkerApplicationLike)
.reflectPatchLibrary()
.setPatchRollbackOnScreenOff(true)
.setPatchRestartOnSrceenOff(true)
.setAppChannel("main"+BuildConfig.type);

3.打包生成Apk

clear-build后,我们双击assembleRelease
github
可以看到我们在app-1.0.7-1019-10-02-35的文件夹中生成了两个渠道的app文件。

之后就可以修改baseInfo的路径名称,修改tinkerId的版本号了。做好这些准备并同步后,开始生成差异包。
在生成差异包的时候需要注意,我们必须选择给哪一个渠道单独生成差异包
这里我们选择tinkerPatchMainYRelease
github
完成后就可以看到这里的patch包已经生成好了。

4.上传差异包

这里在输入版本号的时候需要注意,我们之前写的是1.0.7,但是在这里我们需要写B1.0.7,因为为了区别另一个渠道mainx这里的tinkerId版本号就用此来区别。

剩下的都和上述一样。