Tinker Android熱補(bǔ)丁

前言

Tinker簡(jiǎn)介

Tinker是微信官方的Android熱補(bǔ)丁解決方案宪巨,它支持動(dòng)態(tài)下發(fā)代碼、So庫(kù)以及資源溜畅,讓應(yīng)用能夠在不需要重新安裝的情況下實(shí)現(xiàn)更新捏卓。當(dāng)然,你也可以使用Tinker來(lái)更新你的插件慈格。

Tinker已知問(wèn)題

  1. Tinker不支持修改AndroidManifest.xml怠晴,Tinker不支持新增四大組件;
  2. 由于Google Play的開發(fā)者條款限制浴捆,不建議在GP渠道動(dòng)態(tài)更新代碼
  3. 在Android N上龄寞,補(bǔ)丁對(duì)應(yīng)用啟動(dòng)時(shí)間有輕微的影響;
  4. 不支持部分三星android-21機(jī)型汤功,加載補(bǔ)丁時(shí)會(huì)主動(dòng)拋出"TinkerRuntimeException:checkDexInstall failed"物邑;
  5. 對(duì)于資源替換,不支持修改remoteView滔金。例如transition動(dòng)畫色解,notification icon以及桌面圖標(biāo)。

2. Tinker接入指南

2.1. Gradle接入

gradle是推薦的接入方式餐茵,在gradle插件tinker-patch-gradle-plugin中官方幫助完成proguard科阎、multiDex以及Manifest處理等工作。
在項(xiàng)目的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的庫(kù)依賴以及apply tinker的gradle插件.

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

tinker參數(shù)配置

同樣是在項(xiàng)目的app的gradle文件app/build.gradle文件里,在文件的末尾加上如下相關(guān)配置:

