騰訊系熱修復(fù)-Tinker使用及原理

簡(jiǎn)介

Tinker是適用于Android的修補(bǔ)程序庫(kù)解取,它支持dex溢谤,庫(kù)和資源更新媳危,而無(wú)需重新安裝apk络凿。更新完成后重新啟動(dòng)即可
Tinker github官方地址

添加依賴

1.app的build.gradle

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

2.在module的gradle中

dependencies {
    //optional, help to generate the final application
    provided('com.tencent.tinker:tinker-android-anno:1.9.1')
    //tinker's main Android lib
    compile('com.tencent.tinker:tinker-android-lib:1.9.1')
}

Gradle版本大于2.3的這么寫(xiě)
dependencies {
    implementation "com.android.support:multidex:1.0.1"
    //tinker的核心庫(kù)
    implementation("com.tencent.tinker:tinker-android-lib:1.9.1") { changing = true }
    //可選骡送,用于生成application類
    annotationProcessor("com.tencent.tinker:tinker-android-anno:1.9.1") { changing = true }
    compileOnly("com.tencent.tinker:tinker-android-anno:1.9.1") { changing = true }
}
...

開(kāi)啟multiDex

defaultConfig {
        ...
        multiDexEnabled true
}
...

最后別忘了添加apply

apply plugin: 'com.tencent.tinker.patch'

Tinker的配置及任務(wù)

1. Tinker文檔中推薦將jumboMode設(shè)置為true

android {
    dexOptions {
        // 支持大工程模式
        jumboMode = true
    }
    ...
}

2 配置Tinker與任務(wù)

將下面的配置全部復(fù)制粘貼到app的gradle文件(app/build.gradle)末尾昂羡,內(nèi)容很多,但現(xiàn)在只需要看懂bakPath與ext括號(hào)內(nèi)的東東就好了摔踱。

// Tinker配置與任務(wù)
def bakPath = file("${buildDir}/bakApk/")
ext {
    // 是否使用Tinker(當(dāng)你的項(xiàng)目處于開(kāi)發(fā)調(diào)試階段時(shí)虐先,可以改為false)
    tinkerEnabled = true
    // 基礎(chǔ)包文件路徑(名字這里寫(xiě)死為old-app.apk。用于比較新舊app以生成補(bǔ)丁包派敷,不管是debug還是release編譯)
    tinkerOldApkPath = "${bakPath}/old-app.apk"
    // 基礎(chǔ)包的mapping.txt文件路徑(用于輔助混淆補(bǔ)丁包的生成赴穗,一般在生成release版app時(shí)會(huì)使用到混淆,所以這個(gè)mapping.txt文件一般只是用于release安裝包補(bǔ)丁的生成)
    tinkerApplyMappingPath = "${bakPath}/old-app-mapping.txt"
    // 基礎(chǔ)包的R.txt文件路徑(如果你的安裝包中資源文件有改動(dòng)膀息,則需要使用該R.txt文件來(lái)輔助生成補(bǔ)丁包)
    tinkerApplyResourcePath = "${bakPath}/old-app-R.txt"
    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/flavor"
}

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 : android.defaultConfig.versionName
}

def buildWithTinker() {
    return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}

