1. 程式人生 > >Tinker爬坑之路

Tinker爬坑之路

目的

熱修復去年年底出的時候,變成了今年最火的技術之一。依舊記得去年面試的時候統一的MVP,然而今年卻變成了RN,熱修復。這不得不導致我們需要隨時掌握最新的技術。不然可能隨時會被淘汰。記得剛進公司,技術總監就給我們佈置了學習RN和熱修復的任務。不過因為臨時調整去了產品部。學習的時間變少了。最近抽空看了關於熱修復。於是,便有了此文。

概述

關於熱修復,說白了就是線上修復,這個基本就不用多解釋了。因為如果出了緊急bug。天天更新。使用者不嫌煩我都嫌煩。而且頻繁更新會導致使用者的損失。

現在熱修復的技術基本上有阿里的AndFix/Hotfix、QZone的方案、美團提出的思想方案以及騰訊的Tinker等。

我記得andfix我花了半天,hotfix花了半小時,tinker花了半個月。總之踩了蠻多的坑,走了蠻多的彎路的。

andfix算是比較簡單的把。不過相容性太差。hotfix用的阿里百川的後臺,使用方法也不難。QZone的思路可以去看看。至於美團,應該是相容性最好的了。據說,已經能相容99%的手機了。最後,就是tinker了。tinker官方介紹有這麼一句話:

Tinker已執行在微信的數億Android裝置上,那麼為什麼你不使用Tinker呢?

其實我當時感覺hotfix就夠用了。不過當我看了這句話,我就準備去看看了。不然到時候被人瞧不起就完了。“什麼?你除了tinker其他都不會?現在基本都是隻要會個tinker就行了。”不管它,不管他,我們先入坑在說。

入坑Tinker

tinker的接入方式有2種,一種的命令列接入,一種是gradle接入。

命令列接入

命令列的接入和andfix以及hotfix的使用方法是差不多的。我也沒去試~畢竟tinker是推薦用gradle的。好了。我們看看如何使用命令列接入的把。

專案依賴

我們需要在2個build.gradle進行

  • moudle的gradle中:
buildscript {
    dependencies {
        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.11'
) }
}
  • app的gradle中:
//apply tinker外掛
apply plugin: 'com.tencent.tinker.patch'
...
...
dependencies {
    //可選,用於生成application類 
    provided('com.tencent.tinker:tinker-android-anno:1.7.11')
    //tinker的核心庫
    compile('com.tencent.tinker:tinker-android-lib:1.7.11') 
}

然後我們進行sync now,會有如下一句話:
這裡寫圖片描述
tinkerId什麼鬼?我也不知道。然後發現我們接入了gradle還要對其進行配置。好了。那麼,我們接下來進行配置咯~

gradle配置

gradle有一屁股的引數。腦闊疼。我們先看看到底是幹啥的:

