原Android热更新开源项目Tinker源码解析系列之二:资源文件热更新

千金一刻莫空度,老大无成空自伤。这篇文章主要讲述原Android热更新开源项目Tinker源码解析系列之二:资源文件热更新相关的知识,希望能为你提供帮助。
上一篇文章介绍了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()) { 3if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) { 4Logger.e("found add resource: " + name + " ,but it match ignore change pattern, just ignore!"); 5return false; 6} 7FileOperation.copyFileUsingStream(newFile, outputFile); 8addedSet.add(name); 9writeResLog(newFile, oldFile, TypedValue.ADD); 10return true; 11 } 12 ... 13 // 新旧资源文件的md5一样,表示没有修改. 14 if (oldMd5 != null & & oldMd5.equals(newMd5)) { 15return false; 16 } 17 ... 18 // 修改的资源文件使用dealWithModeFile函数处理. 19 dealWithModeFile(name, newMd5, oldFile, newFile, outputFile);

dealWithModeFile会对文件大小进行判断,如果大于设定值(默认100Kb),采用bsdiff算法对新旧文件比较生成补丁包,从而降低补丁包的大小。
如果小于设定值,则直接将该文件加入修改列表,并直接将该文件拷贝到目标目录。
1 if (checkLargeModFile(newFile)) { //大文件采用bsdiff算法 2if (!outputFile.getParentFile().exists()) { 3outputFile.getParentFile().mkdirs(); 4} 5BSDiff.bsdiff(oldFile, newFile, outputFile); 6//treat it as normal modify 7// 对生成的diff文件大小和newFile进行比较,只有在达到我们的压缩效果后才使用diff文件 8if (Utils.checkBsDiffFileSize(outputFile, newFile)) { 9LargeModeInfo largeModeInfo = new LargeModeInfo(); 10largeModeInfo.path = newFile; 11largeModeInfo.crc = FileOperation.getFileCrc32(newFile); 12largeModeInfo.md5 = newMd5; 13largeModifiedSet.add(name); 14largeModifiedMap.put(name, largeModeInfo); 15writeResLog(newFile, oldFile, TypedValue.LARGE_MOD); 16return 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) { 9throw 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) { 24writeMetaFile(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()) { 14TinkerZipEntry zipEntry = entries.nextElement(); 15if (zipEntry == null) { 16throw new TinkerRuntimeException("zipEntry is null when get from oldApk"); 17} 18String name = zipEntry.getName(); 19if (ShareResPatchInfo.checkFileInPattern(resPatchInfo.patterns, name)) { 20//won\'t contain in add set. 21if (!resPatchInfo.deleteRes.contains(name) 22& & !resPatchInfo.modRes.contains(name) 23& & !resPatchInfo.largeModRes.contains(name) 24& & !name.equals(ShareConstants.RES_MANIFEST)) { 25ResUtil.extractTinkerEntry(oldApk, zipEntry, out); 26totalEntryCount++; 27} 28} 29 } 30 31 //process manifest 32 TinkerZipEntry manifestZipEntry = oldApk.getEntry(ShareConstants.RES_MANIFEST); 33 if (manifestZipEntry == null) { 34TinkerLog.w(TAG, "manifest patch entry is null. path:" + ShareConstants.RES_MANIFEST); 35manager.getPatchReporter().onPatchTypeExtractFail(patchFile, resOutput, ShareConstants.RES_MANIFEST, type, isUpgradePatch); 36return false; 37 } 38 ResUtil.extractTinkerEntry(oldApk, manifestZipEntry, out); 39 totalEntryCount++; 40 41 for (String name : resPatchInfo.largeModRes) { 42TinkerZipEntry largeZipEntry = oldApk.getEntry(name); 43if (largeZipEntry == null) { 44TinkerLog.w(TAG, "large patch entry is null. path:" + name); 45manager.getPatchReporter().onPatchTypeExtractFail(patchFile, resOutput, name, type, isUpgradePatch); 46return false; 47} 48ShareResPatchInfo.LargeModeInfo largeModeInfo = resPatchInfo.largeModMap.get(name); 49ResUtil.extractLargeModifyFile(largeZipEntry, largeModeInfo.file, largeModeInfo.crc, out); 50totalEntryCount++; 51 } 52 53 for (String name : resPatchInfo.addRes) { 54TinkerZipEntry addZipEntry = newApk.getEntry(name); 55if (addZipEntry == null) { 56TinkerLog.w(TAG, "add patch entry is null. path:" + name); 57manager.getPatchReporter().onPatchTypeExtractFail(patchFile, resOutput, name, type, isUpgradePatch); 58return false; 59} 60ResUtil.extractTinkerEntry(newApk, addZipEntry, out); 61totalEntryCount++; 62 } 63 64 for (String name : resPatchInfo.modRes) { 65TinkerZipEntry modZipEntry = newApk.getEntry(name); 66if (modZipEntry == null) { 67TinkerLog.w(TAG, "mod patch entry is null. path:" + name); 68manager.getPatchReporter().onPatchTypeExtractFail(patchFile, resOutput, name, type, isUpgradePatch); 69return false; 70} 71ResUtil.extractTinkerEntry(newApk, modZipEntry, out); 72totalEntryCount++; 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) { 2if (resPatchInfo == null || resPatchInfo.resArscMd5 == null) { 3return true; 4} 5String resourceString = directory + "/" + RESOURCE_PATH +"/" + RESOURCE_FILE; 6File resourceFile = new File(resourceString); 7long start = System.currentTimeMillis(); 8 9if (tinkerLoadVerifyFlag) { 10if (!SharePatchFileUtil.checkResourceArscMd5(resourceFile, resPatchInfo.resArscMd5)) { 11Log.e(TAG, "Failed to load resource file, path: " + resourceFile.getPath() + ", expect md5: " + resPatchInfo.resArscMd5); 12ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_MD5_MISMATCH); 13return false; 14} 15Log.i(TAG, "verify resource file:" + resourceFile.getPath() + " md5, use time: " + (System.currentTimeMillis() - start)); 16} 17try { 18TinkerResourcePatcher.monkeyPatchExistingResources(context, resourceString); 19Log.i(TAG, "monkeyPatchExistingResources resource file:" + resourceString + ", use time: " + (System.currentTimeMillis() - start)); 20} catch (Throwable e) { 21Log.e(TAG, "install resources failed"); 22//remove patch dex if resource is installed failed 23try { 24SystemClassLoaderAdder.uninstallPatchDex(context.getClassLoader()); 25} catch (Throwable throwable) { 26Log.e(TAG, "uninstallPatchDex failed", e); 27} 28intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e); 29ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_LOAD_EXCEPTION); 30return false; 31} 32 33return true; 34 }

【原Android热更新开源项目Tinker源码解析系列之二:资源文件热更新】monkeyPatchExistingResources中实现了对外部资源的加载。
1 public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable { 2if (externalResourceFile == null) { 3return; 4} 5// Find the ActivityThread instance for the current thread 6Class< ?> activityThread = Class.forName("android.app.ActivityThread"); 7Object currentActivityThread = getActivityThread(context, activityThread); 8 9for (Field field : new Field[]{packagesFiled, resourcePackagesFiled}) { 10Object value = https://www.songbingjia.com/android/field.get(currentActivityThread); 11 12for (Map.Entry< String, WeakReference< ?> > entry 13: ((Map< String, WeakReference< ?> > ) value).entrySet()) { 14Object loadedApk = entry.getValue().get(); 15if (loadedApk == null) { 16continue; 17} 18if (externalResourceFile != null) { 19resDir.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添加资源路径 26if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) { 27throw 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. 32ensureStringBlocksMethod.invoke(newAssetManager); 33 34for (WeakReference< Resources> wr : references) { 35Resources resources = wr.get(); 36//pre-N 37if (resources != null) { 38// Set the AssetManager of the Resources instance to our brand new one 39try { 40assetsFiled.set(resources, newAssetManager); 41} catch (Throwable ignore) { 42// N 43Object resourceImpl = resourcesImplFiled.get(resources); 44// for Huawei HwResourcesImpl 45Field implAssets = ShareReflectUtil.findField(resourceImpl, "mAssets"); 46implAssets.setAccessible(true); 47implAssets.set(resourceImpl, newAssetManager); 48} 49 50resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics()); 51} 52} 53 54// 使用我们的测试资源文件测试是否更新成功 55if (!checkResUpdate(context)) { 56throw 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
如果发现本文有什么问题和任何建议,也随时欢迎交流~

    推荐阅读