Tinker熱更新源碼接入以及分析

Tinker熱更新源碼接入以及分析

介紹:tinker是一款由騰訊微信團隊提供的熱更新軟件,有著微信龐大的用戶基礎,每一版tinker都會使用超過200個版本的微信進行兼容性和穩定性的測試,并且與手機廠商有著深度的合作,每一版tinker發布之前,都會先經過手機廠商的測試,并且微信使用的也是tinker的開源版本。相比較于其他的熱更新軟件,tinker更具有可靠性。

1.0 以源碼的方式接入Tinker

Tinker GitHub 連接: https://github.com/Tencent/tinker

建議閱讀前先看一遍官網的github,進行一下了解。

下載源碼zip包進行解壓,我們要使用到的就是Tinker的以下幾個插件包:

pic-1.png

tinrd-party:這個是tinker所使用到的一些工具類插件,例如tinker的核心bsdiff、dex文件處理工具、解壓zip文件之類的。

tinker-android:這個就是tinker接入到android工程中之后的主要功能模塊,包含了patch以及loader的部分。

tinker-build:tinker對本地apk進行分析打包,生成patch文件的主要包。

tinker-commons:公共包,放了一些公共的patch算法的東西。

1.1 集成

首先第一步將這四個model導入到當前的工程中,導入之后肯定會出現編譯錯誤,我這里不羅列錯誤,就把解決方法羅列一下:

201808121423052222222.png

導入項目成功之后,肯定會報找不到grandle下面的這四個文件,如上圖,從tinker源碼目錄把這四個文件拷貝過來就行,其中有一個是push的gradle,在這些model中都有引用,下面這個直接刪除就好,如下圖:

201808121423273333333.png

解決完gradle文件的錯誤之后,是一些model中的compile引用依賴報錯,這是因為android gradle版本造成的,tinker默認使用的2.2版本,而在gradle3中,推薦使用的是implementation和api,具體原由不贅述了,
我是采用的直接改成api,這里tinker官方sample里面有更好的集成方式:

4.png
5.png

判斷gradle的版本,而后使用不同的集成依賴的方式。

到這一步,應該源碼包就沒有什么問題了,直接導入依賴到自己的工程中:

6.png

剩下的就是tinker官網的集成流程,這里不再復述了,具體在github中都有詳細的集成方法,無非就是修改一些application的配置,增加一個applicationLike,把原本application中的初始化處理挪到applicationLike當中,這樣的做法是可以讓App支持修改application,applicationLike是通過反射的方法進行加載調用的,通過這種方法,就達到了熱修復Application代碼的效果。

剩下的就是build gradle的集成,這里要把tinker的一些配置和變量添加到文件中,不多復述,直接copyGitHub上的就好
這里要提一點的是,官方的build文件在生成apk的時候,會直接把apk文件放到bakApk目錄下,如果編譯次數過多的話,就會產生N多個文件,尤其是Release下,會生成apk、mapping、R三個文件。而且生成patch文件的時候,也需要修改gradle中的apk名稱,十分繁瑣,這里我稍加改造:

/**
 * you can use assembleRelease to build you base apk
 * use tinkerPatchRelease -POLD_APK=  -PAPPLY_MAPPING=  -PAPPLY_RESOURCE= to build patch
 * add apk from the build/bakApk
 */
ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    tinkerEnabled = true

    oldTime = "0806-18-21-02"

    oldAPKName = "release-${oldTime}"

    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/${oldTime}"

    //for normal build
    //old apk file to build patch apk
    tinkerOldApkPath = "${bakPath}/${oldTime}/app-${oldAPKName}.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/${oldTime}/app-${oldAPKName}-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/${oldTime}/app-${oldAPKName}-R.txt"
}

這里是增加了一個oldApkName和time,直接修改這里的time和name就好,替換原文件中的對應部分代碼。

另外一個就是在當前的基礎上,增加一個時間目錄,把apk文件、mapping文件、R文件統一放到一個目錄下,便于管理和查看:


/**
 * 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 ? "${date}/${fileNamePrefix}" : "${date}/${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")
                    }
                }
            }
        }
    }
}

就是在newFileNamePrefix這個參數后面,加上一個${date}即可。

這樣每次在運行的時候,build目錄下就會生成以下文件(debug):

7.png

這里的文件對應的就是build.gradle中的oldApkPatch,在生成patch文件的過程中,也是根據這個路徑來進行判斷和處理的。而我們只需要修改oldApkPatch的路徑就可以了,修改起來也很簡單,我已經封裝了oldName和oldTime,根據當前是否是正式環境或者測試環境來修改debug和Release版本,然后修改對應的時間就ok了。

1.2 默認的工具、監聽編寫

8.png

tinker官方的sample中,有以上幾個工具類,這都是對原本tinker的一些監聽加載類做的二次封裝,這里先拷貝過去就好,后面逐一進行分析。

首先要關注TinkerManager這個類,這是封裝的一個簡易的工具類,對applicationLike進行的保存,并且包含tinker初始化的一些操作:

主要看installTinker這個方法就好:

9.png

tinkerInstall是以build的方式進行加載的,這里我們先點進去看一下源碼:

10.png

而在installTinker當中,創建的一些default監聽器和加載器,其實都沒有做過多的處理,這里其實只是想要表明,這些都可以進行自定義的,這樣說明了tinker的復用性和可定制性都是比較強的。

做完初始化的工作之后,其實tinker就已經接入到工程當中了,與bugly和tinker pathc不同,這里沒有任何多余的操作和初始化內容,只是把tinker這個熱更新工具集成進來,那如何使用呢?

很簡單,先生成一個apk文件包,也就是bakApk目錄下要有一個已經生成好的版本目錄,然后修改build.gradle文件中的路徑指向這個apk包。

11.png

最后,根據當前是debug還是release版本執行對應的tinkerPatchDebug和tinkerPatchRelease,tinkerPatch就生成好了,生成好的文件路徑如下圖:

12.png

這個就是要執行的補丁包了,前提是要在生成之前修改好tinkerId,也就是build.gradle文件中的getSha()方法返回的字符串參數,原本tinker官方是把git當前節點的id作為版本id,這里我是寫死了,大家隨便寫一個參數把base包和patch包做個區分就好。

使用起來也很簡單,把補丁包放到手機中可以讀取到的目錄,我這里是放到應用sd卡的cache目錄下:

File rootFile = getExternalCacheDir();
if (rootFile != null && rootFile.exists()){
    mPath = rootFile.getAbsolutePath() + File.separatorChar;
}
...
File patchFile = new File(mPath, "patch_signed_7zip.apk");
if (patchFile.exists()){
    TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), patchFile.getAbsolutePath());
}

執行以上代碼其實就可以加載成功了,重啟查看效果就OK,前提是要對patch版本的包做出一些改動。
以上就是tinker的接入流程,其實結合網上大部分接入教程,基本都可以輕松的完成以上源碼的接入,下面開始說一下對源碼以及流程上的一些分析的分析。


2. tinker源碼執行流程的分析

tinker 的使用流程分為三部分

  1. 生成差異包
  2. 加載差異補丁包
  3. 重新啟動之后,通過tinkerLoader進行加載

我會通過這三個步驟,逐一分析tinker 的加載過程。

2.1 打包生成patch的流程

調用tinkerPatchDebug和tinkerPatchRealse之后,是通過

com.tencent.tinker.build.patch.Runner.tinkerPatch()

來進行補丁加載的,文件在如下目錄:

13.png

執行過程:

14.png

其實是通過apkDecoder來進行Path處理的,繼續往下追蹤,主要看一下他的patch方法:

manifestDecoder.patch(oldFile, newFile);

代碼中首先對manifest文件進行了判斷,主要是判斷mainfest文件有沒有做過修過,如果進行了修改,那么tinker就會拋出異常,是因為tinker自身并不支持修改AndroidManifest.xml文件,也就是不支持增加4大組件的。

代碼的下一步是一個文件寫入的過程:

Files.walkFileTree(mNewApkDir.toPath(), new ApkFilesVisitor(config, mNewApkDir.toPath(), mOldApkDir.toPath(), dexPatchDecoder, soPatchDecoder, resPatchDecoder));

這里所有的比對操作都交給了ApkFilesVisitor,繼續追蹤,可以看到,ApkFilesVisitor文件中的所有操作都在 visitor方法中:

15.png

分析方法不難看出,所有的patch操作分為dex、so、res,分別交給了三個decoder去進行操作,然后我們逐一分析這些decoder。

首先是dexDecoder,這些decoder都是在ApkDecoder的構造方法中進行初始化的,而dexDecoder是來源于UniqueDexDiffDecoder對象,該對象繼承于DexDiffDecoder,而所有的patch操作也都是在該decoder類下的 patch(final File oldFile, final File newFile) 方法。

該方法主要是對dex進行檢測和比對,并保存新舊dex的對應關系到數組當中,執行完畢后,會走到onAllPatchesEnd()方法,最終會執行generatePatchInfoFile() 方法執行生成補丁文件。

這里會執行生成一個DexPatchGenerator對象,負責整個新舊dex的比對工作,其中包含以下幾種:

  1. private DexSectionDiffAlgorithm<StringData> stringDataSectionDiffAlg;
  2. private DexSectionDiffAlgorithm<Integer> typeIdSectionDiffAlg;
  3. private DexSectionDiffAlgorithm<ProtoId> protoIdSectionDiffAlg;
  4. private DexSectionDiffAlgorithm<FieldId> fieldIdSectionDiffAlg;
  5. private DexSectionDiffAlgorithm<MethodId> methodIdSectionDiffAlg;
  6. private DexSectionDiffAlgorithm<ClassDef> classDefSectionDiffAlg;
  7. private DexSectionDiffAlgorithm<TypeList> typeListSectionDiffAlg;
  8. private DexSectionDiffAlgorithm<AnnotationSetRefList> annotationSetRefListSectionDiffAlg;
  9. private DexSectionDiffAlgorithm<AnnotationSet> annotationSetSectionDiffAlg;
  10. private DexSectionDiffAlgorithm<ClassData> classDataSectionDiffAlg;
  11. private DexSectionDiffAlgorithm<Code> codeSectionDiffAlg;
  12. private DexSectionDiffAlgorithm<DebugInfoItem> debugInfoSectionDiffAlg;
  13. private DexSectionDiffAlgorithm<Annotation> annotationSectionDiffAlg;
  14. private DexSectionDiffAlgorithm<EncodedValue> encodedArraySectionDiffAlg;
  15. private DexSectionDiffAlgorithm<AnnotationsDirectory> annotationsDirectorySectionDiffAlg;

這里涉及到了tinker的核心算法,以及dex格式的介紹,dex比對算法是二路歸并,這里有一篇文章做了非常詳細的解讀:https://www.zybuluo.com/dodola/note/554061

執行完executeAndSaveTo之后,比對后的差異也就以buffer的形式保存下來了。

這里放個連接,里面有tinker對dex進行比對的詳細描述,來自于官方:微信Tinker的一切都在這里,包括源碼(一)

接下來分析res的比對過程:

資源的比對工作是交由ResDiffDecoder來完成的,我們點開該類下的patch方法來進行分析,核心的幾個方法如下:

File outputFile = getOutputPath(newFile).toFile();

if (oldFile == null || !oldFile.exists()) {
    if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) {
        Logger.e("found add resource: " + name + " ,but it match ignore change pattern, just ignore!");
        return false;
    }
    FileOperation.copyFileUsingStream(newFile, outputFile);
    addedSet.add(name);
    writeResLog(newFile, oldFile, TypedValue.ADD);
    return true;
}

如果舊文件不存在,則直接添加到差異包中。

//oldFile or newFile may be 0b length
if (oldMd5 != null && oldMd5.equals(newMd5)) {
    return false;
}

MD5文件一致,則表示文件沒有做出修改。

如果文件發生變化,則是交給了dealWithModifyFile(name, newMd5, oldFile, newFile, outputFile);去進行處理。

private boolean dealWithModifyFile(String name, String newMd5, File oldFile, File newFile, File outputFile) throws IOException {
        if (checkLargeModFile(newFile)) {
            if (!outputFile.getParentFile().exists()) {
                outputFile.getParentFile().mkdirs();
            }
            BSDiff.bsdiff(oldFile, newFile, outputFile);
            //treat it as normal modify
            if (Utils.checkBsDiffFileSize(outputFile, newFile)) {
                LargeModeInfo largeModeInfo = new LargeModeInfo();
                largeModeInfo.path = newFile;
                largeModeInfo.crc = FileOperation.getFileCrc32(newFile);
                largeModeInfo.md5 = newMd5;
                largeModifiedSet.add(name);
                largeModifiedMap.put(name, largeModeInfo);
                writeResLog(newFile, oldFile, TypedValue.LARGE_MOD);
                return true;
            }
        }
        modifiedSet.add(name);
        FileOperation.copyFileUsingStream(newFile, outputFile);
        writeResLog(newFile, oldFile, TypedValue.MOD);
        return false;
    }

核心就是BSDiff.bsdiff(oldFile, newFile, outputFile);

這里利用bsdiff對文件進行二進制比對,目的是可以很好的控制差異文件的大小,這個就是一個增量更新的比對算法,這里不多做解釋。

最后在patch執行完畢后,會執行以下代碼將所有檢索出來的資源增刪改的集合放到生成的meta文件中,而在tinker loader的時候,會讀取補丁文件中的meta文件執行相對應的操作。

//write meta file, write large modify first
writeMetaFile(largeModifiedSet, TypedValue.LARGE_MOD);
writeMetaFile(modifiedSet, TypedValue.MOD);
writeMetaFile(addedSet, TypedValue.ADD);
writeMetaFile(deletedSet, TypedValue.DEL);
writeMetaFile(storedSet, TypedValue.STORED);

文件在生成的補丁包中的assets下面的res_meta.txt,可自行解壓補丁包查看。

在然后是soDecoder。

soDecoder其實就是BsDiffDecoder,與資源文件的比對和加載一樣,通過bsDiff進行新舊文件對比,然后生成增量更新包,加載則是通過bsPatch來進行的。

//new add file
String newMd5 = MD5.getMD5(newFile);
File bsDiffFile = getOutputPath(newFile).toFile();

if (oldFile == null || !oldFile.exists()) {
    FileOperation.copyFileUsingStream(newFile, bsDiffFile);
    writeLogFiles(newFile, null, null, newMd5);
    return true;
}

//both file length is 0
if (oldFile.length() == 0 && newFile.length() == 0) {
    return false;
}
if (oldFile.length() == 0 || newFile.length() == 0) {
    FileOperation.copyFileUsingStream(newFile, bsDiffFile);
    writeLogFiles(newFile, null, null, newMd5);
    return true;
}

//new add file
String oldMd5 = MD5.getMD5(oldFile);

if (oldMd5.equals(newMd5)) {
    return false;
}

if (!bsDiffFile.getParentFile().exists()) {
    bsDiffFile.getParentFile().mkdirs();
}
BSDiff.bsdiff(oldFile, newFile, bsDiffFile);

if (Utils.checkBsDiffFileSize(bsDiffFile, newFile)) {
    writeLogFiles(newFile, oldFile, bsDiffFile, newMd5);
} else {
    FileOperation.copyFileUsingStream(newFile, bsDiffFile);
    writeLogFiles(newFile, null, null, newMd5);
}

以上是so文件比對的核心部分,與res一致,首先也是判斷有沒有新增,其次比對文件的MD5是否一致,再然后是通過bsDiff進行比對生成差異化增量包。

tinker更推薦的是把so庫的加載交付給tinker去管理,這里貼出github wiki上的描述:

不使用Hack的方式
更新的Library庫文件我們幫你保存在tinker下面的子目錄下,但是我們并沒有為你區分abi(部分手機判斷不準確)。所以若想加載最新的庫,你有兩種方法,第一個是直接嘗試去Tinker更新的庫文件中加載,第二個參數是庫文件相對安裝包的路徑。

TinkerLoadLibrary.loadLibraryFromTinker(getApplicationContext(), "assets/x86", "libstlport_shared");

但是我們更推薦的是,使用TinkerInstaller.loadLibrary接管你所有的庫加載,它會自動先嘗試去Tinker中的庫文件中加載,但是需要注意的是當前這種方法只支持lib/armeabi目錄下的庫文件!

//load lib/armeabi library
TinkerLoadLibrary.loadArmLibrary(getApplicationContext(), "libstlport_shared");
//load lib/armeabi-v7a library
TinkerLoadLibrary.loadArmV7Library(getApplicationContext(), "libstlport_shared");

若存在Tinker還沒install之前調用加載補丁中的Library庫,可使用TinkerApplicationHelper.java的接口

//load lib/armeabi library
TinkerApplicationHelper.loadArmLibrary(tinkerApplicationLike, "libstlport_shared");
//load lib/armeabi-v7a library
TinkerApplicationHelper.loadArmV7Library(tinkerApplicationLike, "libstlport_shared");

若想對第三方代碼的庫文件更新,可先使用TinkerLoadLibrary.load*Library對第三方庫做提前的加載!更多使用方法可參考MainActivity.java。

查看了官方demo中的三個加載方式,使用起來正好對應的是:

// #method 1, hack classloader library path
TinkerLoadLibrary.installNavitveLibraryABI(getApplicationContext(), "armeabi");
System.loadLibrary("stlport_shared");

// #method 2, for lib/armeabi, just use TinkerInstaller.loadLibrary
TinkerLoadLibrary.loadArmLibrary(getApplicationContext(), "stlport_shared");

// #method 3, load tinker patch library directly
TinkerInstaller.loadLibraryFromTinker(getApplicationContext(), "assets/x86", "stlport_shared");

方法三這么寫,參考bugly封裝的TinkerManager:

public static void loadLibraryFromTinker(Context context, String relativePath, String libname) {
    TinkerLoadLibrary.loadLibraryFromTinker(context, relativePath, libname);
}

經過了以上幾個步驟之后,tinker會把所有比對后的記錄,增量資源打包到補丁當中,接下來分析一下tinker的Patch的過程

2.2 tinkerPatch的過程

前面在接入部分我使用了tinker的加載補丁的方法:

TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), patchFile.getAbsolutePath());

就開始從onReceiveUpgradePatch進行代碼的追蹤。

點進去這個方法:

/**
 * new patch file to install, try install them with :patch process
 *
 * @param context
 * @param patchLocation
 */
