Tinker源碼分析(四):加載資源補(bǔ)丁流程

本系列 Tinker 源碼解析基于 Tinker v1.9.12

加載資源補(bǔ)丁流程

將到資源補(bǔ)丁的加載,首先還要回過(guò)頭來(lái)先看資源補(bǔ)丁的校驗(yàn)和檢查。

我們回到 TinkerLoader.tryLoadPatchFilesInternal 方法中來(lái)看。

tryLoadPatchFilesInternal

//check resource
final boolean isEnabledForResource = ShareTinkerInternals.isTinkerEnabledForResource(tinkerFlag);
Log.w(TAG, "tryLoadPatchFiles:isEnabledForResource:" + isEnabledForResource);
if (isEnabledForResource) {
    boolean resourceCheck = TinkerResourceLoader.checkComplete(app, patchVersionDirectory, securityCheck, resultIntent);
    if (!resourceCheck) {
        //file not found, do not load patch
        Log.w(TAG, "tryLoadPatchFiles:resource check fail");
        return;
    }
}

具體的校驗(yàn)是在 TinkerResourceLoader.checkComplete 中完成的。這里為了校驗(yàn)的速度,所以只會(huì)校驗(yàn)資源補(bǔ)丁存不存在。

checkComplete

checkComplete 方法我們分段來(lái)看吧

// 讀取 assets/res_meta.txt 
String meta = securityCheck.getMetaContentMap().get(RESOURCE_META_FILE);
//not found resource
if (meta == null) {
    return true;
}
//only parse first line for faster
ShareResPatchInfo.parseResPatchInfoFirstLine(meta, resPatchInfo);

為了校驗(yàn)的速度,只讀取了 assets/res_meta.txt 的第一行,并存入到 resPatchInfo 中

res_meta.txt 的第一行主要是資源的 crc 值和 md5 值 ,在后面會(huì)做校驗(yàn)。

if (resPatchInfo.resArscMd5 == null) {
    return true;
}
if (!ShareResPatchInfo.checkResPatchInfo(resPatchInfo)) {
    intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_PATCH_CHECK, ShareConstants.ERROR_PACKAGE_CHECK_RESOURCE_META_CORRUPTED);
    ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_PACKAGE_CHECK_FAIL);
    return false;
}

校驗(yàn)上面讀取到的 md5 值是否為空以及 md5 值長(zhǎng)度是否是 32 位

String resourcePath = directory + "/" + RESOURCE_PATH + "/";

File resourceDir = new File(resourcePath);

if (!resourceDir.exists() || !resourceDir.isDirectory()) {
    ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_DIRECTORY_NOT_EXIST);
    return false;
}

校驗(yàn)資源補(bǔ)丁文件夾是否存在。

File resourceFile = new File(resourcePath + RESOURCE_FILE);
if (!SharePatchFileUtil.isLegalFile(resourceFile)) {
    ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_FILE_NOT_EXIST);
    return false;
}

校驗(yàn)資源補(bǔ)丁文件是否存在及合法性。

try {
    TinkerResourcePatcher.isResourceCanPatch(context);
} catch (Throwable e) {
    Log.e(TAG, "resource hook check failed.", e);
    intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
    ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_LOAD_EXCEPTION);
    return false;
}
return true;

通過(guò) context 來(lái)檢查當(dāng)前環(huán)境是否支持加載資源補(bǔ)丁。方法里面做的事就是通過(guò)反射來(lái)獲取各種系統(tǒng)的屬性和方法。簡(jiǎn)單地舉例以下幾種:

  • ActivityThread : 當(dāng)前的 ActivityThread 實(shí)例,app主線程的入口。利用 ActivityThread 可以獲取到 LoadedApk 對(duì)象;
  • LoadedApk : 通過(guò) LoadedApk 可以獲取 mResDir 屬性;
  • mResDir : 這個(gè)值很關(guān)鍵,就是資源文件的路徑。在后面會(huì)被 hook 成資源補(bǔ)丁的路徑;
  • addAssetPath : 通過(guò) addAssetPath 方法將資源補(bǔ)丁文件加載進(jìn)新的 AssetManager 中;
  • mActiveResources : ResourcesManager 的 Resources 容器。里面會(huì)存儲(chǔ)著每個(gè) apk 對(duì)應(yīng)的 Resources 對(duì)象。mActiveResources 是 ArrayMap 類型的,不同的 apk 都有一個(gè)不同的 key 來(lái)獲取對(duì)應(yīng)的 apk 的 Resource 對(duì)象;
  • mAssets : 即 Resources 類中的 mAssets 屬性,其實(shí)就是一個(gè) AssetManager 對(duì)象。在資源打補(bǔ)丁的時(shí)候,Resources 中原來(lái)的 mAssets 對(duì)象會(huì)被替換成新的 AssetManager 對(duì)象。

