在HotFix SDK中接入Tinker


title: HotFix&&Tinker融合調研文檔 tags: hotfix,tinker,熱修復 grammar_cjkRuby: true

兩年前的調研,準備離職了不想白費之前的汗水因此發出來php

在HotFix SDK Library中接入Tinker

1. 指定Tinker SDK版本

gradle.propertites中指定tinker接入版本,例如:java

TINKER_VERSION=1.7.7
複製代碼

2. 添加Tinker gradle依賴

  • 項目build.gradle中添加tinker-patch-gradle-plugin的依賴
dependencies {
        classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"
    }
複製代碼
  • HotFix SDK library模塊下的build.gradle中添加tinker的庫依賴
compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
複製代碼

3. 在HotFix SDK 代理Application中初始化Tinker

  • 將HotFixProxyApplication 繼承自TinkerApplication 繼承繼承自TinkerApplication須要複寫其構造方法,方法實現直接調用父類的方法。
super(tinker_flag);
複製代碼

這裏能夠考慮多寫一個代理Application直接繼承Application(即原HotFixProxyApplication),若是不打算接入Tinker,則能夠直接在app模塊的AndroidManifest.xml使用該代理Applicationandroid

  • 在runOriginalApplication方法中的反射調用完原Application的attach方法後進行Tinker的初始化工做
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

4. 向服務器發起查詢補丁請求,若是返回的補丁是Tinker補丁類型,下載完成校驗經過則用Tinker SDK進行補丁安裝。

目前查詢補丁的請求參數能夠不變,返回參數中加一個補丁是不是TInker補丁說明字段便可。github

當補丁下載成功後咱們就能夠調用如下方法嘗試安裝Tinker補丁了api

TinkerInstaller.onReceiveUpgradePatch(Context context, String patchLocation)
複製代碼

Tinker補丁生效實際上有3個過程,一是補丁檢驗,二是補丁合成,三是補丁加載。補丁加載過程是在應用重啓後進行的。安全

  • 補丁檢驗 Tinker自帶了補丁前置檢驗功能,固然是最基礎的校驗,由於在補丁的合成和加載的時候還會進行其餘一些校驗,補丁前置檢驗包括是否開啓了Tinker補丁功能,補丁文件是否合法,當前進程是不是Tinker補丁服務進程等等。而咱們HotFix的XCHK校驗就須要經過拓展Tinker補丁檢驗回調方法來實現,方法自定義一個PatchListener繼承Tinker的DefaultPatchListenerpatchCheck方法,在其中進行XCHK校驗方法。 此外,還能夠在patchCheck方法中加入是不是谷歌Play渠道或者360渠道判斷(所以谷歌渠道不容許代碼下發,360渠道必需要通過360加固可是Tinker1.7.6之後不支持加固),應用得到的最大內存空間是否知足指定的最小內存空間要求等等。
  • 補丁合成 補丁合成結果會回調DefaultPatchReporter中的方法,而自定義PatchReporter繼承DefaultPatchReporter,複寫其中的回調方法。具體回調方法見下表
回調方法 描述
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

  • 補丁加載 LoadReporter類定義了Tinker在加載補丁時的一些回調,Tinker爲咱們提供了默認實現DefaultLoadReporter類,不過這確定不知足咱們的需求,由於在這裏咱們須要上報補丁加載的結果。;例子中的TinkerLoadReporter是繼承DefaultLoadReporter的類,在onLoadResult中能夠進行補丁安裝結果的上報,以及補丁加載失敗也能夠在這個方法中選擇重試安裝補丁。 補丁加載也有回調方法,見下表:
回調方法 描述
onLoadResult 這個是不管加載失敗或者成功都會回調的接口,它返回了本次加載所用的時間、返回碼等信息。默認咱們只是簡單的輸出這個信息,你能夠在這裏加上監控上報邏輯。
onLoadPatchListenerReceiveFail 全部的補丁合成請求都須要先經過PatchListener的檢查過濾。此次檢查不經過的回調,它運行在發起請求的進程。默認咱們只是打印日誌
onLoadPatchVersionChanged 補丁包版本升級的回調,只會在主進程調用。默認咱們會殺掉其餘全部的進程(保證全部進程代碼的一致性),而且刪掉舊版本的補丁文件。
onLoadFileNotFound 在加載過程當中,發現部分文件丟失的回調。默認如果dex,dex優化文件或者lib文件丟失,咱們將嘗試從補丁包去修復這些丟失的文件。若補丁包或者版本文件丟失,將卸載補丁包。
onLoadFileMd5Mismatch 部分文件的md5與meta中定義的不一致。默認咱們爲了安全考慮,依然會清空補丁。
onLoadPatchInfoCorrupted patch.info是用來管理補丁包版本的文件,這是info文件損壞的回調。默認咱們會卸載補丁包,由於此時咱們已經沒法恢復了。
onLoadPackageCheckFail 加載過程補丁包的檢查失敗,這裏能夠經過錯誤碼區分,例如簽名校驗失敗、tinkerId不一致等緣由。默認咱們將會卸載補丁包
onLoadException 在加載過程捕捉到異常。默認咱們會直接卸載補丁包

根據HotFix業務需求,須要重寫LoadReporteronLoadPatchVersionChanged方法,默認的onLoadPatchVersionChanged方法會殺掉其餘全部的進程(保證全部進程代碼的一致性),而且刪掉舊版本的補丁文件。這就致使了HotFix監控進程也被殺掉,所以須要在這裏排除掉HotFix監控進程,使其不被殺死。

Tinker補丁包生成

在須要集成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改動說明

示例工程HotTinker是在HotFix SDK工程的基礎上作出如下改動:

  • 修改HotFixProxyApplication
    • 使其繼承TinkerApplication,添加構造函數並直接調用super(TINKER_FLAG);
    • 添加 private void installTinker(Application application) 方法,並在runOriginalApplication中調用,進行Tinker的初始化
    • 這裏考慮多寫一個直接繼承Application的HotFix應用代理,當接入者不考慮接入Tinker時能夠避免沒必要要的消耗。
  • 新增com.cn21.hotfix.tinkerReporter包,在這個包下新增了:
    • HotFixTinkerReport類:在這個類中定義了真正的上報接口Reporter不過並無真正實現,而TinkerLoadReporter和TinkerPatchReporter都是調用了HotFixTinkerReport中上報方法,具體的上報須要Reporter實現類去完成。這裏須要完成的是根據HotFix的API協議修改HotFixTinkerReport類中上報結果碼,以及實現Reporter接口完成真正的打補丁結果上報HotFix服務器。
      • TinkerLoadReporter類:Tinker補丁加載結果回調類,這裏須要補充的是補丁加載回調結果上報以及重寫onLoadPatchVersionChanged方法,onLoadPatchVersionChanged方法默認會殺掉主進程外的其餘全部的進程(保證全部進程代碼的一致性),不過Tinker補丁基本不會對HotFix SDK的監控服務進程進行修改,所以須要在這個方法中排除掉HotFix SDK的監控服務進程。
      • TinkerPatchReporter類:Tinker補丁合成結果回調類,這裏須要補充的是若是合成失敗須要進行上報失敗緣由給HotFix服務器,若是合成失敗能夠選擇重試(可選)。可參考Tinker官方示例工程tinker-sample-androidUpgradePatchRetry
      • TinkerPatchListener類:Tinker補丁前置檢查,在這裏進行了應用是否爲谷歌play渠道檢查,內存空間大小檢查以及補丁文件是否存在檢查等等,須要補充的是HotFix補丁xchk檢查以及是不是360渠道檢查(不支持360渠道)
  • utils包下新增TinkerUtils類
  • service包下新增HotFixTinkerResultService類:補丁合成進程將合成結果返回給主進程的類,須要修改onPatchResult方法,由於默認的實現是補丁合成成功後就當即殺死當前應用進程,而這種方式確定不行的,HotFixTinkerResultService的作法是在用戶鎖屏的時候重啓應用,固然也能夠在其餘合適的時機重啓應用,還可讓接入者在Manifest進行配置選擇重啓時機。
  • 將app模塊中的AndroidManifest.xmlHotFixService移到了library模塊的AndroidManifest.xml,並添加了自定義的HotFixTinkerResultService service節點。
<?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

相關文章
相關標籤/搜索