/**Tinker 相關(guān)配置----------------開始-----------------------------------*/
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 = true
    //for normal build
    //old apk file to build patch apk 舊包名
    tinkerOldApkPath = "${bakPath}/app-debug-0711-15-50-35.apk"
    //proguard mapping file to build patch apk  舊包混淆文件目錄
    tinkerApplyMappingPath = "${bakPath}/app-debug-0711-15-50-35-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed 舊包R文件
    tinkerApplyResourcePath = "${bakPath}/app-debug-0711-15-50-35-R.txt"
    //only use for build all flavor, if not, just ignore this field  多渠道
    tinkerBuildFlavorDirectory = "${bakPath}/app-0711-15-50-35"
}
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 {
        /**
         * 基準(zhǔn)apk包路徑错英,也就是舊包路徑
         * */
        oldApk = getOldApkPath()
        /**
         * 如果出現(xiàn)以下的情況入撒,并且ignoreWarning為false,我們將中斷編譯椭岩。因?yàn)檫@些情況可能會(huì)導(dǎo)致編譯出來(lái)的patch包
         * 帶來(lái)風(fēng)險(xiǎn):
         * 1. minSdkVersion小于14茅逮,但是dexMode的值為"raw";
         * 2. 新編譯的安裝包出現(xiàn)新增的四大組件(Activity, BroadcastReceiver...);
         * 3. 定義在dex.loader用于加載補(bǔ)丁的類不在main dex中;
         * 4. 定義在dex.loader用于加載補(bǔ)丁的類出現(xiàn)修改判哥;
         * 5. resources.arsc改變献雅,但沒(méi)有使用applyResourceMapping編譯。
         * */
        ignoreWarning = false
        /**
         * 在運(yùn)行過(guò)程中塌计,我們需要驗(yàn)證基準(zhǔn)apk包與補(bǔ)丁包的簽名是否一致挺身,我們是否需要為你簽名
         * */
        useSign = true

        buildConfig {
            /**
             * 可選參數(shù);在編譯新的apk時(shí)候锌仅,我們希望通過(guò)保持舊apk的proguard混淆方式瞒渠,從而減少補(bǔ)丁包的大小。這個(gè)只
             * 是推薦的技扼,但設(shè)置applyMapping會(huì)影響任何的assemble編譯。
             * */
            applyMapping = getApplyMappingPath()
            /**
             * 可選參數(shù)嫩痰;在編譯新的apk時(shí)候剿吻,我們希望通過(guò)舊apk的R.txt文件保持ResId的分配,這樣不僅可以減少補(bǔ)丁包的
             * 大小串纺,同時(shí)也避免由于ResId改變導(dǎo)致remote view異常丽旅。
             * */
            applyResourceMapping = getApplyResourceMappingPath()
            /**
             * 在運(yùn)行過(guò)程中,我們需要驗(yàn)證基準(zhǔn)apk包的tinkerId是否等于補(bǔ)丁包的tinkerId纺棺。這個(gè)是決定補(bǔ)丁包能運(yùn)行在哪些
             * 基準(zhǔn)包上面榄笙,一般來(lái)說(shuō)我們可以使用git版本號(hào)、versionName等等祷蝌。
             * */
            tinkerId = getTinkerIdValue()
        }
        dex {
            /**
             * 只能是'raw'或者'jar'茅撞。
             * 對(duì)于'raw'模式,我們將會(huì)保持輸入dex的格式巨朦。
             * 對(duì)于'jar'模式米丘,我們將會(huì)把輸入dex重新壓縮封裝到j(luò)ar。如果你的minSdkVersion小于14糊啡,你必須選擇‘jar’模式
             * 拄查,而且它更省存儲(chǔ)空間,但是驗(yàn)證md5時(shí)比'raw'模式耗時(shí)()
             * */
            dexMode = "jar"
            /**
             * 是否提前生成dex棚蓄,而非合成的方式堕扶。這套方案即回退成Qzone的方案碍脏,對(duì)于需要使用加固或者多flavor打包(建
             * 議使用其他方式生成渠道包)的用戶可使用。但是這套方案需要插樁稍算,會(huì)造成Dalvik下性能損耗以及Art補(bǔ)丁包可
             * 能過(guò)大的問(wèn)題典尾,務(wù)必謹(jǐn)慎使用。另外一方面邪蛔,這種方案在Android N之后可能會(huì)產(chǎn)生問(wèn)題急黎,建議過(guò)濾N之后的用戶。
             */
            //usePreGeneratedPatchDex = false
            /**
             * 需要處理dex路徑侧到,支持*勃教、?通配符,必須使用'/'分割匠抗。路徑是相對(duì)安裝包的故源,例如/assets/...
             */
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            /**
             *     這一項(xiàng)非常重要,它定義了哪些類在加載補(bǔ)丁包的時(shí)候會(huì)用到汞贸。這些類是通過(guò)Tinker無(wú)法修改的類绳军,也是一定要放在main dex的類。
             這里需要定義的類有:
             1. 你自己定義的Application類矢腻;
             2. Tinker庫(kù)中用于加載補(bǔ)丁包的部分類门驾,即com.tencent.tinker.loader.*;
             3. 如果你自定義了TinkerLoader多柑,需要將它以及它引用的所有類也加入loader中奶是;
             4. 其他一些你不希望被更改的類,例如Sample中的BaseBuildInfo類竣灌。這里需要注意的是聂沙,這些類的直接引用類也
             需要加入到loader中〕踵冢或者你需要將這個(gè)類變成非preverify及汉。
             */
            loader = ["com.tencent.tinker.loader.*",
                      //warning, you must change it with your application
                      //TODO 換成自己的Application
                      "com.troy.tinkertrydemo.MyApplication",
            ]
        }
        lib {
            /**
             * 需要處理lib路徑,支持*屯烦、?通配符坷随,必須使用'/'分割。與dex.pattern一致, 路徑是相對(duì)安裝包的驻龟,例如/assets/...
             */
            pattern = ["lib/armeabi/*.so"]
        }
        res {
            /**
             * 需要處理res路徑甸箱,支持*、?通配符迅脐,必須使用'/'分割芍殖。與dex.pattern一致, 路徑是相對(duì)安裝包的,例如/assets/...谴蔑,
             * 務(wù)必注意的是豌骏,只有滿足pattern的資源才會(huì)放到合成后的資源包龟梦。
             */
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
            /**
             * 支持*、?通配符窃躲,必須使用'/'分割计贰。若滿足ignoreChange的pattern,在編譯時(shí)會(huì)忽略該文件的新增蒂窒、刪除與修改躁倒。
             * 最極端的情況,ignoreChange與上面的pattern一致洒琢,即會(huì)完全忽略所有資源的修改秧秉。
             */
            ignoreChange = ["assets/sample_meta.txt"]
            /**
             * 對(duì)于修改的資源,如果大于largeModSize衰抑,我們將使用bsdiff算法象迎。這可以降低補(bǔ)丁包的大小,但是會(huì)增加合成
             * 時(shí)的復(fù)雜度呛踊。默認(rèn)大小為100kb
             */
            largeModSize = 100
        }
        packageConfig {
            /**
             * configField("key", "value"), 默認(rèn)我們自動(dòng)從基準(zhǔn)安裝包與新安裝包的Manifest中讀取tinkerId,并自動(dòng)
             * 寫入configField砾淌。在這里,你可以定義其他的信息谭网,在運(yùn)行時(shí)可以通過(guò)TinkerLoadResult.getPackageConfigByName得到相應(yīng)的數(shù)值汪厨。但是建議直接通過(guò)修改代碼來(lái)實(shí)現(xiàn),例如BuildConfig愉择。
             */
            configField("patchMessage", "tinker is sample to use")
        }
        sevenZip {
            /**
             * 例如"com.tencent.mm:SevenZip:1.1.10"劫乱,將自動(dòng)根據(jù)機(jī)器屬性獲得對(duì)應(yīng)的7za運(yùn)行文件,推薦使用
             */
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
        }
        /**
         *  文件名                              描述
         *  patch_unsigned.apk                  沒(méi)有簽名的補(bǔ)丁包
         *  patch_signed.apk                  簽名后的補(bǔ)丁包
         *  patch_signed_7zip.apk              簽名后并使用7zip壓縮的補(bǔ)丁包薄辅,也是我們通常使用的補(bǔ)丁包。但正式發(fā)布的時(shí)候抠璃,最好不要以.apk結(jié)尾站楚,防止被運(yùn)營(yíng)商挾持。
         *  log.txt                              在編譯補(bǔ)丁包過(guò)程的控制臺(tái)日志
         *  dex_log.txt                          在編譯補(bǔ)丁包過(guò)程關(guān)于dex的日志
         *  so_log.txt                          在編譯補(bǔ)丁包過(guò)程關(guān)于lib的日志
         *  tinker_result                      最終在補(bǔ)丁包的內(nèi)容搏嗡,包括diff的dex窿春、lib以及assets下面的meta文件
         *  resources_out.zip                  最終在手機(jī)上合成的全量資源apk,你可以在這里查看是否有文件遺漏
         *  resources_out_7z.zip              根據(jù)7zip最終在手機(jī)上合成的全量資源apk
         *  tempPatchedDexes                  在Dalvik與Art平臺(tái)采盒,最終在手機(jī)上合成的完整Dex旧乞,我們可以在這里查看dex合成的產(chǎn)物。
         *
         *
         * */

        /**
         * 獲得所有渠道集合磅氨,并判斷數(shù)量
         */
        List<String> flavors = new ArrayList<>();
        project.android.productFlavors.each {flavor ->
            flavors.add(flavor.name)
        }
        boolean hasFlavors = flavors.size() > 0
        /**
         * bak apk and mapping
         *  創(chuàng)建Task并執(zhí)行文件操作
         */
        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")
                            }
                        }
                    }
                }
            }
        }
        /**
         * 如果有渠道則進(jìn)行多渠道打包
         */
        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 相關(guān)配置----------------結(jié)束-----------------------------------*/