public static void onReceiveUpgradePatch(Context context, String patchLocation) {
    Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);
}

可以看到這里獲取了tinker對象的實例,通過app自身的application的上下文對象獲取到,而后通過patch的listener的方法進行加載,這里listener只是一個接口,分析這里需要回到第一部分installTinker的時候添加的:

//or you can just use DefaultLoadReporter
LoadReporter loadReporter = new SampleLoadReporter(appLike.getApplication());
//or you can just use DefaultPatchReporter
PatchReporter patchReporter = new SamplePatchReporter(appLike.getApplication());
//or you can just use DefaultPatchListener
PatchListener patchListener = new SamplePatchListener(appLike.getApplication());

這里主要看SamplePatchListener,追蹤到DefaultPatchListener的onPatchReceived方法,這里就是開始進行patch操作的地方。

首先通過patchCheck方法進行了一系列的校驗工作,然后通過TinkerPatchService.runPatchService(context, path);運行了起了一個patchService,繼續查看TinkerPatchService,可以看到以下兩個核心的啟動方法:

private static void runPatchServiceByIntentService(Context context, String path) {
    TinkerLog.i(TAG, "run patch service by intent service.");
    Intent intent = new Intent(context, IntentServiceRunner.class);
    intent.putExtra(PATCH_PATH_EXTRA, path);
    intent.putExtra(RESULT_CLASS_EXTRA, resultServiceClass.getName());
    context.startService(intent);
}

