Android 微信熱修復 Tinker 接入過程及使用方法

一、前言

學習熱修復 Tinker 的使用,主要有兩個原因:

  1. 業務需要:項目會出現一些細小的bug,需要等到發佈新版才能修復,因此一直有關注熱修復這塊的技術。
  2. 技術驅動:這是一件需要一直保持的事情,不斷的學習,與時俱進,才能提高自己。

二、Tinker 接入指南

熱補丁:讓應用能夠在無需重新安裝的情況實現更新,幫助應用快速建立動態修復能力。

心理準備:Tinker 的接入相對HotFix、AndFix 等要複雜一點,參考了幾篇文章,前後分別建了 TinkerDemo、官方DEMO、TinkerFixDemo、TinkerTryDemo。到最後 TinkerTryDemo 才完全弄好。

這裏寫圖片描述

2.1、gradle接入

gradle是推薦的接入方式,在gradle插件tinker-patch-gradle-plugin中我們幫你完成proguard、multiDex以及Manifest處理等工作。

添加gradle依賴:在項目的build.gradle中,添加tinker-patch-gradle-plugin的依賴

buildscript {
    dependencies {
        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.7')
    }
}

然後在app的gradle文件app/build.gradle,我們需要添加tinker的庫依賴以及apply tinker的gradle插件.

dependencies {
    //可選,用於生成application類 
    provided('com.tencent.tinker:tinker-android-anno:1.7.7')
    //tinker的核心庫
    compile('com.tencent.tinker:tinker-android-lib:1.7.7') 
}
...
...
//apply tinker插件
apply plugin: 'com.tencent.tinker.patch'