配置參數(shù)參考GitHub:
官方github

自定義Application類

為了使真正的Application實(shí)現(xiàn)可以在補(bǔ)丁包中修改尺栖,需要把Appliction類的所有邏輯移動(dòng)到ApplicationLike代理類中。
即創(chuàng)建一個(gè)SampleApplication類烦租,繼承DefaultApplicationLike

@DefaultLifeCycle(application = "com.example.qiongyu_jiang.tinkerdemo.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表示需要支持的修復(fù)的范圍除盏,然后將AndroidManifest.xml文件中的Application的name改為我們的Application。
DefaultApplicationLike不是繼承自Application挫以,需要用注解來(lái)設(shè)置項(xiàng)目中真正的Application者蠕,Tinker插件會(huì)自動(dòng)生成真正的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””應(yīng)該studio 會(huì)報(bào)紅掐松,不過(guò)正常編譯能通過(guò)踱侣,可以忽略。
需要添加讀寫磁盤的文件:

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

加載補(bǔ)丁

加載補(bǔ)丁的代碼是這個(gè):

TinkerInstaller.onReceiveUpgradePatch(this.getApplication(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");

補(bǔ)丁文件在被加載之后會(huì)被刪除大磺。

具體操作與測(cè)試

具體操作

編譯打補(bǔ)丁前的APK

在項(xiàng)目右側(cè)的gradle操作欄中選擇assembleDebug


這里寫圖片描述
這里寫圖片描述

當(dāng)然也可以采用命令行的方式

./gradlew assembleRelease

在app/build/outputs/apk下生成app-debug.apk抡句,在bakApk文件夾下生成相關(guān)備份


這里寫圖片描述
這里寫圖片描述

如果選擇的生成方式是release,同時(shí)也會(huì)生成一個(gè)mapping.txt文件量没,debug是沒(méi)有的玉转。
隨后在終端進(jìn)入apk所在目錄下,將apk安裝到手機(jī)上:

adb install -r app-debug-0711-11-24-18.apk

這樣就成功的把打補(bǔ)丁前的app安裝到手機(jī)上殴蹄。下面是demo的樣子:


這里寫圖片描述
這里寫圖片描述

生成patchAPK

在build.gradle里修改部分文件,使其與剛剛生成的文件名一致究抓,這里說(shuō)明舊的apk是什么。

ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    //Tinker是否可用
    tinkerEnabled = true
    //for normal build
    //old apk file to build patch apk 舊包名
    tinkerOldApkPath = "${bakPath}/app-debug-0711-11-24-18.apk"
    //proguard mapping file to build patch apk  舊包混淆文件目錄
    tinkerApplyMappingPath = "${bakPath}/app-debug-0711-11-24-18-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed 舊包R文件
    tinkerApplyResourcePath = "${bakPath}/app-debug-0711-11-24-18-R.txt"
    //only use for build all flavor, if not, just ignore this field  多渠道
    tinkerBuildFlavorDirectory = "${bakPath}/app-0711-11-24-18"
}

隨后修改原來(lái)的項(xiàng)目文件袭灯,比如加個(gè)按鈕刺下。
然后在項(xiàng)目右側(cè)的gradle的tinker目錄下選擇tinkerPatchDebug,生成補(bǔ)丁包文件稽荧。生成后放在outputs目錄下橘茉。


這里寫圖片描述
這里寫圖片描述

將此文件推送到手機(jī)的SD卡中

adb push ./app/build/outputs/tinkerPatch/debug/patch_signed_7zip.apk /storage/sdcard0/

重啟APP,補(bǔ)丁推送成功姨丈。


這里寫圖片描述
這里寫圖片描述

測(cè)試

測(cè)試項(xiàng) 修改代碼 增加類 增加布局文件 增加資源文件(圖片)
補(bǔ)丁包大小 4kb 4kb 32kb 22kb(圖片大小9.42kb)
啟動(dòng)時(shí)長(zhǎng)(前) 106ms 117ms 144ms 120ms
啟動(dòng)時(shí)長(zhǎng)(后) 117ms 144ms 120ms 121ms
內(nèi)存占用(前) 8.43/17.17 mb 7.27/18.33 mb 7.26/18.34 mb 5.09/20.51 mb
內(nèi)存占用(后) 7.27/18.33 mb 7.26/18.34 mb 5.09/20.51 mb 6.31/19.29 mb

經(jīng)測(cè)試畅卓,增加代碼,增加方法蟋恬,增加類翁潘,增加布局文件,增加圖片歼争,均能修復(fù)成功拜马。

測(cè)試項(xiàng) 修改代碼 增加類 增加布局文件 增加資源文件(圖片)
補(bǔ)丁包大小 4kb 4kb 32kb 22kb(圖片大小9.42kb)
啟動(dòng)時(shí)長(zhǎng)(前) 106ms 117ms 144ms 120ms
啟動(dòng)時(shí)長(zhǎng)(后) 117ms 144ms 120ms 121ms
內(nèi)存占用(前) 8.43/17.17 mb 7.27/18.33 mb 7.26/18.34 mb 5.09/20.51 mb
內(nèi)存占用(后) 7.27/18.33 mb 7.26/18.34 mb 5.09/20.51 mb 6.31/19.29 mb

原來(lái)項(xiàng)目APK大小:1402KB
加入classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.7')后:1402KB
加入compile('com.tencent.tinker:tinker-android-lib:1.7.7')后:1550KB
加入provided('com.tencent.tinker:tinker-android-anno:1.7.7')后:1550KB

一些坑

1) tinkerId is not set
tinker采用的是git的版本號(hào)來(lái)設(shè)置tinkerId沐绒,項(xiàng)目需要設(shè)置git并且設(shè)置完了記得找段代碼commit一下俩莽。
2)ignoreWarning
在studio終端中輸入gradlew tinkerPatchDebug來(lái)生成pach時(shí)可能會(huì)報(bào)以下異常:
Warning:ignoreWarning is false, but resources.arsc is changed, you should use applyResourceMapping mode to build the new apk, otherwise, it may be crash at some times
com.tencent.tinker.build.util.TinkerPatchException: ignoreWarning is false, but resources.arsc is changed, you should use applyResourceMapping mode to build the new apk, otherwise, it may be crash at some times
將build.gradle中的ignoreWarning = false改為true即可