if (buildWithTinker()) {
    //apply tinker插件
    apply plugin: 'com.tencent.tinker.patch'

    // 全局信息相關(guān)的配置項(xiàng)
    tinkerPatch {
        tinkerEnable = buildWithTinker()// 是否打開(kāi)tinker的功能般眉。
        oldApk = getOldApkPath()        // 基準(zhǔn)apk包的路徑,必須輸入潜支,否則會(huì)報(bào)錯(cuò)甸赃。
        ignoreWarning = false           // 是否忽略有風(fēng)險(xiǎn)的補(bǔ)丁包。這里選擇不忽略冗酿,當(dāng)補(bǔ)丁包風(fēng)險(xiǎn)時(shí)會(huì)中斷編譯埠对。
        useSign = true                  // 在運(yùn)行過(guò)程中,我們需要驗(yàn)證基準(zhǔn)apk包與補(bǔ)丁包的簽名是否一致裁替,我們是否需要為你簽名项玛。
        // 編譯相關(guān)的配置項(xiàng)
        buildConfig {
            applyMapping = getApplyMappingPath()
            // 可選參數(shù);在編譯新的apk時(shí)候弱判,我們希望通過(guò)保持舊apk的proguard混淆方式襟沮,從而減少補(bǔ)丁包的大小。這個(gè)只是推薦設(shè)置昌腰,不設(shè)置applyMapping也不會(huì)影響任何的assemble編譯开伏。
            applyResourceMapping = getApplyResourceMappingPath()
            // 可選參數(shù);在編譯新的apk時(shí)候遭商,我們希望通過(guò)舊apk的R.txt文件保持ResId的分配固灵,這樣不僅可以減少補(bǔ)丁包的大小,同時(shí)也避免由于ResId改變導(dǎo)致remote view異常劫流。
            tinkerId = getTinkerIdValue()
            // 在運(yùn)行過(guò)程中巫玻,我們需要驗(yàn)證基準(zhǔn)apk包的tinkerId是否等于補(bǔ)丁包的tinkerId。這個(gè)是決定補(bǔ)丁包能運(yùn)行在哪些基準(zhǔn)包上面祠汇,一般來(lái)說(shuō)我們可以使用git版本號(hào)仍秤、versionName等等。
            keepDexApply = false
            // 如果我們有多個(gè)dex,編譯補(bǔ)丁時(shí)可能會(huì)由于類的移動(dòng)導(dǎo)致變更增多座哩。若打開(kāi)keepDexApply模式徒扶,補(bǔ)丁包將根據(jù)基準(zhǔn)包的類分布來(lái)編譯粮彤。
            isProtectedApp = false // 是否使用加固模式根穷,僅僅將變更的類合成補(bǔ)丁姜骡。注意,這種模式僅僅可以用于加固應(yīng)用中屿良。
            supportHotplugComponent = false // 是否支持新增非export的Activity(1.9.0版本開(kāi)始才有的新功能)
        }
        // dex相關(guān)的配置項(xiàng)
        dex {
            dexMode = "jar"
// 只能是'raw'或者'jar'圈澈。 對(duì)于'raw'模式,我們將會(huì)保持輸入dex的格式尘惧。對(duì)于'jar'模式康栈,我們將會(huì)把輸入dex重新壓縮封裝到j(luò)ar。如果你的minSdkVersion小于14喷橙,你必須選擇‘jar’模式啥么,而且它更省存儲(chǔ)空間,但是驗(yàn)證md5時(shí)比'raw'模式耗時(shí)贰逾。默認(rèn)我們并不會(huì)去校驗(yàn)md5,一般情況下選擇jar模式即可悬荣。
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            // 需要處理dex路徑,支持*疙剑、?通配符氯迂,必須使用'/'分割。路徑是相對(duì)安裝包的言缤,例如assets/...
            loader = [
                    // 定義哪些類在加載補(bǔ)丁包的時(shí)候會(huì)用到嚼蚀。這些類是通過(guò)Tinker無(wú)法修改的類,也是一定要放在main dex的類管挟。
                    // 如果你自定義了TinkerLoader轿曙,需要將它以及它引用的所有類也加入loader中;
                    // 其他一些你不希望被更改的類僻孝,例如Sample中的BaseBuildInfo類拳芙。這里需要注意的是,這些類的直接引用類也需要加入到loader中皮璧≈墼或者你需要將這個(gè)類變成非preverify。
            ]
        }
        //  lib相關(guān)的配置項(xiàng)
        lib {
            pattern = ["lib/*/*.so","src/main/jniLibs/*/*.so"]
            // 需要處理lib路徑悴务,支持*睹限、?通配符,必須使用'/'分割讯檐。與dex.pattern一致, 路徑是相對(duì)安裝包的羡疗,例如assets/...
        }
        // res相關(guān)的配置項(xiàng)
        res {
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
            // 需要處理res路徑,支持*别洪、?通配符叨恨,必須使用'/'分割。與dex.pattern一致, 路徑是相對(duì)安裝包的挖垛,例如assets/...痒钝,務(wù)必注意的是秉颗,只有滿足pattern的資源才會(huì)放到合成后的資源包。
            ignoreChange = [
                    // 支持*送矩、?通配符蚕甥,必須使用'/'分割。若滿足ignoreChange的pattern栋荸,在編譯時(shí)會(huì)忽略該文件的新增菇怀、刪除與修改。 最極端的情況晌块,ignoreChange與上面的pattern一致爱沟,即會(huì)完全忽略所有資源的修改。
                    "assets/sample_meta.txt"
            ]
            largeModSize = 100
            // 對(duì)于修改的資源匆背,如果大于largeModSize钥顽,我們將使用bsdiff算法。這可以降低補(bǔ)丁包的大小靠汁,但是會(huì)增加合成時(shí)的復(fù)雜度蜂大。默認(rèn)大小為100kb
        }
        // 用于生成補(bǔ)丁包中的'package_meta.txt'文件
        packageConfig {
            // configField("key", "value"), 默認(rèn)我們自動(dòng)從基準(zhǔn)安裝包與新安裝包的Manifest中讀取tinkerId,并自動(dòng)寫(xiě)入configField。
            // 在這里蝶怔,你可以定義其他的信息奶浦,在運(yùn)行時(shí)可以通過(guò)TinkerLoadResult.getPackageConfigByName得到相應(yīng)的數(shù)值。
            // 但是建議直接通過(guò)修改代碼來(lái)實(shí)現(xiàn)踢星,例如BuildConfig澳叉。
            configField("platform", "all")
            configField("patchVersion", "1.0")
//            configField("patchMessage", "tinker is sample to use")
        }
        // 7zip路徑配置項(xiàng),執(zhí)行前提是useSign為true
        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
    def date = new Date().format("MMdd-HH-mm-ss")

    /**
     * bak apk and mapping
     */
    android.applicationVariants.all { variant ->
        /**
         * task type, you want to bak
         */
        def taskName = variant.name

        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.first().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"
                    }

                }
            }
        }
    }
}

