本系列 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ǔ)丁的加載流程就講完了。