兩年前的調研,準備離職了不想白費之前的汗水因此發出來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。