這里就不詳細(xì)講了,總結(jié)起來(lái)就一句話:獲取 Android 系統(tǒng)中與資源有關(guān)的一些屬性和方法,為接下來(lái)的加載資源補(bǔ)丁做準(zhǔn)備。如果在 isResourceCanPatch 方法中報(bào)出異常了,就認(rèn)為當(dāng)前環(huán)境不能加載資源補(bǔ)丁了。

tryLoadPatchFilesInternal

然后我們?cè)僭?tryLoadPatchFilesInternal 中往下看。會(huì)看到資源補(bǔ)丁加載代碼的入口,即 TinkerResourceLoader.loadTinkerResources 方法

//now we can load patch resource
if (isEnabledForResource) {
    boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(app, patchVersionDirectory, resultIntent);
    if (!loadTinkerResources) {
        Log.w(TAG, "tryLoadPatchFiles:onPatchLoadResourcesFail");
        return;
    }
}

loadTinkerResources

loadTinkerResources 方法我們分段來(lái)看。

    //  檢查 res_meta.txt 中讀取出來(lái)的 md5 值,如果 resPatchInfo 或者 md5 是空的,就說(shuō)明補(bǔ)丁包中沒(méi)有資源補(bǔ)丁,不需要加載
    if (resPatchInfo == null || resPatchInfo.resArscMd5 == null) {
        return true;
    }
    String resourceString = directory + "/" + RESOURCE_PATH +  "/" + RESOURCE_FILE;
    File resourceFile = new File(resourceString);
    long start = System.currentTimeMillis();
    // 如果校驗(yàn)設(shè)置為 true ,就去校驗(yàn)資源補(bǔ)丁包 resources.apk 的 md5 值
    if (application.isTinkerLoadVerifyFlag()) {
        if (!SharePatchFileUtil.checkResourceArscMd5(resourceFile, resPatchInfo.resArscMd5)) {
            Log.e(TAG, "Failed to load resource file, path: " + resourceFile.getPath() + ", expect md5: " + resPatchInfo.resArscMd5);
            ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_MD5_MISMATCH);
            return false;
        }
        Log.i(TAG, "verify resource file:" + resourceFile.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
    }

然后就是加載資源補(bǔ)丁了,如果加載失敗了,會(huì)把 dex 補(bǔ)丁卸載了。防止 dex 補(bǔ)丁代碼中會(huì)引用到資源補(bǔ)丁中的資源文件,導(dǎo)致程序崩潰或報(bào)錯(cuò)。

try {
    // 加載資源
    TinkerResourcePatcher.monkeyPatchExistingResources(application, resourceString);
    Log.i(TAG, "monkeyPatchExistingResources resource file:" + resourceString + ", use time: " + (System.currentTimeMillis() - start));
} catch (Throwable e) {
    Log.e(TAG, "install resources failed");
    //remove patch dex if resource is installed failed
    // 如果資源補(bǔ)丁加載失敗的話,會(huì)移除 dex 補(bǔ)丁
    // 因?yàn)槿绻鹍ex補(bǔ)丁代碼中有引用到資源的話,會(huì)報(bào)錯(cuò)
    try {
        SystemClassLoaderAdder.uninstallPatchDex(application.getClassLoader());
    } catch (Throwable throwable) {
        Log.e(TAG, "uninstallPatchDex failed", e);
    }
    intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
    ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_LOAD_EXCEPTION);
    return false;
}

monkeyPatchExistingResources

monkeyPatchExistingResources 方法也一段一段來(lái)看

// 檢查資源補(bǔ)丁apk是否為空
if (externalResourceFile == null) {
    return;
}