@TargetApi(21)
private static boolean runPatchServiceByJobScheduler(Context context, String path) {
    TinkerLog.i(TAG, "run patch service by job scheduler.");
    final JobInfo.Builder jobInfoBuilder = new JobInfo.Builder(
            1, new ComponentName(context, JobServiceRunner.class)
    );
    final PersistableBundle extras = new PersistableBundle();
    extras.putString(PATCH_PATH_EXTRA, path);
    extras.putString(RESULT_CLASS_EXTRA, resultServiceClass.getName());
    jobInfoBuilder.setExtras(extras);
    jobInfoBuilder.setOverrideDeadline(5);
    final JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
    if (jobScheduler == null) {
        TinkerLog.e(TAG, "jobScheduler is null.");
        return false;
    }
    return (jobScheduler.schedule(jobInfoBuilder.build()) == JobScheduler.RESULT_SUCCESS);
}

分別是android 21版本以上和以下兩個方法,這是因為在android 5.0之后,為了對系統的耗電量和內存管理進行優化,Google官方要求對后臺消耗資源的操作推薦放到JobScheduler中去執行。

IntentServiceRunner是改Services的核心,這是一個異步的service,查看他的onHandleIntent,可以看到,所有的操作都在doApplyPatch(getApplicationContext(), intent);當中:

@Override
protected void onHandleIntent(@Nullable Intent intent) {
    increasingPriority();
    doApplyPatch(getApplicationContext(), intent);
}

方法中核心是:

result = upgradePatchProcessor.tryPatch(context, path, patchResult);

而這個upgradePatchProcessor也是在初始化的時候就傳入的。

//you can set your own upgrade patch if you need
AbstractPatch upgradePatchProcessor = new UpgradePatch();

我們查看UpgradePatch的tryPatch進行查看,方法很長,主要核心的部分是:

//we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process
if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
    TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
    return false;
}

if (!BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
    TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch library failed");
    return false;
}

if (!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
    TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch resource failed");
    return false;
}

這里分別對應的是dex的patch,so文件的patch,資源文件的patch。

首先來看DexDiffPatchInternal.tryRecoverDexFiles,追蹤代碼可以看得出,經過一系列的操作校驗之后,patchDexFile是執行patch操作的關鍵方法,而所有的處理交給了new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(patchedDexFile),繼續追蹤下去,可以看到在生成補丁文件時候的熟悉代碼,那就是那一系列dex的比對操作:

// Secondly, run patch algorithms according to sections' dependencies.
this.stringDataSectionPatchAlg = new StringDataSectionPatchAlgorithm(
        patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.typeIdSectionPatchAlg = new TypeIdSectionPatchAlgorithm(
        patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.protoIdSectionPatchAlg = new ProtoIdSectionPatchAlgorithm(
        patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.fieldIdSectionPatchAlg = new FieldIdSectionPatchAlgorithm(
        patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.methodIdSectionPatchAlg = new MethodIdSectionPatchAlgorithm(
        patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.classDefSectionPatchAlg = new ClassDefSectionPatchAlgorithm(
        patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.typeListSectionPatchAlg = new TypeListSectionPatchAlgorithm(
        patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.annotationSetRefListSectionPatchAlg = new AnnotationSetRefListSectionPatchAlgorithm(
        patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.annotationSetSectionPatchAlg = new AnnotationSetSectionPatchAlgorithm(
        patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.classDataSectionPatchAlg = new ClassDataSectionPatchAlgorithm(
        patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.codeSectionPatchAlg = new CodeSectionPatchAlgorithm(
        patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.debugInfoSectionPatchAlg = new DebugInfoItemSectionPatchAlgorithm(
        patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.annotationSectionPatchAlg = new AnnotationSectionPatchAlgorithm(
        patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.encodedArraySectionPatchAlg = new StaticValueSectionPatchAlgorithm(
        patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.annotationsDirectorySectionPatchAlg = new AnnotationsDirectorySectionPatchAlgorithm(
        patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);

this.stringDataSectionPatchAlg.execute();
this.typeIdSectionPatchAlg.execute();
this.typeListSectionPatchAlg.execute();
this.protoIdSectionPatchAlg.execute();
this.fieldIdSectionPatchAlg.execute();
this.methodIdSectionPatchAlg.execute();
this.annotationSectionPatchAlg.execute();
this.annotationSetSectionPatchAlg.execute();
this.annotationSetRefListSectionPatchAlg.execute();
this.annotationsDirectorySectionPatchAlg.execute();
this.debugInfoSectionPatchAlg.execute();
this.codeSectionPatchAlg.execute();
this.classDataSectionPatchAlg.execute();
this.encodedArraySectionPatchAlg.execute();
this.classDefSectionPatchAlg.execute();

截取其中一些片段,上面在生成的部分我提到過一篇分析tinker核心算法的文章,里面也描述了dex的結構,如下圖:

image

這些比對的操作其實就是在對dex中的每個table進行的。

執行完畢之后,依舊是通過二路歸并的算法,生成經過了patch操作之后的dex文件,保存到本地目錄,等待loader的時候使用。

loader的后面說,繼續看其他兩個patch操作。

res的patch操作是ResDiffPatchInternal.tryRecoverResourceFiles,追蹤一下代碼,可以看到,首先第一步就是先讀取了之前生成的meta文件:

String resourceMeta = checker.getMetaContentMap().get(RES_META_FILE);

最后由extractResourceDiffInternals方法來進行補丁的合成,其實原理就是通過讀取meta里面記錄的每個資源對應的操作,來執行相關的增加、刪除、修改的操作,最后將資源文件打包為apk文件,android自帶的loader加載器其實也是支持.apk文件動態加載的。

so文件的操作就更簡單了,就是通過bsPatch進行一次patch操作,然后剩下的重載之類的方法前面也講到了,和applicationLike的原理差不多,就是通過增加一層代理操作的方式,來達到托管的效果。

patch完畢之后,tinker會在本地生成好可讀取的補丁文件,便于再次啟動的時候,進行加載。

2.3 tinker loader的過程

application 初始化的時候,就已經傳入了com.tencent.tinker.loader.TinkerLoader,而tinkerloader也是通過TinkerApplication中反射進行加載的:

private void loadTinker() {
    try {
        //reflect tinker loader, because loaderClass may be define by user!
        Class<?> tinkerLoadClass = Class.forName(loaderClassName, false, getClassLoader());
        Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class);
        Constructor<?> constructor = tinkerLoadClass.getConstructor();
        tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this);
    } catch (Throwable e) {
        //has exception, put exception error code
        tinkerResultIntent = new Intent();
        ShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION);
        tinkerResultIntent.putExtra(INTENT_PATCH_EXCEPTION, e);
    }
}

tinkerLoader的核心方法就是tryLoad,而該方法下又使用了tryLoadPatchFilesInternal,一個非常非常長的方法。

其實逐一分析過后,無非就是三個loader:

  1. TinkerDexLoader
  2. TinkerSoLoader
  3. TinkerResourceLoader

其余的一些方法中的操作大部分都是校驗參數合法性,文件完整性的一些操作,可以略過。
so的加載方式和原理前面已經說了,不在細說了,著重說一下dex和res的加載。

TinkerDexLoader.loadTinkerJars是加載dex的核心方法,點進去又能看到一大部分校驗的判斷的,核心的加載內容是SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);這段代碼,查看installDexes可以看到tinker區分版本進行安裝的操作:

@SuppressLint("NewApi")
public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)
    throws Throwable {
    Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());

    if (!files.isEmpty()) {
        files = createSortedAdditionalPathEntries(files);
        ClassLoader classLoader = loader;
        if (Build.VERSION.SDK_INT >= 24 && !checkIsProtectedApp(files)) {
            classLoader = AndroidNClassLoader.inject(loader, application);
        }
        //because in dalvik, if inner class is not the same classloader with it wrapper class.
        //it won't fail at dex2opt
        if (Build.VERSION.SDK_INT >= 23) {
            V23.install(classLoader, files, dexOptDir);
        } else if (Build.VERSION.SDK_INT >= 19) {
            V19.install(classLoader, files, dexOptDir);
        } else if (Build.VERSION.SDK_INT >= 14) {
            V14.install(classLoader, files, dexOptDir);
        } else {
            V4.install(classLoader, files, dexOptDir);
        }
        //install done
        sPatchDexCount = files.size();
        Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);

        if (!checkDexInstall(classLoader)) {
            //reset patch dex
            SystemClassLoaderAdder.uninstallPatchDex(classLoader);
            throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
        }
    }
}

這里之所以要區分版本,tinker官方也描述了一些android版本上的坑,前面分享過的連接中有詳細的描述,這里隨便點開一個看一下,我打開的v23的:

private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                                    File optimizedDirectory)
    throws IllegalArgumentException, IllegalAccessException,
    NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
    /* The patched class loader is expected to be a descendant of
     * dalvik.system.BaseDexClassLoader. We modify its
     * dalvik.system.DexPathList pathList field to append additional DEX
     * file entries.
     */
    Field pathListField = ShareReflectUtil.findField(loader, "pathList");
    Object dexPathList = pathListField.get(loader);
    ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
        new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
        suppressedExceptions));
    if (suppressedExceptions.size() > 0) {
        for (IOException e : suppressedExceptions) {
            Log.w(TAG, "Exception in makePathElement", e);
            throw e;
        }

    }
}

通過這個方法可以看出,tinker也是通過反射,獲取到系統ClassLoader的dexElements數組,并把需要修改的dex文件插入到了數組當中的最前端。

這里就是tinker整個代碼熱更新的原理,就是把合并過后的dex文件,插入到Elements數組的前端,因為android的類加載器在加載dex的時候,會按照數組的順序查找,如果在下標靠前的位置查找到了,就不繼續向下尋找了,所以也就起到了熱更新的作用。

繼續看Res的加載。

TinkerResourceLoader.loadTinkerResources。

方法中的核心部分是:TinkerResourcePatcher.monkeyPatchExistingResources(application, resourceString);

/**
 * @param context
 * @param externalResourceFile
 * @throws Throwable
 */
public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable {
    if (externalResourceFile == null) {
        return;
    }

    final ApplicationInfo appInfo = context.getApplicationInfo();

    final Field[] packagesFields;
    if (Build.VERSION.SDK_INT < 27) {
        packagesFields = new Field[]{packagesFiled, resourcePackagesFiled};
    } else {
        packagesFields = new Field[]{packagesFiled};
    }
    for (Field field : packagesFields) {
        final Object value = field.get(currentActivityThread);

        for (Map.Entry<String, WeakReference<?>> entry
                : ((Map<String, WeakReference<?>>) value).entrySet()) {
            final Object loadedApk = entry.getValue().get();
            if (loadedApk == null) {
                continue;
            }
            final String resDirPath = (String) resDir.get(loadedApk);
            if (appInfo.sourceDir.equals(resDirPath)) {
                resDir.set(loadedApk, externalResourceFile);
            }
        }
    }

    // Create a new AssetManager instance and point it to the resources installed under
    if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {
        throw new IllegalStateException("Could not create new AssetManager");
    }

    // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
    // in L, so we do it unconditionally.
    if (stringBlocksField != null && ensureStringBlocksMethod != null) {
        stringBlocksField.set(newAssetManager, null);
        ensureStringBlocksMethod.invoke(newAssetManager);
    }

    for (WeakReference<Resources> wr : references) {
        final Resources resources = wr.get();
        if (resources == null) {
            continue;
        }
        // Set the AssetManager of the Resources instance to our brand new one
        try {
            //pre-N
            assetsFiled.set(resources, newAssetManager);
        } catch (Throwable ignore) {
            // N
            final Object resourceImpl = resourcesImplFiled.get(resources);
            // for Huawei HwResourcesImpl
            final Field implAssets = findField(resourceImpl, "mAssets");
            implAssets.set(resourceImpl, newAssetManager);
        }

        clearPreloadTypedArrayIssue(resources);

        resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
    }

    // Handle issues caused by WebView on Android N.
    // Issue: On Android N, if an activity contains a webview, when screen rotates
    // our resource patch may lost effects.
    // for 5.x/6.x, we found Couldn't expand RemoteView for StatusBarNotification Exception
    if (Build.VERSION.SDK_INT >= 24) {
        try {
            if (publicSourceDirField != null) {
                publicSourceDirField.set(context.getApplicationInfo(), externalResourceFile);
            }
        } catch (Throwable ignore) {
        }
    }

    if (!checkResUpdate(context)) {
        throw new TinkerRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL);
    }
}

這里分析不難看出,其實就是通過反射的方法,替換掉系統的AssetManager,也就是mAssets這個變量,而新的NewAssetManager指向的resource是新的資源路徑,這樣在系統調用mAssets進行加載資源的時候,使用的就是熱更新后的資源了。

over~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容