其中沐悦,有幾點(diǎn)配置在這里說(shuō)明一下成洗,方便理解后續(xù)的操作(當(dāng)tinkerEnabled = true的情況下):

  1. app的生成目錄是:主Module(一般是名為app)/build/bakApk文件夾。
  2. 補(bǔ)丁包的生成路徑:主Module(一般是名為app)/build/outputs/apk/tinkerPatch/debug/patch_signed_7zip.apk藏否。
  3. 基礎(chǔ)包的名字:old-app.apk瓶殃,放于bakApk文件夾下。
  4. 基礎(chǔ)包的mapping.txt和R.txt文件一般在編譯release簽名的apk時(shí)才會(huì)用到副签。
  5. 在用到mapping.txt文件時(shí)遥椿,需要重命名為old-app-mapping.txt,放于bakApk文件夾下淆储。
  6. 在用到R.txt文件時(shí)冠场,需要重命名為old-app-R.txt,放于bakApk文件夾下本砰。

Tinker準(zhǔn)備工作

1. 添加一些類

1.這里面有一些我找到的文件碴裙,需要加到工程中,當(dāng)然這些工具類也可以自己寫(xiě)

image.png

這些類的作用大概如下
SampleUncaughtExceptionHandler:Tinker的全局異常捕獲器。
MyLogImp:Tinker的日志輸出實(shí)現(xiàn)類舔株。
SampleLoadReporter:加載補(bǔ)丁時(shí)的一些回調(diào)莺琳。
SamplePatchListener:過(guò)濾Tinker收到的補(bǔ)丁包的修復(fù)、升級(jí)請(qǐng)求督笆。
SamplePatchReporter:修復(fù)或者升級(jí)補(bǔ)丁時(shí)的一些回調(diào)。
SampleTinkerReport:修復(fù)結(jié)果(成功诱贿、沖突娃肿、失敗等)。
SampleResultService::patch補(bǔ)丁合成進(jìn)程將合成結(jié)果返回給主進(jìn)程的類珠十。
TinkerManager:Tinker管理器(安裝料扰、初始化Tinker)。
TinkerUtils:拓展補(bǔ)丁條件判定焙蹭、鎖屏或后臺(tái)時(shí)應(yīng)用重啟功能的工具類晒杈。
對(duì)于這些自定義類及錯(cuò)誤碼的詳細(xì)說(shuō)明,請(qǐng)參考:「Tinker官方Wiki:可選的自定義類」