引數 預設值 描述
tinkerPatch 全域性資訊相關的配置項
tinkerEnable true 是否開啟tinker的功能。
oldApk null 基準apk包的路徑,必須輸入,否則會報錯。
newApk null 選填,用於編譯補丁apk路徑。如果路徑合法,即不再編譯新的安裝包,使用oldApk與newApk直接編譯。
outputFolder null 選填,設定編譯輸出路徑。預設在build/outputs/tinkerPatch
ignoreWarning false 如果出現以下的情況,並且ignoreWarning為false,我們將中斷編譯。因為這些情況可能會導致編譯出來的patch包帶來風險:
1. minSdkVersion小於14,但是dexMode的值為”raw”;
2. 新編譯的安裝包出現新增的四大元件(Activity, BroadcastReceiver…);
3. 定義在dex.loader用於載入補丁的類不在main dex中;
4. 定義在dex.loader用於載入補丁的類出現修改;
5. resources.arsc改變,但沒有使用applyResourceMapping編譯。
useSign true 在執行過程中,我們需要驗證基準apk包與補丁包的簽名是否一致,我們是否需要為你簽名。
buildConfig 編譯相關的配置項
applyMapping null 可選引數;在編譯新的apk時候,我們希望通過保持舊apk的proguard混淆方式,從而減少補丁包的大小。這個只是推薦設定,不設定applyMapping也不會影響任何的assemble編譯
applyResourceMapping null 可選引數;在編譯新的apk時候,我們希望通過舊apk的R.txt檔案保持ResId的分配,這樣不僅可以減少補丁包的大小,同時也避免由於ResId改變導致remote view異常
tinkerId null 在執行過程中,我們需要驗證基準apk包的tinkerId是否等於補丁包的tinkerId。這個是決定補丁包能執行在哪些基準包上面,一般來說我們可以使用git版本號、versionName等等。
keepDexApply false 如果我們有多個dex,編譯補丁時可能會由於類的移動導致變更增多。若開啟keepDexApply模式,補丁包將根據基準包的類分佈來編譯。
isProtectedApp false 是否使用加固模式,僅僅將變更的類合成補丁。注意,這種模式僅僅可以用於加固應用中。
dex dex相關的配置項
dexMode jar 只能是’raw’或者’jar’。
對於’raw’模式,我們將會保持輸入dex的格式。
對於’jar’模式,我們將會把輸入dex重新壓縮封裝到jar。如果你的minSdkVersion小於14,你必須選擇‘jar’模式,而且它更省儲存空間,但是驗證md5時比’raw’模式耗時。預設我們並不會去校驗md5,一般情況下選擇jar模式即可。
pattern [] 需要處理dex路徑,支援*、?萬用字元,必須使用’/’分割。路徑是相對安裝包的,例如assets/…
loader [] 這一項非常重要,它定義了哪些類在載入補丁包的時候會用到。這些類是通過Tinker無法修改的類,也是一定要放在main dex的類。
這裡需要定義的類有:
1. 你自己定義的Application類;
2. Tinker庫中用於載入補丁包的部分類,即com.tencent.tinker.loader.*;
3. 如果你自定義了TinkerLoader,需要將它以及它引用的所有類也加入loader中;
4. 其他一些你不希望被更改的類,例如Sample中的BaseBuildInfo類。這裡需要注意的是,這些類的直接引用類也需要加入到loader中。或者你需要將這個類變成非preverify。
5. 使用1.7.6版本之後版本,引數1、2會自動填寫。
lib lib相關的配置項
pattern [] 需要處理lib路徑,支援*、?萬用字元,必須使用’/’分割。與dex.pattern一致, 路徑是相對安裝包的,例如assets/…
res res相關的配置項
pattern [] 需要處理res路徑,支援*、?萬用字元,必須使用’/’分割。與dex.pattern一致, 路徑是相對安裝包的,例如assets/…,務必注意的是,只有滿足pattern的資源才會放到合成後的資源包。
ignoreChange [] 支援*、?萬用字元,必須使用’/’分割。若滿足ignoreChange的pattern,在編譯時會忽略該檔案的新增、刪除與修改。 最極端的情況,ignoreChange與上面的pattern一致,即會完全忽略所有資源的修改。
largeModSize 100 對於修改的資源,如果大於largeModSize,我們將使用bsdiff演算法。這可以降低補丁包的大小,但是會增加合成時的複雜度。預設大小為100kb
packageConfig 用於生成補丁包中的’package_meta.txt’檔案
configField TINKER_ID, NEW_TINKER_ID configField(“key”, “value”), 預設我們自動從基準安裝包與新安裝包的Manifest中讀取tinkerId,並自動寫入configField。在這裡,你可以定義其他的資訊,在執行時可以通過TinkerLoadResult.getPackageConfigByName得到相應的數值。但是建議直接通過修改程式碼來實現,例如BuildConfig。
sevenZip 7zip路徑配置項,執行前提是useSign為true
zipArtifact null 例如”com.tencent.mm:SevenZip:1.1.10”,將自動根據機器屬性獲得對應的7za執行檔案,推薦使用。
path 7za 系統中的7za路徑,例如”/usr/local/bin/7za”。path設定會覆蓋zipArtifact,若都不設定,將直接使用7za去嘗試。

