【原】Android熱更新開源項目Tinker源碼解析系列之二:資源文件熱更新

上一篇文章介紹了Dex文件的熱更新流程,本文將會分析Tinker中對資源文件的熱更新流程。html

同Dex,資源文件的熱更新一樣包括三個部分:資源補丁生成,資源補丁合成及資源補丁加載。android

 

本系列將從如下三個方面對Tinker進行源碼解析:git

  1. Android熱更新開源項目Tinker源碼解析系列之一:Dex熱更新
  2. Android熱更新開源項目Tinker源碼解析系列之二:資源熱更新
  3. Android熱更新開源項目Tinker源碼解析系類之三:so熱更新

 

轉載請標明本文來源:http://www.cnblogs.com/yyangblog/p/6252490.html
更多內容歡迎star做者的github:https://github.com/LaurenceYang/article
若是發現本文有什麼問題和任何建議,也隨時歡迎交流~github

 

1、資源補丁生成

ResDiffDecoder.patch(File oldFile, File newFile)主要負責資源文件補丁的生成。算法

若是是新增的資源,直接將資源文件拷貝到目標目錄。app

若是是修改的資源文件則使用dealWithModeFile函數處理。ide

 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屬於二進制比較,其具體實現你們能夠自行百度。this

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

到此,資源文件的補丁打包流程結束。

 

2、補丁下發成功後資源補丁的合成

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文件生成完畢。

 

3、資源補丁加載

合成好的資源補丁存放在/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
若是發現本文有什麼問題和任何建議,也隨時歡迎交流~

相關文章
相關標籤/搜索