2. 清單文件中添加服務(wù)

<service
android:name="com.lqr.tinker.service.SampleResultService"
android:exported="false"/>

編寫(xiě)Application代理

Tinker表示孔厉,Application無(wú)法動(dòng)態(tài)修復(fù)拯钻,所以有兩種選擇:

  1. 使用「繼承TinkerApplication + DefaultApplicationLike」。
  2. 使用「DefaultLifeCycle注解 + DefaultApplicationLike」撰豺。
    我們?cè)谶@里使用第二種方式
@SuppressWarnings("unused")
@DefaultLifeCycle(application = "com.lqr.tinker.MyApplication",// application類名粪般。只能用字符串,這個(gè)MyApplication文件是不存在的污桦,但可以在AndroidManifest.xml的application標(biāo)簽上使用(name)
        flags = ShareConstants.TINKER_ENABLE_ALL,// tinkerFlags
        loaderClass = "com.tencent.tinker.loader.TinkerLoader",//loaderClassName, 我們這里使用默認(rèn)即可!(可不寫(xiě))
        loadVerifyFlag = false)//tinkerLoadVerifyFlag
public class TinkerApplicationLike extends DefaultApplicationLike {

    private Application mApplication;
    private Context mContext;
    private Tinker mTinker;

    // 固定寫(xiě)法
    public TinkerApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    }

    // 固定寫(xiě)法
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
        getApplication().registerActivityLifecycleCallbacks(callback);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        mApplication = getApplication();
        mContext = getApplication();
        initTinker(base);
        // 可以將之前自定義的Application中onCreate()方法所執(zhí)行的操作搬到這里...
    }

    private void initTinker(Context base) {
        // tinker需要你開(kāi)啟MultiDex
        MultiDex.install(base);

        TinkerManager.setTinkerApplicationLike(this);
        // 設(shè)置全局異常捕獲
        TinkerManager.initFastCrashProtect();
        //開(kāi)啟升級(jí)重試功能(在安裝Tinker之前設(shè)置)
        TinkerManager.setUpgradeRetryEnable(true);
        //設(shè)置Tinker日志輸出類
        TinkerInstaller.setLogIml(new MyLogImp());
        //安裝Tinker(在加載完multiDex之后亩歹,否則你需要將com.tencent.tinker.**手動(dòng)放到main dex中)
        TinkerManager.installTinker(this);
        mTinker = Tinker.with(getApplication());
    }

}

在Application中添加TinkerApplicationLike

public class TinkerApplicationLike extends DefaultApplicationLike {
    ...
    @Override
    public void onCreate() {
        super.onCreate();
        // 將之前自定義的Application中onCreate()方法所執(zhí)行的操作搬到這里...
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        mApplication = getApplication();
        mContext = getApplication();
        initTinker(base);
        // 或搬到這里...
    }
}

使用TinkerApplication

通過(guò)上面的注釋,MyApplication會(huì)自動(dòng)生成凡橱,我使用了自定義的Application使用方法如下

public class MyApplicatoin extends TinkerApplication {

   public MyApplicatoin(){
       super(ShareConstants.TINKER_ENABLE_ALL,TinkerApplicationLike.class.getName(),TinkerLoader.class.getName(),false);
   }
   
}