2.2、Tinker 參數 配置

 /**Tinker 相關配置----------------開始-----------------------------------*/
        def bakPath = file("${buildDir}/bakApk/")
        def gitSha() {
        try {
        String gitRev = '11.2.3.4'
        if (gitRev == null) {
            throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
        }
        return gitRev
        } catch (Exception e) {
            throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
            }
        }
        ext {
            //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
            //Tinker是否可用
        tinkerEnabled = false
        //for normal build
        //old apk file to build patch apk 舊包名
        tinkerOldApkPath = "${bakPath}/tinker_demo-debug-1121-17-43-24.apk"
        //proguard mapping file to build patch apk  舊包混淆文件目錄
        tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"
        //resource R.txt to build patch apk, must input if there is resource changed 舊包R文件
        tinkerApplyResourcePath = "${bakPath}/tinker_demo-debug-1121-17-43-24-R.txt"
        //only use for build all flavor, if not, just ignore this field  多渠道
        tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
        }
        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() {
            return hasProperty("TINKER_ID") ? TINKER_ID : gitSha()
        }
        def buildWithTinker() {
            return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
        }
        def getTinkerBuildFlavorDirectory() {
            return ext.tinkerBuildFlavorDirectory
        }
        if (buildWithTinker()) {
            apply plugin: 'com.tencent.tinker.patch'
            tinkerPatch {
                /**
                 * 基準apk包路徑,也就是舊包路徑
                 * */
                oldApk = getOldApkPath()
                /**
                 * 如果出現以下的情況,並且ignoreWarning爲false,我們將中斷編譯。因爲這些情況可能會導致編譯出來的patch包
                 * 帶來風險:
                 * 1. minSdkVersion小於14,但是dexMode的值爲"raw";
                 * 2. 新編譯的安裝包出現新增的四大組件(Activity, BroadcastReceiver...);
                 * 3. 定義在dex.loader用於加載補丁的類不在main dex中;
                 * 4. 定義在dex.loader用於加載補丁的類出現修改;
                 * 5. resources.arsc改變,但沒有使用applyResourceMapping編譯。
                 * */
                ignoreWarning = false
                /**
                 * 在運行過程中,我們需要驗證基準apk包與補丁包的簽名是否一致,我們是否需要爲你簽名
                 * */
                useSign = true

        buildConfig {
            /**
             * 可選參數;在編譯新的apk時候,我們希望通過保持舊apk的proguard混淆方式,從而減少補丁包的大小。這個只
             * 是推薦的,但設置applyMapping會影響任何的assemble編譯。
             * */
            applyMapping = getApplyMappingPath()
            /**
             * 可選參數;在編譯新的apk時候,我們希望通過舊apk的R.txt文件保持ResId的分配,這樣不僅可以減少補丁包的
             * 大小,同時也避免由於ResId改變導致remote view異常。
             * */
            applyResourceMapping = getApplyResourceMappingPath()
            /**
             * 在運行過程中,我們需要驗證基準apk包的tinkerId是否等於補丁包的tinkerId。這個是決定補丁包能運行在哪些
             * 基準包上面,一般來說我們可以使用git版本號、versionName等等。
             * */
            tinkerId = getTinkerIdValue()
        }
        dex {
            /**
             * 只能是'raw'或者'jar'。
             * 對於'raw'模式,我們將會保持輸入dex的格式。
             * 對於'jar'模式,我們將會把輸入dex重新壓縮封裝到jar。如果你的minSdkVersion小於14,你必須選擇‘jar’模式
             * ,而且它更省存儲空間,但是驗證md5時比'raw'模式耗時()
             * */
            dexMode = "jar"
            /**
             * 是否提前生成dex,而非合成的方式。這套方案即回退成Qzone的方案,對於需要使用加固或者多flavor打包(建
             * 議使用其他方式生成渠道包)的用戶可使用。但是這套方案需要插樁,會造成Dalvik下性能損耗以及Art補丁包可
             * 能過大的問題,務必謹慎使用。另外一方面,這種方案在Android N之後可能會產生問題,建議過濾N之後的用戶。
             */
            usePreGeneratedPatchDex = false
            /**
             * 需要處理dex路徑,支持*、?通配符,必須使用'/'分割。路徑是相對安裝包的,例如/assets/...
             */
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            /**
             *     這一項非常重要,它定義了哪些類在加載補丁包的時候會用到。這些類是通過Tinker無法修改的類,也是一定要放在main dex的類。
             這裏需要定義的類有:
             1. 你自己定義的Application類;
             2. Tinker庫中用於加載補丁包的部分類,即com.tencent.tinker.loader.*;
             3. 如果你自定義了TinkerLoader,需要將它以及它引用的所有類也加入loader中;
             4. 其他一些你不希望被更改的類,例如Sample中的BaseBuildInfo類。這裏需要注意的是,這些類的直接引用類也
             需要加入到loader中。或者你需要將這個類變成非preverify。
             */
            loader = ["com.tencent.tinker.loader.*",
                      //warning, you must change it with your application
                    //TODO 換成自己的Application
                      "com.material.hbl.tinker_demo.MyApplication",
            ]
        }
        lib {
            /**
             * 需要處理lib路徑,支持*、?通配符,必須使用'/'分割。與dex.pattern一致, 路徑是相對安裝包的,例如/assets/...
             */
            pattern = ["lib/armeabi/*.so"]
        }
        res {
            /**
             * 需要處理res路徑,支持*、?通配符,必須使用'/'分割。與dex.pattern一致, 路徑是相對安裝包的,例如/assets/...,
             * 務必注意的是,只有滿足pattern的資源纔會放到合成後的資源包。
             */
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
            /**
             * 支持*、?通配符,必須使用'/'分割。若滿足ignoreChange的pattern,在編譯時會忽略該文件的新增、刪除與修改。
             * 最極端的情況,ignoreChange與上面的pattern一致,即會完全忽略所有資源的修改。
             */
            ignoreChange = ["assets/sample_meta.txt"]
            /**
             * 對於修改的資源,如果大於largeModSize,我們將使用bsdiff算法。這可以降低補丁包的大小,但是會增加合成
             * 時的複雜度。默認大小爲100kb
             */
            largeModSize = 100
        }
        packageConfig {
            /**
             * configField("key", "value"), 默認我們自動從基準安裝包與新安裝包的Manifest中讀取tinkerId,並自動
             * 寫入configField。在這裏,你可以定義其他的信息,在運行時可以通過TinkerLoadResult.getPackageConfigByName得到相應的數值。但是建議直接通過修改代碼來實現,例如BuildConfig。
             */
            configField("patchMessage", "tinker is sample to use")
        }
        sevenZip {
            /**
             * 例如"com.tencent.mm:SevenZip:1.1.10",將自動根據機器屬性獲得對應的7za運行文件,推薦使用
             */
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
        }
        /**
         *  文件名                              描述
         *  patch_unsigned.apk                  沒有簽名的補丁包
         *  patch_signed.apk                  簽名後的補丁包
         *  patch_signed_7zip.apk              簽名後並使用7zip壓縮的補丁包,也是我們通常使用的補丁包。但正式發佈的時候,最好不要以.apk結尾,防止被運營商挾持。
         *  log.txt                              在編譯補丁包過程的控制檯日誌
         *  dex_log.txt                          在編譯補丁包過程關於dex的日誌
         *  so_log.txt                          在編譯補丁包過程關於lib的日誌
         *  tinker_result                      最終在補丁包的內容,包括diff的dex、lib以及assets下面的meta文件
         *  resources_out.zip                  最終在手機上合成的全量資源apk,你可以在這裏查看是否有文件遺漏
         *  resources_out_7z.zip              根據7zip最終在手機上合成的全量資源apk
         *  tempPatchedDexes                  在Dalvik與Art平臺,最終在手機上合成的完整Dex,我們可以在這裏查看dex合成的產物。
         *
         *
         * */

        /**
         * 獲得所有渠道集合,並判斷數量
         */
        List<String> flavors = new ArrayList<>();
        project.android.productFlavors.each {flavor ->
            flavors.add(flavor.name)
        }
        boolean hasFlavors = flavors.size() > 0
        /**
         * bak apk and mapping
         *  創建Task並執行文件操作
         */
        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"
                        }

                    }
                }
            }
        }
        }
        }
        /**Tinker 相關配置----------------結束-----------------------------------*/

