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

来源:http://www.cnblogs.com/yyangblog/archive/2017/01/06/6252490.html
-Advertisement-
Play Games

上一篇文章介紹了Dex文件的熱更新流程,本文將會分析Tinker中對資源文件的熱更新流程。 同Dex,資源文件的熱更新同樣包括三個部分:資源補丁生成,資源補丁合成及資源補丁載入。 本系列將從以下三個方面對Tinker進行源碼解析: 轉載請標明本文來源:http://www.cnblogs.com/y ...


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

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

 

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

  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
如果發現本文有什麼問題和任何建議,也隨時歡迎交流~

 

一、資源補丁生成

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
如果發現本文有什麼問題和任何建議,也隨時歡迎交流~


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 作者:Antonio Leiva 時間:Jan 5, 2017 原文鏈接:https://antonioleiva.com/lambdas-kotlin/ 由於Lambda表達式允許更簡單的方式建模式函數,所以它是Kotlin和任何其他現代開發語言的最強工具之一。 在Java6中,我們僅能下麵方法這 ...
  • 看完這篇你學到什麼: 熟悉gradle的構建配置 熟悉代碼構建環境的目錄結構,你知道的不僅僅是只有src/main 開發、生成環境等等環境可以任意切換打包 多渠道打包 APK輸出文件配置 需求 一般我們開發的環境分為:debug 和 release,但是你想再分內測1環境、內測2環境等等怎麼辦呢? ...
  • UI層複習筆記 在main文件中,UIApplicationMain函數一共做了三件事 根據第三個參數創建了一個應用程式對象 預設寫nil,即創建的是UIApplication類型的對象,此對象看成是整個應用程式的一個抽象,負責存儲應用程式的狀態。 根據第四個參數創建了一個應用程式代理類對象 所謂代 ...
  • 昨天 提交App Store 的時候被拒了 We discovered one or more bugs in your app when reviewed on iPhone running iOS 10.2 on Wi-Fi connected to an IPv6 network. Speci ...
  • 我們都知道,開發一個app很大程度依賴服務端:服務端提供介面數據,然後我們展示;另外,開發一個app,還需要美工協助切圖。沒了介面,沒了美工,app似乎只能做成單機版或工具類app,真的是這樣的嗎?先來展示下我的個人app,沒有服務端,沒有美工完成的,換言之,我幹了所有人的活: 這個app叫“微言” ...
  • 1.如果可以重置模擬器 首先試試重置模擬器 2.如果不能重置,可以選擇使用如下命令殺死模擬器服務: killall -9 com.apple.CoreSimulator.CoreSimulatorService //殺死模擬器服務 3.使用如下命令刪除模擬器目錄下是所有文件 rm -rf ~/Lib ...
  • 一、使用Raw文件夾下的資料庫文件 在使用GreenDao框架時,資料庫和數據表都是根據生成的框架代碼來自動創建的,從生成的DaoMaster中的OpenHelper類可以看出: 對應的createAllTables函數代碼: 再接著往下看: 從以上的代碼可以看出GreenDao在第一次使用的時候會 ...
  • 今天在看到App Store 上架過程中,蘋果公司反饋的拒絕原因發現了這麼一個問題: Legal - 5.1.5 Your app uses background location services but does not clarify the purpose of its use in the ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...