Tinker的使用

1.編譯基礎(chǔ)包
在Terminal中使用命令行./gradlew assembleDebug之后再module的build目錄下會(huì)有個(gè)bakApk目錄小作,在這目錄中就會(huì)生成基礎(chǔ)包
其實(shí)在配置完Tinker的配置后,make項(xiàng)目稼钩,就看這個(gè)目錄下已經(jīng)存在基礎(chǔ)包了


image.png

如果這個(gè)apk文件是release簽名且是要放到應(yīng)用市場(chǎng)上的顾稀,那么你必須將apk與R.txt(如果有使用混淆的話,還會(huì)有一個(gè)mapping.txt)這幾個(gè)文件保存好坝撑,切記础拨。
2.制作補(bǔ)丁包
將要修復(fù)的基礎(chǔ)包apk重命名為old-app.apk
點(diǎn)擊tinkerPatchDebug


image.png

在output文件夾中生成分叉包


image.png

在這里我遇到了一個(gè)問(wèn)題
在編譯的時(shí)候補(bǔ)丁包的時(shí)候總是不成功報(bào)
com.tencent.tinker.build.util.TinkerPatchException:
解決方法如下
image.png

3.將生成的補(bǔ)丁包patch_signed_7zip.apk放置指定目錄下
執(zhí)行TinkerInstaller.onReceiveUpgradePatch方法進(jìn)行安裝
之后成功修復(fù)

以上步驟可以修復(fù)java、so 绍载、資源文件

加載SO注意的地方

image.png

上圖是Tinker官網(wǎng)Wiki的文檔部分截圖诡宗,從紅線部分可以知道,因?yàn)椴糠质謾C(jī)判斷abi并不準(zhǔn)確(可能因?yàn)锳ndroid碎片化比較嚴(yán)重吧)击儡,Tinker沒(méi)有區(qū)分abi塔沃,自然也不會(huì)在app啟動(dòng)時(shí),自動(dòng)加載對(duì)應(yīng)的so庫(kù)阳谍,這需要開(kāi)發(fā)者自己判斷蛀柴。
需要調(diào)用如下方法

public void load_library_hack(View view) {
    String CPU_ABI = android.os.Build.CPU_ABI;
    // 將tinker library中的 CPU_ABI架構(gòu)的so 注冊(cè)到系統(tǒng)的library path中螃概。
    TinkerLoadLibrary.installNavitveLibraryABI(this, CPU_ABI);
}

常用API

1、請(qǐng)求打補(bǔ)丁
TinkerInstaller.onReceiveUpgradePatch(context, 補(bǔ)丁包的本地路徑);
2鸽疾、卸載補(bǔ)丁
Tinker.with(getApplicationContext()).cleanPatch();// 卸載所有的補(bǔ)丁
Tinker.with(getApplicationContext()).cleanPatchByVersion(版本號(hào))// 卸載指定版本的補(bǔ)丁
復(fù)制代碼
3吊洼、殺死應(yīng)用的其他進(jìn)程
ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
復(fù)制代碼
4、Hack方式修復(fù)so
TinkerLoadLibrary.installNavitveLibraryABI(this, abi);
abi:cpu架構(gòu)類型

5制肮、非Hack方式修復(fù)so
TinkerLoadLibrary.loadLibraryFromTinker(getApplicationContext(), "lib/" + abi, so庫(kù)的模塊名); // 加載任意abi庫(kù)
TinkerLoadLibrary.loadArmLibrary(getApplicationContext(), so庫(kù)的模塊名); // 只適用于加載armeabi庫(kù)
TinkerLoadLibrary.loadArmV7Library(getApplicationContext(), so庫(kù)的模塊名); // 只適用于加載armeabi-v7a庫(kù)
復(fù)制代碼

loadArmLibrary()與loadArmV7Library()本質(zhì)是調(diào)用了loadLibraryFromTinker()冒窍,有興趣的可以查看下源碼。
對(duì)于Tinker所有API的詳細(xì)說(shuō)明豺鼻,請(qǐng)參考:「Tinker官方Wiki:Tinker-API概覽」综液。