2.3、Application 構造

創建一個SampleApplication類,繼承DefaultApplicationLike:

@DefaultLifeCycle(application = "com.troy.tinkertrydemo.MyApplication",
        flags = ShareConstants.TINKER_ENABLE_ALL,
        loadVerifyFlag = false)

public class SampleApplication extends DefaultApplicationLike{

    public SampleApplication(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        /************************Action*************************/
        TinkerInstaller.install(this);
        /************************Action*************************/
    }
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
        getApplication().registerActivityLifecycleCallbacks(callback);
    }
}

其中DefaultLifeCycle中的Application爲我們真正的Application,flags表示需要支持的修復的範圍,然後將AndroidManifest.xml文件中的Application的name改爲我們的Application。

<application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

注意:“android:name=”.MyApplication””應該studio 會報紅,不過正常編譯能通過,可以忽略。

添加讀寫磁盤的權限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

三、具體操作

3.1、測試代碼

在將Tinker成功接入以後,可以寫一個簡單的例子來檢測一下是否可以使用。我在DEMO 中分別測試了,增加代碼,增加方法,增加類,增加布局文件,增加圖片,均能修復成功。

測試代碼見DEMO。

3.2、打BaseApk(有Bug的 Apk)

將Gradle中ext中的 tinkerEnabled 置爲true,然後點擊studio頂部的Build中Build APk,打一個DeBug包,成功之後會在Module的build目錄下生成一個文件夾,如圖:

這裏寫圖片描述

因爲是Debug 包所以只會有兩個文件,一個是APK,一個是對應的R 文件。如果有使用混淆的話,還會有一個mapping的混淆映射文件。
我們把 outputs/apk/ 中的apk文件拷出去,並安裝到自己的手機上,運行。