官方常見(jiàn)問(wèn)題

參考博客

http://blog.csdn.net/qiyei2009/article/details/57079973
http://blog.csdn.net/happy_horse/article/details/55224356

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市乔遮,隨后出現(xiàn)的幾起案子扮超,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瞒津,死亡現(xiàn)場(chǎng)離奇詭異蝉衣,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)巷蚪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門病毡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人屁柏,你說(shuō)我怎么就攤上這事啦膜。” “怎么了淌喻?”我有些...
    開封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵僧家,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我裸删,道長(zhǎng)八拱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任涯塔,我火速辦了婚禮肌稻,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘匕荸。我一直安慰自己爹谭,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開白布榛搔。 她就那樣靜靜地躺著诺凡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪践惑。 梳的紋絲不亂的頭發(fā)上腹泌,一...
    開封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音尔觉,去河邊找鬼凉袱。 笑死,一個(gè)胖子當(dāng)著我的面吹牛穷娱,可吹牛的內(nèi)容都是我干的绑蔫。 我是一名探鬼主播运沦,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼泵额,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了携添?” 一聲冷哼從身側(cè)響起嫁盲,我...
    開封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后羞秤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缸托,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年瘾蛋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了俐镐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡哺哼,死狀恐怖佩抹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情取董,我是刑警寧澤棍苹,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站茵汰,受9級(jí)特大地震影響枢里,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蹂午,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一栏豺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧画侣,春花似錦冰悠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至搬泥,卻和暖如春桑寨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背忿檩。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工尉尾, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人燥透。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓沙咏,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親班套。 傳聞我的和親對(duì)象是個(gè)殘疾皇子肢藐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容