好了。胡亂一通介紹完了。然後官方來了句具體參考sample的配置。於是乎,我當時興沖沖的把他的配置給複製過來了= =然後居然還是剛才的問題。我相信官方的demo他們敢放出來肯定沒問題的。那麼是我的問題咯?那就想辦法改咯。最後問了好多小夥伴,終於。build success了~神器!!!那麼為了讓更多小夥伴不再入我的坑位,我這邊貼出我的build.gradle的配置:



/**Tinker 相關配置----------------開始-----------------------------------*/
def gitSha() {
    try {
        String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
        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'")
    }
}

def javaVersion = JavaVersion.VERSION_1_7

def bakPath = file("${buildDir}/bakApk/")

/**
 * 你可以使用組裝版本構建你的基礎apk
 * you can use assembleRelease to build you base apk
 * 使用修補補丁釋出
 * use tinkerPatchRelease -POLD_APK=  -PAPPLY_MAPPING=  -PAPPLY_RESOURCE= to build patch
 * 新增apk到build/bakApk
 * add apk from the build/bakApk
 */
ext {
    /**
     * 出於某種原因,你可能想要忽略修改構建,例如即時執行除錯構建
     * for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
     */
    tinkerEnabled = true

    /** 對於正常的構建
     *  for normal build
     */

    /**
     * 老的apk去構建補丁的apk
     * old apk file to build patch apk
     */

    tinkerOldApkPath = "${bakPath}/app-debug-0605-16-34-49.apk"
    /**
     *  構建補丁apk的混淆對映檔案
     *  proguard mapping file to build patch apk
     */
    tinkerApplyMappingPath = "${bakPath}/app-debug-0605-16-34-49-mapping.txt"
    /**
     * 構建補丁apk時,R.txt必須輸入資原始檔是否改變
     * resource R.txt to build patch apk, must input if there is resource changed
     */

    tinkerApplyResourcePath = "${bakPath}/app-debug-0605-16-34-49-R.txt"

    /**
     * 只是用於構建所有的特點,如果不,可以忽略這一領域
     * only use for build all flavor, if not, just ignore this field
     */
    tinkerBuildFlavorDirectory = "${bakPath}/app-0605-16-34-49"
}


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 {
        /**
         * 必要的,預設為null
         * 舊的apk路徑,從build/bakApk中,用不同於新的apk去構建新增apk
         * necessary,default 'null'
         * the old apk path, use to diff with the new apk to build
         * add apk from the build/bakApk
         */
        oldApk = getOldApkPath()
        /**
         * 可選的,預設為false
         * 這邊有一些情況,我們或許會得到警告
         * 如果它為true,我們會維護補丁的過程
         * 情形1:最小sdk小於14,但你用原生的敏捷模式,當載入時,他必定會奔潰
         * 情形2:在manifest中新建的android元件,當載入時,他必定會崩潰
         * 情形3:在dex.loader不能保證在main dex中載入classes,他必須讓tinker無效
         * 情形4:在dex.loader載入classes檔案改變時,去載入補丁時,改變他們是不會奔潰的,但這些變化不影響,你可以忽視他們
         * 情形5:資原始檔改變是,我們不能用應用資源去對映
         * optional,default 'false'
         * there are some cases we may get some warnings
         * if ignoreWarning is true, we would just assert the patch process
         * case 1: minSdkVersion is below 14, but you are using dexMode with raw.
         *         it must be crash when load.
         * case 2: newly added Android Component in AndroidManifest.xml,
         *         it must be crash when load.
         * case 3: loader classes in dex.loader{} are not keep in the main dex,
         *         it must be let tinker not work.
         * case 4: loader classes in dex.loader{} changes,
         *         loader classes is ues to load patch dex. it is useless to change them.
         *         it won't crash, but these changes can't effect. you may ignore it
         * case 5: resources.arsc has changed, but we don't use applyResourceMapping to build
         */
        ignoreWarning = true

        /**
         * 可選的,預設為true
         * 是否簽署的補丁檔案,如果沒有,你必須自定義,否則不能成功的載入補丁,在你建立型別時,我們將使用構建簽訂
         * optional,default 'true'
         * whether sign the patch file
         * if not, you must do yourself. otherwise it can't check success during the patch loading
         * we will use the sign config with your build type
         */
        useSign = true

        /**
         * 可選的,預設為true
         * 是否使用tinker建立
         * optional,default 'true'
         * whether use tinker to build
         */
        tinkerEnable = true

        /**
         * 警告,應用對映將影響android的構建
         * Warning, applyMapping will affect the normal android build!
         */
        buildConfig {
            /**
             * 可選的,預設為null
             * 如果我們用tinker補丁去構建補丁包,如果minifyEnabled啟用了,你最好依賴舊的apk對映檔案
             * 警告:
             * 你一定要小心,他講影響正常的構建
             * optional,default 'null'
             * if we use tinkerPatch to build the patch apk, you'd better to apply the old
             * apk mapping file if minifyEnabled is enable!
             * Warning:
             * you must be careful that it will affect the normal assemble build!
             */
            applyMapping = getApplyMappingPath()
            /**
             * 可選的,預設為null
             * 他從R.txt中很好的儲存資源id,減少java的變化
             * optional,default 'null'
             * It is nice to keep the resource id from R.txt file to reduce java changes
             */
            applyResourceMapping = getApplyResourceMappingPath()

            /**
             * 必要的,預設為null
             * 因為我們不想在執行時用md5檢查基礎的apk檔案(因為太慢了)
             * 當嘗試應用補丁時,tinkerId來識別獨特的基礎apk
             * 我們用gif,svn或者簡單的版本控制,我們將在你的清單中自動建立tinkerId
             * necessary,default 'null'
             * because we don't want to check the base apk with md5 in the runtime(it is slow)
             * tinkerId is use to identify the unique base apk when the patch is tried to apply.
             * we can use git rev, svn rev or simply versionCode.
             * we will gen the tinkerId in your manifest automatic
             */
            tinkerId = "1.0"

            /**
             * 如果它為true,他將參考舊的apk
             * 開啟他可以減少dex檔案的大小
             * if keepDexApply is true, class in which dex refer to the old apk.
             * open this can reduce the dex diff file size.
             */
            keepDexApply = false
        }

        dex {
            /**
             * 可選的,預設為jar
             * 只能為raw或者jar,
             * optional,default 'jar',為raw時,我們保持原來的格式
             * 為jar時,我們會重新打包dexes和zip
             * 如果你的support小於14,你必須使用jar
             * 或者你想檢查更快的話,你必須使用raw
             * only can be 'raw' or 'jar'. for raw, we would keep its original format
             * for jar, we would repack dexes with zip format.
             * if you want to support below 14, you must use jar
             * or you want to save rom or check quicker, you can use raw mode also
             */
            dexMode = "jar"

            /**
             * 必要的,預設為[]
             * dex的apk預計處理或修補補丁麼?
             * necessary,default '[]'
             * what dexes in apk are expected to deal with tinkerPatch
             * it support * or ? pattern.
             */
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            /**
             * 必要的,預設為[]
             * 警告,這是非常非常重要的,載入classes時不能改變補丁,因此,他們講從補丁dex中移除
             * 你必須把這些類放入主的dex
             * 簡單,你應該新增自己的application,自己的tinkerloader和自己使用的classes
             * necessary,default '[]'
             * Warning, it is very very important, loader classes can't change with patch.
             * thus, they will be removed from patch dexes.
             * you must put the following class into main dex.
             * Simply, you should add your own application {@code tinker.sample.android.SampleApplication}
             * own tinkerLoader, and the classes you use in them
             *
             */
            loader = [
                    //use sample, let BaseBuildInfo unchangeable with tinker
                    "tinker.sample.android.app.BaseBuildInfo"
            ]
        }

        lib {
            /**
             * 可選的,預設[]
             *apk預處理修復補丁
             * 在assets的library中,我們就會在補丁目錄中恢復他們
             * 你可以在tinker中得到他的結果
             * optional,default '[]'
             * what library in apk are expected to deal with tinkerPatch
             * it support * or ? pattern.
             * for library in assets, we would just recover them in the patch directory
             * you can get them in TinkerLoadResult with Tinker
             */
            pattern = ["lib/*/*.so"]
        }

        res {
            /**
             * 可選的,預設[]
             * apk中資原始檔預計處理修理補丁
             * 在這個apk中,你必須包括你的所有資源,否則,他們不會重新打包到新的apk中
             * optional,default '[]'
             * what resource in apk are expected to deal with tinkerPatch
             * it support * or ? pattern.
             * you must include all your resources in apk here,
             * otherwise, they won't repack in the new apk resources.
             */
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

            /**
             * 可選的,預設[]
             * 資原始檔排除模式,忽視新增,刪除或修改的檔案
             * 警告,我們可以使用沒有與資源相對的檔案
             * optional,default '[]'
             * the resource file exclude patterns, ignore add, delete or modify resource change
             * it support * or ? pattern.
             * Warning, we can only use for files no relative with resources.arsc
             */
            ignoreChange = ["assets/sample_meta.txt"]

            /**
             * 預設1000kb
             * 為了資源修改,如果它比‘largemodsize’大,我們會使用bsdiff演算法去減少補丁檔案的大小
             * default 100kb
             * for modify resource, if it is larger than 'largeModSize'
             * we would like to use bsdiff algorithm to reduce patch file size
             */
            largeModSize = 100
        }

        packageConfig {
            /**
             * 可選的,預設..........
             * 包檔案的gen路徑是在assets/package_meta.txt 的補丁檔案
             * 你可以似乎用安全檢查。在你的包中檢查方法或者在載入結果中獲取包的屬性
             * 我們將會從舊的apk中得到tinkerID,其他配置檔案(如補丁訊息下面)是不必要的
             * optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE'
             * package meta file gen. path is assets/package_meta.txt in patch file
             * you can use securityCheck.getPackageProperties() in your ownPackageCheck method
             * or TinkerLoadResult.getPackageConfigByName
             * we will get the TINKER_ID from the old apk manifest for you automatic,
             * other config files (such as patchMessage below)is not necessary
             */
            configField("patchMessage", "tinker is sample to use")
            /**
             * 只是一個正常的情況下,你可以用sdk版本,渠道....
             * 你可以在樣本補丁監聽器解析它,然後你就有條件可以使用補丁
             * just a sample case, you can use such as sdkVersion, brand, channel...
             * you can parse it in the SamplePatchListener.
             * Then you can use patch conditional!
             */
            configField("platform", "all")
            /**
             * 通過包配置補丁版本
             * patch version via packageConfig
             */
            configField("patchVersion", "1.0")
        }
        //or you can add config filed outside, or get meta value from old apk
        //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
        //project.tinkerPatch.packageConfig.configField("test2", "sample")

        /**
         * 如果你不使用zip,我們只是嘗試使用7za
         * if you don't use zipArtifact or path, we just use 7za to try
         */
        sevenZip {
            /**
             * optional,default '7za'
             * the 7zip artifact path, it will use the right 7za with your platform
             */
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
            /**
             * optional,default '7za'
             * you can specify the 7za path yourself, it will overwrite the zipArtifact value
             */
//        path = "/usr/local/bin/7za"
        }
    }

    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"
                    }

                }
            }
        }
    }
}

