上一篇文章介绍了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文件的格式范例如下:

  1. resources_out.zip,4019114434,6148149bd5ed4e0c2f5357c6e2c577d6
  2. pattern:4
  3. resources.arsc
  4. r/*
  5. res/*
  6. assets/*
  7. modify:1
  8. r/g/ag.xml
  9. add:1
  10. 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
如果发现本文有什么问题和任何建议,也随时欢迎交流~

【原】Android热更新开源项目Tinker源码解析系列之二:资源文件热更新的更多相关文章

  1. 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新

    本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...

  2. 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新

    [原]Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代. Tinker github地址:http ...

  3. 【安卓网络请求开源框架Volley源码解析系列】定制自己的Request请求及Volley框架源码剖析

    通过前面的学习我们已经掌握了Volley的基本用法,没看过的建议大家先去阅读我的博文[安卓网络请求开源框架Volley源码解析系列]初识Volley及其基本用法.如StringRequest用来请求一 ...

  4. Python优秀开源项目Rich源码解析

    这篇文章对优秀的开源项目Rich的源码进行解析,OMG,盘他.为什么建议阅读源码,有两个原因,第一,单纯学语言很难在实践中灵活应用,通过阅读源码可以看到每个知识点的运用场景,印象会更深,以后写代码的时 ...

  5. 【安卓网络请求开源框架Volley源码解析系列】初识Volley及其基本用法

    在安卓中当涉及到网络请求时,我们通常使用的是HttpUrlConnection与HttpClient这两个类,网络请求一般是比较耗时,因此我们通常会在一个线程中来使用,但是在线程中使用这两个类时就要考 ...

  6. Android热更新开源项目Tinker集成实践总结

    前言 最近项目集成了Tinker,开始认为集成会比较简单,但是在实际操作的过程中还是遇到了一些问题,本文就会介绍在集成过程大家基本会遇到的主要问题. 考虑一:后台的选取 目前后台功能可以通过三种方式实 ...

  7. 开源项目Telegram源码 Telegram for Android Source

    背景介绍 Telegram 是一款跨平台的即时通信软件,它的客户端是自由及开放源代码软件.用户可以相互交换加密与自毁消息,发送照片.影片等所有类型文件.官方提供手机版.桌面版和网页版等多种平台客户端. ...

  8. Android源码解析系列

    转载请标明出处:一片枫叶的专栏 知乎上看了一篇非常不错的博文:有没有必要阅读Android源码 看完之后痛定思过,平时所学往往是知其然然不知其所以然,所以为了更好的深入Android体系,决定学习an ...

  9. Android进阶:五、RxJava2源码解析 2

    上一篇文章Android进阶:四.RxJava2 源码解析 1里我们讲到Rxjava2 从创建一个事件到事件被观察的过程原理,这篇文章我们讲Rxjava2中链式调用的原理.本文不讲用法,仍然需要读者熟 ...

随机推荐

  1. servlet文件下载

    创建web工程servlet,新建DownloadServlet.java package com.xmyself.servlet; import java.io.File; import java. ...

  2. 探真无阻塞加载javascript脚本技术,我们会发现很多意想不到的秘密

    下面的图片是我使用firefox和chrome浏览百度首页时候记录的http请求 下面是firefox: 下面是chrome: 在浏览百度首页前我都将浏览器的缓存全部清理掉,让这个场景最接近第一次访问 ...

  3. 利用SQLServer数据库发送邮件

    汇总篇:http://www.cnblogs.com/dunitian/p/4822808.html#tsql 这个应用案例很多,一般都是预警,比如异常连接的时候,或者数据库报错的时候.等等,,, 先 ...

  4. 网站定位之---根据IP获得区域

    记得以前做一个培训机构网站时候需要定位,那时候用的搜狐的api,不是很精准. demo:https://github.com/dunitian/LoTCodeBase/tree/master/NetC ...

  5. 学习ASP.NET Core, 怎能不了解请求处理管道[6]: 管道是如何随着WebHost的开启被构建出来的?

    注册的服务器和中间件共同构成了ASP.NET Core用于处理请求的管道, 这样一个管道是在我们启动作为应用宿主的WebHost时构建出来的.要深刻了解这个管道是如何被构建出来的,我们就必须对WebH ...

  6. [转载]强制不使用“兼容性视图”的HTML代码

    在IE8浏览器以后版本,都有一个"兼容性视图",让不少新技术无法使用.那么如何禁止浏览器自动选择"兼容性视图",强制IE以最高级别的可用模式显示内容呢?下面就介 ...

  7. arcgis api for js入门开发系列七图层控制(含源代码)

    上一篇实现了demo的地图分屏对比模块,本篇新增图层控制模块,截图如下(源代码见文章底部): 图层控制模块实现的思路如下: 1.在地图配置文件map.config.js里面配置图层目录树节点信息,作为 ...

  8. CRM 数据密钥 忘记 解决方案

    UPDATE EmailServerProfile SET IncomingPassword=nullUPDATE EmailServerProfile SET OutgoingPassword=nu ...

  9. Git使用详细教程(二)

    分支 其实在项目clone下来后就有一个分支,叫做master分支.新建分支的步骤:右键项目→Git→Repository...→Branches... master分支应该是最稳定的,开发的时候,建 ...

  10. 在开源中国(oschina)git中新建标签(tags)

    我今天提交代码到主干上面,本来想打个标签(tags)的. 因为我以前新建过标签(tags),但是我现在新建的时候不知道入库在哪了.怎么找也找不到了. 从网上找资料也没有,找客服没有人理我,看到一个交流 ...