Tinker原理

Tinker的原理,類加載方案基于Dex分包方案儒飒,什么是Dex分包方案呢谬莹?這個(gè)得先從65536限制和LinearAlloc限制說(shuō)起。
65536限制
隨著應(yīng)用功能越來(lái)越復(fù)雜桩了,代碼量不斷地增大附帽,引入的庫(kù)也越來(lái)越多,可能會(huì)在編譯時(shí)提示如下異常:

com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536

這說(shuō)明應(yīng)用中引用的方法數(shù)超過(guò)了最大數(shù)65536個(gè)井誉。產(chǎn)生這一問(wèn)題的原因就是系統(tǒng)的65536限制士葫,65536限制的主要原因是DVM Bytecode的限制,DVM指令集的方法調(diào)用指令invoke-kind索引為16bits送悔,最多能引用 65535個(gè)方法慢显。
LinearAlloc限制
在安裝時(shí)可能會(huì)提示INSTALL_FAILED_DEXOPT。產(chǎn)生的原因就是LinearAlloc限制欠啤,DVM中的LinearAlloc是一個(gè)固定的緩存區(qū)荚藻,當(dāng)方法數(shù)過(guò)多超出了緩存區(qū)的大小時(shí)會(huì)報(bào)錯(cuò)。

為了解決65536限制和LinearAlloc限制洁段,從而產(chǎn)生了Dex分包方案应狱。Dex分包方案主要做的是在打包時(shí)將應(yīng)用代碼分成多個(gè)Dex,將應(yīng)用啟動(dòng)時(shí)必須用到的類和這些類的直接引用類放到主Dex中祠丝,其他代碼放到次Dex中疾呻。當(dāng)應(yīng)用啟動(dòng)時(shí)先加載主Dex,等到應(yīng)用啟動(dòng)后再動(dòng)態(tài)的加載次Dex写半,從而緩解了主Dex的65536限制和LinearAlloc限制岸蜗。
在ClassLoader的加載過(guò)程,其中一個(gè)環(huán)節(jié)就是調(diào)用DexPathList的findClass的方法叠蝇,如下所示璃岳。
libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

public Class<?> findClass(String name, List<Throwable> suppressed) {
       for (Element element : dexElements) {//1
           Class<?> clazz = element.findClass(name, definingContext, suppressed);//2
           if (clazz != null) {
               return clazz;
           }
       }
       if (dexElementsSuppressedExceptions != null) {
           suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
       }
       return null;
   }

Element內(nèi)部封裝了DexFile,DexFile用于加載dex文件,因此每個(gè)dex文件對(duì)應(yīng)一個(gè)Element铃慷。
多個(gè)Element組成了有序的Element數(shù)組dexElements单芜。當(dāng)要查找類時(shí),會(huì)在注釋1處遍歷Element數(shù)組dexElements(相當(dāng)于遍歷dex文件數(shù)組)犁柜,注釋2處調(diào)用Element的findClass方法洲鸠,其方法內(nèi)部會(huì)調(diào)用DexFile的loadClassBinaryName方法查找類。如果在Element中(dex文件)找到了該類就返回馋缅,如果沒(méi)有找到就接著在下一個(gè)Element中進(jìn)行查找扒腕。
根據(jù)上面的查找流程,我們將有bug的類Key.class進(jìn)行修改股囊,再將Key.class打包成包含dex的補(bǔ)丁包Patch.jar袜匿,放在Element數(shù)組dexElements的第一個(gè)元素更啄,這樣會(huì)首先找到Patch.dex中的Key.class去替換之前存在bug的Key.class稚疹,排在數(shù)組后面的dex文件中的存在bug的Key.class根據(jù)ClassLoader的雙親委托模式就不會(huì)被加載,這就是類加載方案祭务,如下圖所示内狗。


image.png