差不多就這樣,原諒我蹩腳的英文,為了讓自己搞明白,翻譯了一個下午= = 翻譯好難。。。一般來說TinkerId是你軟體當前的版本號。然後我們進行sync now。build success。binggo搞定了。

這是我們需要進行gradle打包。具體怎麼打包,我們後面說,打包完成之後會在你的app/build/outputs/tinkerPatch生成一些檔案。

檔名 描述
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合成的產物。

這些都是官方的介紹。我記得當時我不會gradle打包的時候,執行完成後發現並沒有那些路徑。最後你拿在app/build/bakApk的old.apk和在app/build/outputs/tinkerPatch的new.apk進行對比。執行如下:

java -jar tinker-patch-cli.jar -old old.apk -new new.apk -config tinker_config.xml -out output_path

關於tinker_config.xml配置看官方介紹:tinker_config.xml
在執行命令前別忘了我們需要在AndroidMainfest.xml進行配置thinkerId:

<meta-data android:name="TINKER_ID" android:value="版本號"/>

最後我們進行載入就行了。載入方式我們下面會說。

Gradle接入

對於gradle的接入,官方還是比較推薦的,不過因為有很多人因為懶所以進行命令列接入。其實如果命令列接入你還不如用hotfix。下面我們繼續說下如何進行gradle接入。至於build.gradle的配置和命令列沒有什麼區別。所以我就不說第二遍 了。下面我們具體來看程式碼。
我們需要在Application進行初始化:

/**
 * com.angel.MyApplication才是真正的App,Tinker會自動生成一個
 */
@DefaultLifeCycle(application = "com.angel.MyApplication",
        flags = ShareConstants.TINKER_ENABLE_ALL,
        loadVerifyFlag = false)
public class MyApplication extends DefaultApplicationLike {


    private static final String APATCH_PATH = "/test.apk";

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

    public void onBaseContextAttached(Context context) {
        super.onBaseContextAttached(context);
        //you must install multiDex whatever tinker is installed!
        MultiDex.install(context);
        TinkerInstaller.install(this);
    }

}

MainActivity很簡單,我們來用一個button進行模擬熱修復:


    public void click(View view) {
//        textView.setText("一個不小心就被我熱修復了。");
        String patchFileString = Environment.getExternalStorageDirectory()
                .getAbsolutePath() + APATCH_PATH;
        File file = new File(patchFileString);
        if (file.exists()) {
            TinkerInstaller.onReceiveUpgradePatch(getApplication(), patchFileString);
            Log.i("TAG", "補丁包存在>>>>" + patchFileString);
        } else {
            Log.i("TAG", "補丁包不存在");
        }
    }

執行完如下:
這裡寫圖片描述
我們點選按鈕之後會發現,嗯哼,補丁包不存在。這就對了。
這裡寫圖片描述
現在我們需要修改一些配置:
這裡寫圖片描述
把gradle裡的配置成bakApk下的時間。配置完再說。

