• 【原】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()) {
     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
    如果发现本文有什么问题和任何建议,也随时欢迎交流~

  • 相关阅读:
    Nmap笔记
    Spring AOP(一)
    Spring IOC(三)
    Spring IOC(二)
    Spring IOC(一)
    bootstrap 使用(三)
    bootstrap 使用(二)
    bootstrap 使用(一)
    js(二)
    QQ邮件
  • 原文地址:https://www.cnblogs.com/yyangblog/p/6252490.html
Copyright © 2020-2023  润新知