類加載方案需要重啟App后讓ClassLoader重新加載新的類,為什么需要重啟呢义锥?這是因?yàn)轭愂菬o(wú)法被卸載的柳沙,因此要想重新加載新的類就需要重啟App,因此采用類加載方案的熱修復(fù)框架是不能即時(shí)生效的拌倍。
雖然很多熱修復(fù)框架采用了類加載方案赂鲤,但具體的實(shí)現(xiàn)細(xì)節(jié)和步驟還是有一些區(qū)別的,比如QQ空間的超級(jí)補(bǔ)丁和Nuwa是按照上面說(shuō)得將補(bǔ)丁包放在Element數(shù)組的第一個(gè)元素得到優(yōu)先加載柱恤。微信Tinker將新舊apk做了diff,得到patch.dex梗顺,然后將patch.dex與手機(jī)中apk的classes.dex做合并泡孩,生成新的classes.dex,然后在運(yùn)行時(shí)通過(guò)反射將classes.dex放在Element數(shù)組的第一個(gè)元素寺谤。餓了么的Amigo則是將補(bǔ)丁包中每個(gè)dex 對(duì)應(yīng)的Element取出來(lái)仑鸥,之后組成新的Element數(shù)組,在運(yùn)行時(shí)通過(guò)反射用新的Element數(shù)組替換掉現(xiàn)有的Element 數(shù)組变屁。

采用類加載方案的主要是以騰訊系為主眼俊,包括微信的Tinker、QQ空間的超級(jí)補(bǔ)丁粟关、手機(jī)QQ的QFix泵琳、餓了么的Amigo和Nuwa等等。

demo地址

參考劉望舒博客

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市获列,隨后出現(xiàn)的幾起案子谷市,更是在濱河造成了極大的恐慌,老刑警劉巖击孩,帶你破解...
    沈念sama閱讀 212,816評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迫悠,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡巩梢,警方通過(guò)查閱死者的電腦和手機(jī)创泄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)括蝠,“玉大人鞠抑,你說(shuō)我怎么就攤上這事〖删” “怎么了搁拙?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,300評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)法绵。 經(jīng)常有香客問(wèn)我箕速,道長(zhǎng),這世上最難降的妖魔是什么朋譬? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,780評(píng)論 1 285
  • 正文 為了忘掉前任盐茎,我火速辦了婚禮,結(jié)果婚禮上徙赢,老公的妹妹穿的比我還像新娘字柠。我一直安慰自己,他們只是感情好狡赐,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布窑业。 她就那樣靜靜地躺著,像睡著了一般阴汇。 火紅的嫁衣襯著肌膚如雪数冬。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 50,084評(píng)論 1 291
  • 那天搀庶,我揣著相機(jī)與錄音拐纱,去河邊找鬼。 笑死哥倔,一個(gè)胖子當(dāng)著我的面吹牛秸架,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播咆蒿,決...
    沈念sama閱讀 39,151評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼东抹,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蚂子!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起缭黔,我...
    開(kāi)封第一講書(shū)人閱讀 37,912評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤食茎,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后馏谨,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體别渔,經(jīng)...
    沈念sama閱讀 44,355評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評(píng)論 2 327
  • 正文 我和宋清朗相戀三年惧互,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了哎媚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,809評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡喊儡,死狀恐怖拨与,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情艾猜,我是刑警寧澤买喧,帶...
    沈念sama閱讀 34,504評(píng)論 4 334
  • 正文 年R本政府宣布,位于F島的核電站箩朴,受9級(jí)特大地震影響岗喉,放射性物質(zhì)發(fā)生泄漏秋度。R本人自食惡果不足惜炸庞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望荚斯。 院中可真熱鬧埠居,春花似錦、人聲如沸事期。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)兽泣。三九已至绎橘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間唠倦,已是汗流浹背称鳞。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,121評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留稠鼻,地道東北人冈止。 一個(gè)月前我還...
    沈念sama閱讀 46,628評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像候齿,于是被迫代替她去往敵國(guó)和親熙暴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子闺属,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評(píng)論 2 351

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