好了。配置完成。下面是重點了,gradle的打包。跟著我的圖,一步兩步慢動作,兩步一步慢動作重播。
這裡寫圖片描述
當然至於debug和relase就隨便你了哈。
這裡寫圖片描述

這樣我們進行打包完成了。然後我們會在這個路徑:app\build\outputs\tinkerPatch\debug生成對應的檔案。這個我前面講過了。然後我們把包扔到你指定的目錄中,例如我這邊是根目錄。就直接扔進去。然後我們來點選按鈕。(無需重啟點選)。會列印如下內容:
這裡寫圖片描述
成功了。然後他會直接閃退,官方所說的重啟我是沒碰到過。進入後我們在次點選按鈕。最後效果如下:
這裡寫圖片描述
這裡寫圖片描述
這樣修復就算完成了。

爬坑Tinker

前面我留了一個坑,不知道已經會的是否發現,我剛才只說把包扔到目錄裡。但我們的包有3個,到底是哪個。你們一個個的試會發現只有7z包列印的時候會是isSuccess:ture 而其他兩個全是false。至於為什麼,我也並不瞭解。我記得官方好像沒給答案。當然還有剛才說的他不會重啟。這tm是個很尷尬的事啊~我也沒搞懂為什麼。

說說我之前遇到的幾個坑把,怎麼說呢,其實並不算坑,只是平時碼程式碼的一些細節。一種情況是第一次沒加讀取許可權,第二次你加了之後是沒有用的,因為他要在第一個上進行讀取進行修復的。導致返回碼是-2。特地去官方看了這是因為補丁包不存在。我當時就矇蔽了。好吧,自己細節沒有注意= =

