上一篇文章介紹了Dex文件的熱更新流程,本文將會分析Tinker中對資源文件的熱更新流程。 同Dex,資源文件的熱更新同樣包括三個部分:資源補丁生成,資源補丁合成及資源補丁載入。 本系列將從以下三個方面對Tinker進行源碼解析: 轉載請標明本文來源:http://www.cnblogs.com/y ...
上一篇文章介紹了Dex文件的熱更新流程,本文將會分析Tinker中對資源文件的熱更新流程。
同Dex,資源文件的熱更新同樣包括三個部分:資源補丁生成,資源補丁合成及資源補丁載入。
本系列將從以下三個方面對Tinker進行源碼解析:
- Android熱更新開源項目Tinker源碼解析系列之一:Dex熱更新
- Android熱更新開源項目Tinker源碼解析系列之二:資源熱更新
- Android熱更新開源項目Tinker源碼解析系類之三:so熱更新
轉載請標明本文來源:http://www.cnblogs.com/yyangblog/p/6252490.html
更多內容歡迎star作者的github:https://github.com/LaurenceYang/article
如果發現本文有什麼問題和任何建議,也隨時歡迎交流~
一、資源補丁生成
ResDiffDecoder.patch(File oldFile, File newFile)主要負責資源文件補丁的生成。
如果是新增的資源,直接將資源文件拷貝到目標目錄。
如果是修改的資源文件則使用dealWithModeFile函數處理。
1 // 如果是新增的資源,直接將資源文件拷貝到目標目錄.
2 if (oldFile == null || !oldFile.exists()) {
3 if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) {
4 Logger.e("found add resource: " + name + " ,but it match ignore change pattern, just ignore!");
5 return false;
6 }
7 FileOperation.copyFileUsingStream(newFile, outputFile);
8 addedSet.add(name);
9 writeResLog(newFile, oldFile, TypedValue.ADD);
10 return true;
11 }
12 ...
13 // 新舊資源文件的md5一樣,表示沒有修改.
14 if (oldMd5 != null && oldMd5.equals(newMd5)) {
15 return false;
16 }
17 ...
18 // 修改的資源文件使用dealWithModeFile函數處理.
19 dealWithModeFile(name, newMd5, oldFile, newFile, outputFile);
dealWithModeFile會對文件大小進行判斷,如果大於設定值(預設100Kb),採用bsdiff演算法對新舊文件比較生成補丁包,從而降低補丁包的大小。
如果小於設定值,則直接將該文件加入修改列表,並直接將該文件拷貝到目標目錄。
1 if (checkLargeModFile(newFile)) { //大文件採用bsdiff演算法
2 if (!outputFile.getParentFile().exists()) {
3 outputFile.getParentFile().mkdirs();
4 }
5 BSDiff.bsdiff(oldFile, newFile, outputFile);
6 //treat it as normal modify
7 // 對生成的diff文件大小和newFile進行比較,只有在達到我們的壓縮效果後才使用diff文件
8 if (Utils.checkBsDiffFileSize(outputFile, newFile)) {
9 LargeModeInfo largeModeInfo = new LargeModeInfo();
10 largeModeInfo.path = newFile;
11 largeModeInfo.crc = FileOperation.getFileCrc32(newFile);
12 largeModeInfo.md5 = newMd5;
13 largeModifiedSet.add(name);
14 largeModifiedMap.put(name, largeModeInfo);
15 writeResLog(newFile, oldFile, TypedValue.LARGE_MOD);
16 return true;
17 }
18 }
19 modifiedSet.add(name); // 加入修改列表
20 FileOperation.copyFileUsingStream(newFile, outputFile);
21 writeResLog(newFile, oldFile, TypedValue.MOD);
22 return false;
BsDiff屬於二進位比較,其具體實現大家可以自行百度。
ResDiffDecoder.onAllPatchesEnd()中會加入一個測試用的資源文件,放在assets目錄下,用於在載入補丁時判斷其是否加在成功。
這一步同時會向res_meta.txt文件中寫入資源更改的信息。
1 //加入一個測試用的資源文件
2 addAssetsFileForTestResource();
3 ...
4 //first, write resource meta first
5 //use resources.arsc's base crc to identify base.apk
6 String arscBaseCrc = FileOperation.getZipEntryCrc(config.mOldApkFile, TypedValue.RES_ARSC);
7 String arscMd5 = FileOperation.getZipEntryMd5(extractToZip, TypedValue.RES_ARSC);
8 if (arscBaseCrc == null || arscMd5 == null) {
9 throw new TinkerPatchException("can't find resources.arsc's base crc or md5");
10 }
11
12 String resourceMeta = Utils.getResourceMeta(arscBaseCrc, arscMd5);
13 writeMetaFile(resourceMeta);
14
15 //pattern
16 String patternMeta = TypedValue.PATTERN_TITLE;
17 HashSet<String> patterns = new HashSet<>(config.mResRawPattern);
18 //we will process them separate
19 patterns.remove(TypedValue.RES_MANIFEST);
20
21 writeMetaFile(patternMeta + patterns.size());
22 //write pattern
23 for (String item : patterns) {
24 writeMetaFile(item);
25 }
26 //write meta file, write large modify first
27 writeMetaFile(largeModifiedSet, TypedValue.LARGE_MOD);
28 writeMetaFile(modifiedSet, TypedValue.MOD);
29 writeMetaFile(addedSet, TypedValue.ADD);
30 writeMetaFile(deletedSet, TypedValue.DEL);
最後的res_meta.txt文件的格式範例如下:
resources_out.zip,4019114434,6148149bd5ed4e0c2f5357c6e2c577d6
pattern:4
resources.arsc
r/*
res/*
assets/*
modify:1
r/g/ag.xml
add:1
assets/only_use_to_test_tinker_resource.txt
到此,資源文件的補丁打包流程結束。
二、補丁下發成功後資源補丁的合成
ResDiffPatchInternal.tryRecoverResourceFiles會調用extractResourceDiffInternals進行補丁的合成。
合成過程比較簡單,沒有使用bsdiff生成的文件直接寫入到resources.apk文件;
使用bsdiff生成的文件則採用bspatch演算法合成資源文件,然後將合成文件寫入resouces.apk文件。
最後,生成的resouces.apk文件會存放到/data/data/${package_name}/tinker/res對應的目錄下。
1 / 首先讀取res_meta.txt的數據
2 ShareResPatchInfo.parseAllResPatchInfo(meta, resPatchInfo);
3 // 驗證resPatchInfo的MD5是否合法
4 if (!SharePatchFileUtil.checkIfMd5Valid(resPatchInfo.resArscMd5)) {
5 ...
6 // resources.apk
7 File resOutput = new File(directory, ShareConstants.RES_NAME);
8
9 // 該函數裡面會對largeMod的文件進行合成,合成的演算法也是採用bsdiff
10 if (!checkAndExtractResourceLargeFile(context, apkPath, directory, patchFile, resPatchInfo, type, isUpgradePatch)) {
11
12 // 基於oldapk,合併補丁後將這些資源文件寫入resources.apk文件中
13 while (entries.hasMoreElements()) {
14 TinkerZipEntry zipEntry = entries.nextElement();
15 if (zipEntry == null) {
16 throw new TinkerRuntimeException("zipEntry is null when get from oldApk");
17 }
18 String name = zipEntry.getName();
19 if (ShareResPatchInfo.checkFileInPattern(resPatchInfo.patterns, name)) {
20 //won't contain in add set.
21 if (!resPatchInfo.deleteRes.contains(name)
22 && !resPatchInfo.modRes.contains(name)
23 && !resPatchInfo.largeModRes.contains(name)
24 && !name.equals(ShareConstants.RES_MANIFEST)) {
25 ResUtil.extractTinkerEntry(oldApk, zipEntry, out);
26 totalEntryCount++;
27 }
28 }
29 }
30
31 //process manifest
32 TinkerZipEntry manifestZipEntry = oldApk.getEntry(ShareConstants.RES_MANIFEST);
33 if (manifestZipEntry == null) {
34 TinkerLog.w(TAG, "manifest patch entry is null. path:" + ShareConstants.RES_MANIFEST);
35 manager.getPatchReporter().onPatchTypeExtractFail(patchFile, resOutput, ShareConstants.RES_MANIFEST, type, isUpgradePatch);
36 return false;
37 }
38 ResUtil.extractTinkerEntry(oldApk, manifestZipEntry, out);
39 totalEntryCount++;
40
41 for (String name : resPatchInfo.largeModRes) {
42 TinkerZipEntry largeZipEntry = oldApk.getEntry(name);
43 if (largeZipEntry == null) {
44 TinkerLog.w(TAG, "large patch entry is null. path:" + name);
45 manager.getPatchReporter().onPatchTypeExtractFail(patchFile, resOutput, name, type, isUpgradePatch);
46 return false;
47 }
48 ShareResPatchInfo.LargeModeInfo largeModeInfo = resPatchInfo.largeModMap.get(name);
49 ResUtil.extractLargeModifyFile(largeZipEntry, largeModeInfo.file, largeModeInfo.crc, out);
50 totalEntryCount++;
51 }
52
53 for (String name : resPatchInfo.addRes) {
54 TinkerZipEntry addZipEntry = newApk.getEntry(name);
55 if (addZipEntry == null) {
56 TinkerLog.w(TAG, "add patch entry is null. path:" + name);
57 manager.getPatchReporter().onPatchTypeExtractFail(patchFile, resOutput, name, type, isUpgradePatch);
58 return false;
59 }
60 ResUtil.extractTinkerEntry(newApk, addZipEntry, out);
61 totalEntryCount++;
62 }
63
64 for (String name : resPatchInfo.modRes) {
65 TinkerZipEntry modZipEntry = newApk.getEntry(name);
66 if (modZipEntry == null) {
67 TinkerLog.w(TAG, "mod patch entry is null. path:" + name);
68 manager.getPatchReporter().onPatchTypeExtractFail(patchFile, resOutput, name, type, isUpgradePatch);
69 return false;
70 }
71 ResUtil.extractTinkerEntry(newApk, modZipEntry, out);
72 totalEntryCount++;
73 }
74
75 //最後對resouces.apk文件進行MD5檢查,判斷是否與resPatchInfo中的MD5一致
76 boolean result = SharePatchFileUtil.checkResourceArscMd5(resOutput, resPatchInfo.resArscMd5);
到此,resources.apk文件生成完畢。
三、資源補丁載入
合成好的資源補丁存放在/data/data/${PackageName}/tinker/res/中,名為reosuces.apk。
資源補丁的載入的操作主要放在TinkerResourceLoader.loadTinkerResources函數中,同dex的載入時機一樣,在app啟動時會被調用。直接上源碼,loadTinkerResources會調用monkeyPatchExistingResources執行實際的補丁載入。
1 public static boolean loadTinkerResources(Context context, boolean tinkerLoadVerifyFlag, String directory, Intent intentResult) {
2 if (resPatchInfo == null || resPatchInfo.resArscMd5 == null) {
3 return true;
4 }
5 String resourceString = directory + "/" + RESOURCE_PATH + "/" + RESOURCE_FILE;
6 File resourceFile = new File(resourceString);
7 long start = System.currentTimeMillis();
8
9 if (tinkerLoadVerifyFlag) {
10 if (!SharePatchFileUtil.checkResourceArscMd5(resourceFile, resPatchInfo.resArscMd5)) {
11 Log.e(TAG, "Failed to load resource file, path: " + resourceFile.getPath() + ", expect md5: " + resPatchInfo.resArscMd5);
12 ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_MD5_MISMATCH);
13 return false;
14 }
15 Log.i(TAG, "verify resource file:" + resourceFile.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
16 }
17 try {
18 TinkerResourcePatcher.monkeyPatchExistingResources(context, resourceString);
19 Log.i(TAG, "monkeyPatchExistingResources resource file:" + resourceString + ", use time: " + (System.currentTimeMillis() - start));
20 } catch (Throwable e) {
21 Log.e(TAG, "install resources failed");
22 //remove patch dex if resource is installed failed
23 try {
24 SystemClassLoaderAdder.uninstallPatchDex(context.getClassLoader());
25 } catch (Throwable throwable) {
26 Log.e(TAG, "uninstallPatchDex failed", e);
27 }
28 intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
29 ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_LOAD_EXCEPTION);
30 return false;
31 }
32
33 return true;
34 }
monkeyPatchExistingResources中實現了對外部資源的載入。
1 public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable {
2 if (externalResourceFile == null) {
3 return;
4 }
5 // Find the ActivityThread instance for the current thread
6 Class<?> activityThread = Class.forName("android.app.ActivityThread");
7 Object currentActivityThread = getActivityThread(context, activityThread);
8
9 for (Field field : new Field[]{packagesFiled, resourcePackagesFiled}) {
10 Object value = field.get(currentActivityThread);
11
12 for (Map.Entry<String, WeakReference<?>> entry
13 : ((Map<String, WeakReference<?>>) value).entrySet()) {
14 Object loadedApk = entry.getValue().get();
15 if (loadedApk == null) {
16 continue;
17 }
18 if (externalResourceFile != null) {
19 resDir.set(loadedApk, externalResourceFile);
20 }
21 }
22 }
23 // Create a new AssetManager instance and point it to the resources installed under
24 // /sdcard
25 // 通過反射調用AssetManager的addAssetPath添加資源路徑
26 if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {
27 throw new IllegalStateException("Could not create new AssetManager");
28 }
29
30 // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
31 // in L, so we do it unconditionally.
32 ensureStringBlocksMethod.invoke(newAssetManager);
33
34 for (WeakReference<Resources> wr : references) {
35 Resources resources = wr.get();
36 //pre-N
37 if (resources != null) {
38 // Set the AssetManager of the Resources instance to our brand new one
39 try {
40 assetsFiled.set(resources, newAssetManager);
41 } catch (Throwable ignore) {
42 // N
43 Object resourceImpl = resourcesImplFiled.get(resources);
44 // for Huawei HwResourcesImpl
45 Field implAssets = ShareReflectUtil.findField(resourceImpl, "mAssets");
46 implAssets.setAccessible(true);
47 implAssets.set(resourceImpl, newAssetManager);
48 }
49
50 resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
51 }
52 }
53
54 // 使用我們的測試資源文件測試是否更新成功
55 if (!checkResUpdate(context)) {
56 throw new TinkerRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL);
57 }
58 }
主要原理還是依靠反射,通過AssertManager的addAssetPath函數,加入外部的資源路徑,然後將Resources的mAssets的欄位設為前面的AssertManager,這樣在通過getResources去獲取資源的時候就可以獲取到我們外部的資源了。更多具體資源動態替換的原理,可以參考文檔。
轉載請標明本文來源:http://www.cnblogs.com/yyangblog/p/6252490.html
更多內容歡迎star作者的github:https://github.com/LaurenceYang/article
如果發現本文有什麼問題和任何建議,也隨時歡迎交流~