通過上一篇文章《tinker熱修復——dex補丁載入過程》,基本上我們已經熟悉了tinker載入dex的過程,tinker除了能夠新增類,還可以新增資源和so庫等,那麼tinker是如何做到熱更資源的呢?資源補丁的載入過程又是怎麼樣的呢?
我們都知道,tinker打補丁包的時候,只會打diff的補丁包,也就說補丁包中包括資源的diff,而生成資源的diff的時候,會把變化的索引寫在補丁包的assets/res_meta.txt中,當補丁下發到app後,會將所有的資源整合起來生成一個resources.apk(該資源包,包含新增的資源和原來的資源),在載入資源補丁時候就是載入這個包含所有資源的包,然後將LoadApk中的資源路徑替換成補丁的路徑,並將Resources容器中的屬性都替換成新的載入補丁的AssertManager。那麼接下來我們看看tinker是如何一步步載入資源補丁的。
在《tinker熱修復——dex補丁載入過程》文章中知道,最終呼叫的載入過程都是在TinkerLoader的tryLoadPatchFilesInternal方法中的。那麼我們重點關注該方法,我們注意到經過對補丁包的一層層的安全校驗,檢查資源補丁的時候,會呼叫TinkerResourceLoader的checkComplete方法。
/**
* resource file exist?
* fast check, only check whether exist
*
* @param directory
* @return boolean
*/
public static boolean checkComplete(Context context, String directory, ShareSecurityCheck securityCheck, Intent intentResult) {
//拿到補丁包中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
* 將meta中第一行的資料讀取到resPatchInfo中用來快速校驗.
*/
ShareResPatchInfo.parseResPatchInfoFirstLine(meta, resPatchInfo);
if (resPatchInfo.resArscMd5 == null) {
return true;
}
//checkResPatchInfo:檢查asrc檔案的MD5本身是否為空或者長度是否合法
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;
}
/**
* 校驗合成資源補丁的路徑和檔案是否存在
*/
//合成資源補丁的路徑
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;
}
//合成資源補丁的檔案
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;
}
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;
}
複製程式碼
該方法首先會讀取res_meta.txt的資訊,並把meta中的第一行的資料讀取到resPatchInfo中,做一個快速檢驗,然後檢查檔案的md5是否有效,以及檢查資源補丁的檔案是否有效,最後呼叫TinkerResourcePatcher的isResourceCanPatch方法。
/**
* 獲取資源更新時需要的method和Field
* 並且儲存起來,方便補丁載入時使用
* @param context
* @throws Throwable
*/
public static void isResourceCanPatch(Context context) throws Throwable {
// - Replace mResDir to point to the external resource file instead of the .apk. This is
// used as the asset path for new Resources objects.
// - Set Application#mLoadedApk to the found LoadedApk instance
/**
*
* Find the ActivityThread instance for the current thread
* 獲取當前執行緒的ActivityThread物件
*/
Class<?> activityThread = Class.forName("android.app.ActivityThread");
currentActivityThread = ShareReflectUtil.getActivityThread(context, activityThread);
/**
*
* API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know.
* LoadedApk是從Android 2.3開始才有的
* 該類是對之前系統ActivityThread的內部類PackageInfo重新封裝而成的.所以這裡要分開處理.
*/
Class<?> loadedApkClass;
try {
loadedApkClass = Class.forName("android.app.LoadedApk");
} catch (ClassNotFoundException e) {
loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");
}
//獲取loadedApkClass的mResDir屬性,設定成可訪問
resDir = loadedApkClass.getDeclaredField("mResDir");
resDir.setAccessible(true);
//獲取activityThread的mPackages屬性,設定成可訪問
packagesFiled = activityThread.getDeclaredField("mPackages");
packagesFiled.setAccessible(true);
//獲取activityThread的mResourcePackages屬性,設定可訪問
resourcePackagesFiled = activityThread.getDeclaredField("mResourcePackages");
resourcePackagesFiled.setAccessible(true);
/**
*
* Create a new AssetManager instance and point it to the resources
* 建立一個AssetManager例項
* 一些ROM修改了類名,如Baidu的ROM改成了android.content.res.BaiduAssetManager
* 所以要做相容
*/
AssetManager assets = context.getAssets();
// Baidu os
if (assets.getClass().getName().equals("android.content.res.BaiduAssetManager")) {
Class baiduAssetManager = Class.forName("android.content.res.BaiduAssetManager");
newAssetManager = (AssetManager) baiduAssetManager.getConstructor().newInstance();
} else {
newAssetManager = AssetManager.class.getConstructor().newInstance();
}
//獲取AssetManager的addAssetPath方法,設定成可訪問
addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.setAccessible(true);
/**
*
* Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
* in L, so we do it unconditionally.
* 在Kitkat需要呼叫AssetManager的ensureStringBlocks方法
* 在Lollipop則不需要呼叫
* 但是不用區分系統,照常呼叫,不會造成任何問題
*/
ensureStringBlocksMethod = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
ensureStringBlocksMethod.setAccessible(true);
/**
* 載入補丁要替換的Resources物件在KITKAT以下是以HashMap的型別作為ActivityThread類的屬性.
* 其餘的系統版本都是以ArrayMap被ResourcesManager持有的.
* 需要做系統區分
*/
// Iterate over all known Resources objects
//獲取所有的Resources物件的引用
if (SDK_INT >= KITKAT) {
//pre-N
// Find the singleton instance of ResourcesManager
Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance");
mGetInstance.setAccessible(true);
//呼叫getInstance方法,拿到ResourcesManager物件
Object resourcesManager = mGetInstance.invoke(null);
try {
//獲取ResourcesManager的mActiveResources物件,設定可訪問
Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
//獲取得到所有Resources物件的引用
ArrayMap<?, WeakReference<Resources>> activeResources19 =
(ArrayMap<?, WeakReference<Resources>>) fMActiveResources.get(resourcesManager);
references = activeResources19.values();
} catch (NoSuchFieldException ignore) {
// N moved the resources to mResourceReferences
Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
mResourceReferences.setAccessible(true);
references = (Collection<WeakReference<Resources>>) mResourceReferences.get(resourcesManager);
}
} else {
Field fMActiveResources = activityThread.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
HashMap<?, WeakReference<Resources>> activeResources7 =
(HashMap<?, WeakReference<Resources>>) fMActiveResources.get(currentActivityThread);
references = activeResources7.values();
}
// check resource,校驗是否為null
if (references == null) {
throw new IllegalStateException("resource references is null");
}
// fix jianGuo pro has private field 'mAssets' with Resource
// try use mResourcesImpl first
//將Resources物件中持有AssetManager物件引用的屬性mAssets,hook出來
//mAssets是載入補丁時進行替換的
if (SDK_INT >= 24) {
//Android N的路徑變成了Resources -> ResourcesImpl -> AssetManager
try {
// N moved the mAssets inside an mResourcesImpl field
resourcesImplFiled = Resources.class.getDeclaredField("mResourcesImpl");
resourcesImplFiled.setAccessible(true);
} catch (Throwable ignore) {
// for safety
assetsFiled = Resources.class.getDeclaredField("mAssets");
assetsFiled.setAccessible(true);
}
} else {
assetsFiled = Resources.class.getDeclaredField("mAssets");
assetsFiled.setAccessible(true);
}
// final Resources resources = context.getResources();
// isMiuiSystem = resources != null && MIUI_RESOURCE_CLASSNAME.equals(resources.getClass().getName());
try {
publicSourceDirField = ShareReflectUtil.findField(ApplicationInfo.class, "publicSourceDir");
} catch (NoSuchFieldException ignore) {
}
}
複製程式碼
該方法就是為資源補丁的替換做準備的工作,並且把這些準備的工作儲存起來,等到替換的時候用。首先獲取ActivityThread和LoadedApk,通過反射獲取LoadedApk的mResDir屬性,該屬性需要設定補丁包的路徑。通過ActivityThread獲取mPackages和mResourcePackages屬性,然後建立新的AssetManager同時通過反射獲取AssetManager的addAssetPath方法(通過addAssetPath可以將補丁載入到新的AssetManager中)。
Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance");
mGetInstance.setAccessible(true);
//呼叫getInstance方法,拿到ResourcesManager物件
Object resourcesManager = mGetInstance.invoke(null);
複製程式碼
通過反射呼叫getInstance方法,拿到ResourcesManager物件,因為該物件持有Resources容器物件,因此通過反射就可以拿到所有的Resources物件,拿到Resources物件就可以將其屬性替換為新建立的AssetManager。
這裡就是通過反射拿到Resources物件中持有AssetManager物件引用的屬性mAssets,而mAssets是載入補丁時需要進行替換的。
publicSourceDirField = ShareReflectUtil.findField(ApplicationInfo.class, "publicSourceDir");
複製程式碼
最後獲取ApplicationInfo的屬性publicSourceDir是瞭解決Android的webview引起的問題,在Android N上,如果一個活動包含一個webview,當螢幕旋轉時,資源補丁可能會失去效果。
當全部校驗通過,並且為資源補丁更新做好準備之後,呼叫TinkerResourceLoader的loadTinkerResources方法進行資源補丁更新。
loadTinkerResources:
/**
* Load tinker resources
*/
public static boolean loadTinkerResources(TinkerApplication application, String directory, Intent intentResult) {
//判斷是否有資源更新
if (resPatchInfo == null || resPatchInfo.resArscMd5 == null) {
return true;
}
//補丁檔案
String resourceString = directory + "/" + RESOURCE_PATH + "/" + RESOURCE_FILE;
File resourceFile = new File(resourceString);
long start = System.currentTimeMillis();
if (application.isTinkerLoadVerifyFlag()) {
//校驗檔案的md5
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));
}
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
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;
}
return true;
}
複製程式碼
該方法首先檢查資原始檔是否有更新,然後找到資原始檔路徑,並檢查md5是否校驗通過,再進一步呼叫TinkerResourcePatcher的monkeyPatchExistingResources方法來載入資源。
/**
* @param context
* @param externalResourceFile
* @throws Throwable
*/
public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable {
if (externalResourceFile == null) {
return;
}
for (Field field : new Field[]{packagesFiled, resourcePackagesFiled}) {
//獲取當前執行緒的ActivityThread物件
Object value = field.get(currentActivityThread);
for (Map.Entry<String, WeakReference<?>> entry
: ((Map<String, WeakReference<?>>) value).entrySet()) {
//獲取LoadedApk容器的物件
Object loadedApk = entry.getValue().get();
if (loadedApk == null) {
continue;
}
if (externalResourceFile != null) {
//將LoadedApk物件的mResDir屬性的值替換成資源補丁包的路徑
resDir.set(loadedApk, externalResourceFile);
}
}
}
/**
*
* Create a new AssetManager instance and point it to the resources installed under
* 通過addAssetPath方法,將補丁載入到新的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.
ensureStringBlocksMethod.invoke(newAssetManager);
//遍歷ResourcesManager持有的Resources容器物件
for (WeakReference<Resources> wr : references) {
Resources resources = wr.get();
//pre-N
if (resources != null) {
// Set the AssetManager of the Resources instance to our brand new one
try {
//替換Resources的屬性assetsFiled為新的AssetManager
assetsFiled.set(resources, newAssetManager);
} catch (Throwable ignore) {
// N
Object resourceImpl = resourcesImplFiled.get(resources);
// for Huawei HwResourcesImpl
Field implAssets = ShareReflectUtil.findField(resourceImpl, "mAssets");
implAssets.setAccessible(true);
implAssets.set(resourceImpl, newAssetManager);
}
clearPreloadTypedArrayIssue(resources);
//根據原屬性重新更新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);
}
}
複製程式碼
1、獲取當前執行緒的ActivityThread物件和該物件持有的LoadedApk容器;
2、遍歷容器中的LoadedApk物件,替換LoadedApk的mResDir屬性為補丁物理路徑;
3、通過addAssetPath方法,將補丁載入到新建立的AssetManager中;
4、遍歷ResourcesManager持有的Resources容器物件;
5、替換Resources的屬性assetsFiled為新的AssetManager;
6、根據原屬性重新更新Resources物件的配置;
7、呼叫checkResUpdate方法,補丁生效校驗及解除安裝;
8、更加詳細說明請看文章的程式碼的註釋。
以上就是tinker的資源補丁載入的全過程,本人也是剛剛涉及熱修復的,如果哪裡講的不夠明白,也可以參考《Android 熱修復方案Tinker(四) 資源補丁載入》,該文章講得更加詳細,思路也非常清晰。