第二個自然就是gradle的接入了。我記得好像官方說是利用gradle進行打包。其實我並不懂= = 看來還是對gradle不熟悉哈。

總結

Tinker的爬坑之路算是結束了。其實發現只要配置完了,後面很簡單,主要還是配置的坑比較多。對比其他的熱修復,tinker肯定是首選,如果想簡單不改程式碼的話用hotfix也可以。不過還是建議看下QZone和美團的原理。聽說會讓你容易理解模組化的~~tinker的使用基本就這樣了。歡迎入坑,當然有問題可以一起來爬坑。

相關推薦

Tinker

目的 熱修復去年年底出的時候,變成了今年最火的技術之一。依舊記得去年面試的時候統一的MVP,然而今年卻變成了RN,熱修復。這不得不導致我們需要隨時掌握最新的技術。不然可能隨時會被淘汰。記得剛進公司,技術總監就給我們佈置了學習RN和熱修復的任務。不過因為臨時調整

Tinker + Bugly + Jenkins

eset down 文件中 text 空間 ack dsi In 無需 前陣子 Android 端的線上崩潰比較多,熱修復被提上日程。實現方案是 Tinker,Jenkins 打包,最後補丁包上傳到 Bugly 進行分發。主要在 Jenkins 打包這一塊爬了不少坑,現記

angular4(2)angular腳手引入第三方類庫

scrip 1-1 sta 引入 logs .json 類比 ima 第三方 如何在angular4腳手架中引入第三方類庫呢比如jquery、swiper、bootstrap。。。。。。 例如引入jquery:(其他類庫類比jquery即可) 第一步:在我們的項目目錄下

react(一)--報錯output.path不是絕對路徑

bpa file 文件 開始 put pac 這就是 文件頭部 之前   之前,一直在糾結是學習angular好,學習vue好,還是學習react好,網上一搜索,也是各種對比,各種互噴,看過之後更糾結。就跟小時候一樣糾結長大了是上清華好,還是上北大好,最後證明我想多了。總之

——用Fuel部署Openstack

電源 並且 blog src 下載 百度雲 pro open 組件 前段時間看到這個NASA發起這個開源的雲計算管理平臺項目,挺感興趣的。索性看了一些概念和原理就準備操作了。但是網上搭建完整版openstack組件的文檔,要不是網路源有問題就是環境有問題。很難流暢的安裝。我

Vue (一)—— 使用 vue-cli 搭建項目 (增補)

web http class 模板 clas ebp 搭建 com png cd 指定好安裝目錄 vue init webpack 項目名稱 執行 vue vue list 查看可應用模板 vue init webpack +名字

Vue (四)—— 與 Vuex 的第一次接觸

參數 之間 scrip span 變量 tle 技術分享 管理 tool 在 Vue.js 的項目中,如果項目結構簡單, 父子組件之間的數據傳遞可以使用 props 或者 $emit 等方式 http://www.cnblogs.com/wisewrong/p/62660

Vue (一)—— 使用 vue-cli 搭建項目

wrong run 自己 文件的 ima tca 來安 mage 前綴 vue-cli 是一個官方發布 vue.js 項目腳手架,使用 vue-cli 可以快速創建 vue 項目,GitHub地址是:https://github.com/vuejs/vue-cli 一、

Vue (二)—— 組件之間的數據傳遞

組件 圖片 NPU tar bubuko 9.png com clas entry Vue 的組件作用域都是孤立的,不允許在子組件的模板內直接引用父組件的數據。必須使用特定的方法才能實現組件之間的數據傳遞。 首先用 vue-cli 創建一個項目,其中 App.vue 是父組