這時相等於 有bug 的apk 已經在用戶設備上運行了。

同時,將 Gradle 中ext 中的 tinkerOldApkPath 後的路徑設置爲上一步中bakApk 中的 apk 名稱,同理把 tinkerApplyResourcePath 設置爲 R 文件的名稱。其他的不用管。

3.3、生成 patch

修改代碼重新編譯之後,點擊Studio 右側的Gradle 工具欄,展開之後找到自己的Module 找到tinker, 雙擊其中的 tinkerPathDeBug , 圖示:

這裏寫圖片描述

運行之後會在:

這裏寫圖片描述

outputs下多出一個目錄,都是一些編譯打差異包過程的log和相關生成的apk,選擇其中的patch_signed_7zip.apk,拷出來,放在手機根目錄下。

重新啓動app,點擊load,tinker 會加載補丁,打印日誌信息如下:

PatchFileUtil: safeDeleteFile, try to delete path: /data/data/com.troy.tinkertrydemo/tinker/patch-9de9cc81/res/resources.arsc
patch I/Tinker.ResDiffPatchInternal: final new resource file:/data/data/com.troy.tinkertrydemo/tinker/patch-9de9cc81/res/resources.apk, entry count:342, size:474067
674 25862-patch I/Tinker.ResDiffPatchInternal: recover resource result:true, cost:124
674 25862-patch I/Tinker.DexDiffPatchInternal: dex count: 2, final wait time: 12
684 25862-patch I/Tinker.DexDiffPatchInternal: check dex optimizer file classes.dex.dex, size 5882288
684 25862-patch I/Tinker.DexDiffPatchInternal: check dex optimizer file test.dex.dex, size 12720
684 25862-patch I/PatchInfo: rewritePatchInfoFile file path:/data/data/com.troy.tinkertrydemo/tinker/patch.info , oldVer:ac6e19ae8ee0e84b8e02c5683dba8424, newVer:9de9cc8103630d0dc0ed839fb0b70157
684 25862-patch W/Tinker.UpgradePatch: UpgradePatch tryPatch: done, it is ok
684 25862-patch I/Tinker.DefaultPatchReporter: patchReporter onPatchResult: patch all result path:/storage/emulated/0/patch_signed_7zip.apk, success:true, cost:3545
 I/Tinker.DefaultTinkerResultService: DefaultTinkerResultService received a result:
                                                                                           PatchResult: 
                                                                                           isSuccess:true
                                                                                           rawPatchFilePath:/storage/emulated/0/patch_signed_7zip.apk
                                                                                           costTime:3545
                                                                                           patchVersion:9de9cc8103630d0dc0ed839fb0b70157

 I/Process: Sending signal. PID: 25862 SIG: 9
 W/Tinker.DefaultTinkerResultService: deleteRawPatchFile rawFile path: /storage/emulated/0/patch_signed_7zip.apk
 I/Tinker.PatchFileUtil: safeDeleteFile, try to delete path: /storage/emulated/0/patch_signed_7zip.apk

說明加載補丁成功,重新啓動App,會顯示修改的內容。

DEMO 下載

參考致謝:

(1)http://www.jianshu.com/p/f485af00672a
當時按照此寫的,最後才成功。
(2)、http://blog.csdn.net/a750457103/article/details/52815096
按照此寫的,一直報code -3,一直沒有解決。

tinkerId is not equal, base is tinker_id_1.0, but patch is 1.0

這裏寫圖片描述

(3)、http://blog.csdn.net/xiejc01/article/details/52735920
這個基本是官方的DEMO,按照此寫,會報錯:

Current version is 2.10. If using the gradle wrapper, try editing the distributionUrl in D:\workspaceghnice\tinker\tinker-sample-android\gradle\wrapper\gradle-wrapp

(4)、官方接入指南
https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97

文檔結構不是很清晰,沒有重點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章