两年前的调研,准备离职了不想白费之前的汗水因此发出来php
在gradle.propertites中指定tinker接入版本,例如:java
TINKER_VERSION=1.7.7
复制代码
dependencies {
classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"
}
复制代码
compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
复制代码
super(tinker_flag);
复制代码
这里能够考虑多写一个代理Application直接继承Application(即原HotFixProxyApplication),若是不打算接入Tinker,则能够直接在app模块的AndroidManifest.xml使用该代理Application。android
private void installTinker(Application application) {
if (isInstalled) {
TinkerLog.w(TAG, "install tinker, but has installed, ignore");
return;
}
Intent tinkerResultIntent = new Intent();
try {
//reflect tinker loader, because loaderClass may be define by user!
Class<?> tinkerLoadClass = Class.forName(TinkerLoader.class.getName(), false, getClassLoader());
Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class, int.class, boolean.class);
Constructor<?> constructor = tinkerLoadClass.getConstructor();
tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this, TINKER_FLAG, tinkerLoadVerifyFlag);
} catch (Throwable e) {
//has exception, put exception error code
ShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION);
tinkerResultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
}
Tinker tinker = new Tinker.Builder(application).build();
Tinker.create(tinker);
tinker.install(tinkerResultIntent);
isInstalled=true;
}
复制代码
至此,在HotFix SDK中已经完成了Tinker SDK的最初步的接入了。git
目前查询补丁的请求参数能够不变,返回参数中加一个补丁是不是TInker补丁说明字段便可。github
当补丁下载成功后咱们就能够调用如下方法尝试安装Tinker补丁了api
TinkerInstaller.onReceiveUpgradePatch(Context context, String patchLocation)
复制代码
Tinker补丁生效实际上有3个过程,一是补丁检验,二是补丁合成,三是补丁加载。补丁加载过程是在应用重启后进行的。安全
回调方法 | 描述 |
---|---|
onPatchResult | 这个是不管补丁合成失败或者成功都会回调的接口,它返回了本次合成的类型,时间以及结果等。默认咱们只是简单的输出这个信息,你能够在这里加上监控上报逻辑。 |
onPatchServiceStart | 这个是Patch进程启动时的回调,咱们能够在这里进行一个统计的工做。 |
onPatchPackageCheckFail | 补丁合成过程对输入补丁包的检查失败,这里能够经过错误码区分,例如签名校验失败、tinkerId不一致等缘由。默认咱们会删除临时文件。 |
onPatchVersionCheckFail | 对patch.info的校验版本合法性校验。若校验失败,默认咱们会删除临时文件。 |
onPatchTypeExtractFail | 从补丁包与原始安装包中合成某种类型的文件出现错误,默认咱们会删除临时文件。 |
onPatchDexOptFail | 对合成的dex文件提早进行dexopt时出现异常,默认咱们会删除临时文件。 |
onPatchInfoCorrupted | patch.info是用来管理补丁包版本的文件,这是在更新info文件时发生损坏的回调。默认咱们会卸载补丁包,由于此时咱们已经没法恢复了。 |
onPatchException | 在补丁合成过程捕捉到异常,十分但愿你能够把错误信息反馈给咱们。默认咱们会删除临时文件,而且将tinkerFlag设为不可用。 |
若是补丁合成失败,则能够经过回调方法上报HotFix服务器的report接口。现有的HotFix补丁错误码能够讨论继续增长。服务器
须要注意的是,PatchReporter中有个onPatchResult方法,这个方法是在补丁合成进程中进行的补丁合成结果回调方法,还有一个TinkerResultService中也有一个onPatchResult方法,TinkerResultService是补丁合成进程将合成结果返回给主进程的服务,Tinker默认的DefaultTinkerResultService是会杀掉:patch进程,假设当前是补丁升级而且成功了,Tinker会杀掉当前进程,让补丁包更快的生效,如果修复类型的补丁包而且失败了,Tinker会卸载补丁包。app
Tinker默认的ResultService不是很符合咱们当前HotFix的业务逻辑,所以ResultService必须重写。能够参考例子的HotFixTinkerResultService,在当前应用在退入后台或手机锁屏时这两个时机杀掉当前进程去应用补丁。当前也能够提供接口让接入者选择重启时机。ide
回调方法 | 描述 |
---|---|
onLoadResult | 这个是不管加载失败或者成功都会回调的接口,它返回了本次加载所用的时间、返回码等信息。默认咱们只是简单的输出这个信息,你能够在这里加上监控上报逻辑。 |
onLoadPatchListenerReceiveFail | 全部的补丁合成请求都须要先经过PatchListener的检查过滤。此次检查不经过的回调,它运行在发起请求的进程。默认咱们只是打印日志 |
onLoadPatchVersionChanged | 补丁包版本升级的回调,只会在主进程调用。默认咱们会杀掉其余全部的进程(保证全部进程代码的一致性),而且删掉旧版本的补丁文件。 |
onLoadFileNotFound | 在加载过程当中,发现部分文件丢失的回调。默认如果dex,dex优化文件或者lib文件丢失,咱们将尝试从补丁包去修复这些丢失的文件。若补丁包或者版本文件丢失,将卸载补丁包。 |
onLoadFileMd5Mismatch | 部分文件的md5与meta中定义的不一致。默认咱们为了安全考虑,依然会清空补丁。 |
onLoadPatchInfoCorrupted | patch.info是用来管理补丁包版本的文件,这是info文件损坏的回调。默认咱们会卸载补丁包,由于此时咱们已经没法恢复了。 |
onLoadPackageCheckFail | 加载过程补丁包的检查失败,这里能够经过错误码区分,例如签名校验失败、tinkerId不一致等缘由。默认咱们将会卸载补丁包 |
onLoadException | 在加载过程捕捉到异常。默认咱们会直接卸载补丁包 |
根据HotFix业务需求,须要重写LoadReporter的onLoadPatchVersionChanged方法,默认的onLoadPatchVersionChanged方法会杀掉其余全部的进程(保证全部进程代码的一致性),而且删掉旧版本的补丁文件。这就致使了HotFix监控进程也被杀掉,所以须要在这里排除掉HotFix监控进程,使其不被杀死。
在须要集成HotFix SDK 的工程的build.gradle中加入Tinker补丁包生成gradle任务 这一部分能够参考Tinker官方接入指南和示例,打包参数含义也有详尽的解释。
须要注意的是:在dex节点下的loader节点中加入须要排除的类,包括自定义的Application和HotFix SDK的类。
下面是我的的使用例子:
def bakPath = file("${buildDir}/bakApk/")
ext {
tinkerEnabled = true
//for normal build
//old apk file to build patch apk
tinkerOldApkPath = "${bakPath}/app-release-0228-15-14-21.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-release-0228-15-14-21-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-release-0228-15-14-21-R.txt"
//only use for build all flavor, if not, just ignore this field
// tinkerBuildFlavorDirectory = "${bakPath}/app-0217-16-54-37"
}
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() {
//versionCode做为TinkerId,这样就不须要git和commit一次
return android.defaultConfig.versionCode + ""
}
def buildWithTinker() {
return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}
def getTinkerBuildFlavorDirectory() {
return ext.tinkerBuildFlavorDirectory
}
if (buildWithTinker()) {
apply plugin: 'com.tencent.tinker.patch'
tinkerPatch {
oldApk = getOldApkPath()
ignoreWarning = true
useSign = true
tinkerEnable = buildWithTinker();
buildConfig {
applyMapping = getApplyMappingPath()
applyResourceMapping = getApplyResourceMappingPath()
tinkerId = getTinkerIdValue()
keepDexApply = false
}
dex {
dexMode = "jar"
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
loader = [
//use sample, let BaseBuildInfo unchangeable with tinker
"com.tencent.tinker.loader.*",
"com.cn21.HotTinker.MyApp",
"com.cn21.hotfix.*"
]
}
lib {
pattern = ["lib/*/*.so"]
}
res {
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
ignoreChange = ["assets/sample_meta.txt"]
largeModSize = 100
}
packageConfig {
configField("patchMessage", "tinker is sample to use")
configField("platform", "all")
configField("patchVersion", "1.0")
}
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
/**
* bak apk and mapping
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
def taskName = variant.name
def date = new Date().format("MMdd-HH-mm-ss")
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.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"
}
}
}
}
}
} else {
//HotFix插件配置
apply plugin: "cn.jiajixin.nuwa"
nuwa {
isCloseHotfixPatch = false
oldBuildHotfixDir = "${project.getProjectDir()}/old"
gradleBuildVersion = "1.5.0"
excludeClass = ["com.cn21.HotTinker.MyApp",
"android/support/multidex/MultiDex.class",
"com/cn21/hotfix/HotFixManager.class"]
excludePackage = ["com/tencent/tinker/lib",
"com/tencent/tinker/loader",
"com/tencent/tinker/commons"]
}
}
复制代码
示例工程HotTinker是在HotFix SDK工程的基础上作出如下改动:
super(TINKER_FLAG);
;private void installTinker(Application application)
方法,并在runOriginalApplication
中调用,进行Tinker的初始化onLoadPatchVersionChanged
方法,onLoadPatchVersionChanged
方法默认会杀掉主进程外的其余全部的进程(保证全部进程代码的一致性),不过Tinker补丁基本不会对HotFix SDK的监控服务进程进行修改,所以须要在这个方法中排除掉HotFix SDK的监控服务进程。UpgradePatchRetry
类onPatchResult
方法,由于默认的实现是补丁合成成功后就当即杀死当前应用进程,而这种方式确定不行的,HotFixTinkerResultService的作法是在用户锁屏的时候重启应用,固然也能够在其余合适的时机重启应用,还可让接入者在Manifest进行配置选择重启时机。<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.cn21.hotfix">
<application>
<!-- HotFix 服务-->
<service android:name="com.cn21.hotfix.service.HotFixService" android:process=":hotfix"/>
<service android:name="com.cn21.hotfix.service.HotFixTinkerResultService" android:exported="false"/>
</application>
</manifest>
复制代码
附:HotFix与Tinker兼容示例工程:HotTinker。