Vue (三)—— 使用 vue-router 跳轉頁面

cli 分類 ace local outer log 適合 創建 start 使用 Vue.js 做項目的時候,一個頁面是由多個組件構成的,所以在跳轉頁面的時候,並不適合用傳統的 href,於是 vue-router 應運而生。 官方文檔: https://router.v

vue的(八)----IOS:Safari不兼容Javascript中的Date問題

找到 () 方案 ace 格式 nbsp 後臺 get repl 在IOS5以上版本(不包含IOS5)中的Safari瀏覽器能正確解釋出Javascript中的 new Date(‘2013-10-21‘) 的日期對象。 但是在IOS5版本裏面的Safar

新手pythoner

sort pri 數字 次數 添加 .so 定位 rem pytho 列表的各種基本操作 fruits_list=[‘蘋果‘,‘梨子‘,‘香蕉‘,‘西紅柿‘] print(fruits_list[1:3])#列表的切片,list[a:b:c]a,b表示切片的始末,c表示

Vue (十二)—— vue-cli 3.x 搭建項目

cas sas 環境變量 npm pack 全局 開發模式 文件 預編譯 Vue Cli 3 官方文檔:https://cli.vuejs.org/zh/guide/ 一、安裝 @vue/cli 更新到 3.x 之後,vue-cli 的包名從 vue-cli 改

java(一)——編譯可以正常編譯,執行時報unsupported major.minor version 52.0

寫了一個很簡單的helloworld,編譯可以正常編譯,執行時報unsupported major.minor version 52.0錯誤。 java程式碼如下: 用javac Hello.java命令編譯時,是沒有問題的,用java Hello執行時,怎麼也執行不了 後

Vue — vue-cli 3.x 搭建專案

一、安裝 @vue/cli 更新到 3.x 之後,vue-cli 的包名從vue-cli 改成了 @vue/cli 如果之前全域性安裝了舊版本的vue-cli (1.x 或 2.x),首先需要使用以下命令解除安裝掉 // 如果沒有安裝舊版本的 vue-cli 可以跳過解除安裝直接

專案實戰vue:vue框架中如何註冊全域性公共過濾器filter、全域性公共外掛、全域性公共元件component

專案需求: 在我們的實際專案的開發中,經常會遇到一些可以重複利用的方法和元件,比如:我們在一個專案中可能會經常用到格式化時間的方法。這個時候就需要我們把這個方法封裝起來,註冊成一個全域性的過濾器,在整個專案中都可以使用,這樣既可以提高程式碼的使用效率,也便於我們

vue+ele (一)—class

上手Vue已將近三月,在這裡記錄下我自己遇到的一些問題吧! class名的修改 1、第一種就是直接繫結 <div class="main-header"></div> 這種樣式方便大家直接給標籤加樣式,不足之處在於無法動態控制class 2、動

vue+ele tree(二)

後臺管理系統中,我們有個很必需的功能,那就是組織機構樹,我當時用這個的時候,不小心陷入了個誤區,導致我走了很多彎路,在這裡我就分享下我遇到的問題吧,希望和我一樣遇到這個問題的人可以一起探討下  在使用的時候,因為要區分一級和二級選單許可權,所以也有不同的判斷,其他的不多

Mint-ui infinite scroll 無限載入

問題一:下拉不會觸發loadMore事件 解決:我們的ul或者設定滾動的元素必須設定高度和溢位滾動的屬性 因為我的介面沒有分頁所以用slice模擬資料分頁載入 CSS程式碼: .newsList { max-height: 35rem;

Vue (二)—— 元件之間的資料傳遞

Vue 的元件作用域都是孤立的,不允許在子元件的模板內直接引用父元件的資料。必須使用特定的方法才能實現元件之間的資料傳遞。 首先用 vue-cli 建立一個專案,其中 App.vue 是父元件,components 資料夾下都是子元件。 一、父元件向子元件傳遞資料