final ApplicationInfo appInfo = context.getApplicationInfo();

final Field[] packagesFields;
// 準(zhǔn)備之前反射好的 packagesFiled 和 resourcePackagesFiled 字段
// 利用 packagesFiled 和 resourcePackagesFiled 可以獲取 LoadedApk 對(duì)象
if (Build.VERSION.SDK_INT < 27) {
    packagesFields = new Field[]{packagesFiled, resourcePackagesFiled};
} else {
    packagesFields = new Field[]{packagesFiled};
}
// 遍歷 packagesFields ,獲取對(duì)應(yīng)的值
for (Field field : packagesFields) {
    // 獲取 ActivityThread 中 packagesFiled 或 resourcePackagesFiled
    // value 其實(shí)為 Map<String, WeakReference<LoadedApk>> 類型
    final Object value = field.get(currentActivityThread);
    // 再對(duì) value 進(jìn)行遍歷,獲取 LoadedApk 對(duì)象
    for (Map.Entry<String, WeakReference<?>> entry
            : ((Map<String, WeakReference<?>>) value).entrySet()) {
        final Object loadedApk = entry.getValue().get();
        if (loadedApk == null) {
            continue;
        }
        // 從 LoadedApk 對(duì)象中獲取 mResDir 屬性,這個(gè)屬性的意義在上面已經(jīng)講過(guò)了
        final String resDirPath = (String) resDir.get(loadedApk);
        // 將 mResDir 的值 hook 成資源補(bǔ)丁 apk 的路徑
        if (appInfo.sourceDir.equals(resDirPath)) {
            resDir.set(loadedApk, externalResourceFile);
        }
    }
}

上面這段代碼基本上都有注釋了,接著往下看

// Create a new AssetManager instance and point it to the resources installed under
// 創(chuàng)建一個(gè)新的 AssetManager 實(shí)例,并把資源補(bǔ)丁apk加載進(jìn) AssetManager 中
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.
// 創(chuàng)建出 AssetManager 后,調(diào)用 ensureStringBlocks 來(lái)確保資源的字符串索引創(chuàng)建出來(lái)
if (stringBlocksField != null && ensureStringBlocksMethod != null) {
    stringBlocksField.set(newAssetManager, null);
    ensureStringBlocksMethod.invoke(newAssetManager);
}

在創(chuàng)建出新的 AssetManager 之后,最后要做的事就是用新的 AssetManager 來(lái)替換舊的。下面代碼中的 references 就是上面提到的 mActiveResources 的 value 集合。也就是每個(gè) apk 的 Resources 資源集合。

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
        // Android N 之前的方案
        // 把原來(lái) resources 的 mAssets 屬性替換成新的 AssetManager 對(duì)象
        assetsFiled.set(resources, newAssetManager);
    } catch (Throwable ignore) {
        // N
        // Android N 之后, mAssets 屬性被放在了 ResourcesImpl 中
        // 所以需要先獲取 ResourcesImpl 對(duì)象再進(jìn)行替換
        final Object resourceImpl = resourcesImplFiled.get(resources);
        // for Huawei HwResourcesImpl
        final Field implAssets = findField(resourceImpl, "mAssets");
        implAssets.set(resourceImpl, newAssetManager);
    }
    // 在 Resource 中會(huì)維護(hù)一個(gè) mTypedArrayPool 資源池
    // 來(lái)減少頻繁訪問(wèn) AssetManager ,所以需要去釋放這個(gè)資源池,否則取到的都是緩存
    clearPreloadTypedArrayIssue(resources);
    // 最后調(diào)用 updateConfiguration 方法來(lái)確保資源更新了
    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) {
    }
}

最后,就是來(lái)確認(rèn)一下資源補(bǔ)丁是否已經(jīng)加載成功了。具體的方法就是在資源補(bǔ)丁Apk的 assets 中有一個(gè) Tinker 的測(cè)試資源,名字叫 only_use_to_test_tinker_resource.txt ,如果可以正確讀取到并且沒(méi)報(bào)錯(cuò)的話,就證明資源補(bǔ)丁加載成功了。否則就拋出異常,會(huì)執(zhí)行 dex 補(bǔ)丁卸載的流程。

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

到這里,資源補(bǔ)丁的加載流程就講完了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容