上一篇文章介绍了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函数处理。

 // 如果是新增的资源,直接将资源文件拷贝到目标目录.
 if (oldFile == null || !oldFile.exists()) {
     if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) {
         Logger.e("found add resource: " + name + " ,but it match ignore change pattern, just ignore!");
         return false;
     }
     FileOperation.copyFileUsingStream(newFile, outputFile);
     addedSet.add(name);
     writeResLog(newFile, oldFile, TypedValue.ADD);
     return true;
 }
 ...
 // 新旧资源文件的md5一样,表示没有修改.
 if (oldMd5 != null && oldMd5.equals(newMd5)) {
     return false;
 }
 ...
 // 修改的资源文件使用dealWithModeFile函数处理.
 dealWithModeFile(name, newMd5, oldFile, newFile, outputFile);

dealWithModeFile会对文件大小进行判断,如果大于设定值(默认100Kb),采用bsdiff算法对新旧文件比较生成补丁包,从而降低补丁包的大小。

如果小于设定值,则直接将该文件加入修改列表,并直接将该文件拷贝到目标目录。

 if (checkLargeModFile(newFile)) { //大文件采用bsdiff算法
     if (!outputFile.getParentFile().exists()) {
         outputFile.getParentFile().mkdirs();
     }
     BSDiff.bsdiff(oldFile, newFile, outputFile);
     //treat it as normal modify
     // 对生成的diff文件大小和newFile进行比较,只有在达到我们的压缩效果后才使用diff文件
     if (Utils.checkBsDiffFileSize(outputFile, newFile)) {
         LargeModeInfo largeModeInfo = new LargeModeInfo();
         largeModeInfo.path = newFile;
         largeModeInfo.crc = FileOperation.getFileCrc32(newFile);
         largeModeInfo.md5 = newMd5;
         largeModifiedSet.add(name);
         largeModifiedMap.put(name, largeModeInfo);
         writeResLog(newFile, oldFile, TypedValue.LARGE_MOD);
         return true;
     }
 }
 modifiedSet.add(name); // 加入修改列表
 FileOperation.copyFileUsingStream(newFile, outputFile);
 writeResLog(newFile, oldFile, TypedValue.MOD);
 return false;

BsDiff属于二进制比较,其具体实现大家可以自行百度。

ResDiffDecoder.onAllPatchesEnd()中会加入一个测试用的资源文件,放在assets目录下,用于在加载补丁时判断其是否加在成功。

这一步同时会向res_meta.txt文件中写入资源更改的信息。

 //加入一个测试用的资源文件
 addAssetsFileForTestResource();
 ...
 //first, write resource meta first
 //use resources.arsc's base crc to identify base.apk
 String arscBaseCrc = FileOperation.getZipEntryCrc(config.mOldApkFile, TypedValue.RES_ARSC);
 String arscMd5 = FileOperation.getZipEntryMd5(extractToZip, TypedValue.RES_ARSC);
 if (arscBaseCrc == null || arscMd5 == null) {
     throw new TinkerPatchException("can't find resources.arsc's base crc or md5");
 }

 String resourceMeta = Utils.getResourceMeta(arscBaseCrc, arscMd5);
 writeMetaFile(resourceMeta);

 //pattern
 String patternMeta = TypedValue.PATTERN_TITLE;
 HashSet<String> patterns = new HashSet<>(config.mResRawPattern);
 //we will process them separate
 patterns.remove(TypedValue.RES_MANIFEST);

 writeMetaFile(patternMeta + patterns.size());
 //write pattern
 for (String item : patterns) {
     writeMetaFile(item);
 }
 //write meta file, write large modify first
 writeMetaFile(largeModifiedSet, TypedValue.LARGE_MOD);
 writeMetaFile(modifiedSet, TypedValue.MOD);
 writeMetaFile(addedSet, TypedValue.ADD);
 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对应的目录下。

 / 首先读取res_meta.txt的数据
 ShareResPatchInfo.parseAllResPatchInfo(meta, resPatchInfo);
 // 验证resPatchInfo的MD5是否合法
 if (!SharePatchFileUtil.checkIfMd5Valid(resPatchInfo.resArscMd5)) {
 ...
 // resources.apk
 File resOutput = new File(directory, ShareConstants.RES_NAME);

 // 该函数里面会对largeMod的文件进行合成,合成的算法也是采用bsdiff
 if (!checkAndExtractResourceLargeFile(context, apkPath, directory, patchFile, resPatchInfo, type, isUpgradePatch)) {

 // 基于oldapk,合并补丁后将这些资源文件写入resources.apk文件中
 while (entries.hasMoreElements()) {
     TinkerZipEntry zipEntry = entries.nextElement();
     if (zipEntry == null) {
         throw new TinkerRuntimeException("zipEntry is null when get from oldApk");
     }
     String name = zipEntry.getName();
     if (ShareResPatchInfo.checkFileInPattern(resPatchInfo.patterns, name)) {
         //won't contain in add set.
         if (!resPatchInfo.deleteRes.contains(name)
             && !resPatchInfo.modRes.contains(name)
             && !resPatchInfo.largeModRes.contains(name)
             && !name.equals(ShareConstants.RES_MANIFEST)) {
             ResUtil.extractTinkerEntry(oldApk, zipEntry, out);
             totalEntryCount++;
         }
     }
 }

 //process manifest
 TinkerZipEntry manifestZipEntry = oldApk.getEntry(ShareConstants.RES_MANIFEST);
 if (manifestZipEntry == null) {
     TinkerLog.w(TAG, "manifest patch entry is null. path:" + ShareConstants.RES_MANIFEST);
     manager.getPatchReporter().onPatchTypeExtractFail(patchFile, resOutput, ShareConstants.RES_MANIFEST, type, isUpgradePatch);
     return false;
 }
 ResUtil.extractTinkerEntry(oldApk, manifestZipEntry, out);
 totalEntryCount++;

 for (String name : resPatchInfo.largeModRes) {
     TinkerZipEntry largeZipEntry = oldApk.getEntry(name);
     if (largeZipEntry == null) {
         TinkerLog.w(TAG, "large patch entry is null. path:" + name);
         manager.getPatchReporter().onPatchTypeExtractFail(patchFile, resOutput, name, type, isUpgradePatch);
         return false;
     }
     ShareResPatchInfo.LargeModeInfo largeModeInfo = resPatchInfo.largeModMap.get(name);
     ResUtil.extractLargeModifyFile(largeZipEntry, largeModeInfo.file, largeModeInfo.crc, out);
     totalEntryCount++;
 }

 for (String name : resPatchInfo.addRes) {
     TinkerZipEntry addZipEntry = newApk.getEntry(name);
     if (addZipEntry == null) {
         TinkerLog.w(TAG, "add patch entry is null. path:" + name);
         manager.getPatchReporter().onPatchTypeExtractFail(patchFile, resOutput, name, type, isUpgradePatch);
         return false;
     }
     ResUtil.extractTinkerEntry(newApk, addZipEntry, out);
     totalEntryCount++;
 }

 for (String name : resPatchInfo.modRes) {
     TinkerZipEntry modZipEntry = newApk.getEntry(name);
     if (modZipEntry == null) {
         TinkerLog.w(TAG, "mod patch entry is null. path:" + name);
         manager.getPatchReporter().onPatchTypeExtractFail(patchFile, resOutput, name, type, isUpgradePatch);
         return false;
     }
     ResUtil.extractTinkerEntry(newApk, modZipEntry, out);
     totalEntryCount++;
 }

 //最后对resouces.apk文件进行MD5检查,判断是否与resPatchInfo中的MD5一致
 boolean result = SharePatchFileUtil.checkResourceArscMd5(resOutput, resPatchInfo.resArscMd5);

到此,resources.apk文件生成完毕。

三、资源补丁加载

合成好的资源补丁存放在/data/data/${PackageName}/tinker/res/中,名为reosuces.apk。

资源补丁的加载的操作主要放在TinkerResourceLoader.loadTinkerResources函数中,同dex的加载时机一样,在app启动时会被调用。直接上源码,loadTinkerResources会调用monkeyPatchExistingResources执行实际的补丁加载。

 public static boolean loadTinkerResources(Context context, boolean tinkerLoadVerifyFlag, String directory, Intent intentResult) {
     if (resPatchInfo == null || resPatchInfo.resArscMd5 == null) {
         return true;
     }
     String resourceString = directory + "/" + RESOURCE_PATH +  "/" + RESOURCE_FILE;
     File resourceFile = new File(resourceString);
     long start = System.currentTimeMillis();

     if (tinkerLoadVerifyFlag) {
         if (!SharePatchFileUtil.checkResourceArscMd5(resourceFile, resPatchInfo.resArscMd5)) {
             Log.e(TAG, "Failed to load resource file, path: " + resourceFile.getPath() + ", expect md5: " + resPatchInfo.resArscMd5);
             ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_MD5_MISMATCH);
             return false;
         }
         Log.i(TAG, "verify resource file:" + resourceFile.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
     }
     try {
         TinkerResourcePatcher.monkeyPatchExistingResources(context, resourceString);
         Log.i(TAG, "monkeyPatchExistingResources resource file:" + resourceString + ", use time: " + (System.currentTimeMillis() - start));
     } catch (Throwable e) {
         Log.e(TAG, "install resources failed");
         //remove patch dex if resource is installed failed
         try {
             SystemClassLoaderAdder.uninstallPatchDex(context.getClassLoader());
         } catch (Throwable throwable) {
             Log.e(TAG, "uninstallPatchDex failed", e);
         }
         intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
         ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_LOAD_EXCEPTION);
         return false;
     }

     return true;
 }

monkeyPatchExistingResources中实现了对外部资源的加载。

 public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable {
     if (externalResourceFile == null) {
         return;
     }
     // Find the ActivityThread instance for the current thread
     Class<?> activityThread = Class.forName("android.app.ActivityThread");
     Object currentActivityThread = getActivityThread(context, activityThread);

     for (Field field : new Field[]{packagesFiled, resourcePackagesFiled}) {
         Object value = field.get(currentActivityThread);

         for (Map.Entry<String, WeakReference<?>> entry
             : ((Map<String, WeakReference<?>>) value).entrySet()) {
             Object loadedApk = entry.getValue().get();
             if (loadedApk == null) {
                 continue;
             }
             if (externalResourceFile != null) {
                 resDir.set(loadedApk, externalResourceFile);
             }
         }
     }
     // Create a new AssetManager instance and point it to the resources installed under
     // /sdcard
     // 通过反射调用AssetManager的addAssetPath添加资源路径
     if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {
         throw new IllegalStateException("Could not create new AssetManager");
     }

     // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
     // in L, so we do it unconditionally.
     ensureStringBlocksMethod.invoke(newAssetManager);

     for (WeakReference<Resources> wr : references) {
         Resources resources = wr.get();
         //pre-N
         if (resources != null) {
             // Set the AssetManager of the Resources instance to our brand new one
             try {
                 assetsFiled.set(resources, newAssetManager);
             } catch (Throwable ignore) {
                 // N
                 Object resourceImpl = resourcesImplFiled.get(resources);
                 // for Huawei HwResourcesImpl
                 Field implAssets = ShareReflectUtil.findField(resourceImpl, "mAssets");
                 implAssets.setAccessible(true);
                 implAssets.set(resourceImpl, newAssetManager);
             }

             resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
         }
     }

     // 使用我们的测试资源文件测试是否更新成功
     if (!checkResUpdate(context)) {
         throw new TinkerRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL);
     }
 }

主要原理还是依靠反射,通过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. 【安卓网络请求开源框架Volley源码解析系列】初识Volley及其基本用法

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

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

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

  6. Android源码解析系列

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

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

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

  8. TiKV 源码解析系列文章(三)Prometheus(上)

    本文为 TiKV 源码解析系列的第三篇,继续为大家介绍 TiKV 依赖的周边库 rust-prometheus,本篇主要介绍基础知识以及最基本的几个指标的内部工作机制,下篇会介绍一些高级功能的实现原理 ...

  9. TiKV 源码解析系列 - Raft 的优化

    本系列文章主要面向 TiKV 社区开发者,重点介绍 TiKV 的系统架构,源码结构,流程解析.目的是使得开发者阅读之后,能对 TiKV 项目有一个初步了解,更好的参与进入 TiKV 的开发中.本文是本 ...

随机推荐

  1. 锁&amp;锁与指令原子操作的关系 &amp; cas_Queue

    锁 锁以及信号量对大部分人来说都是非常熟悉的,特别是常用的mutex.锁有很多种,互斥锁,自旋锁,读写锁,顺序锁,等等,这里就只介绍常见到的, 互斥锁 这个是最常用的,win32:CreateMute ...

  2. 剑指offer:大恒图像

    大恒图像:成立于1991年,专注于视觉部件.视觉系统及互联网医疗相关产品研发.生产和营销的高科技企业. 旗下产品信息: 1.图像采集卡 摄像机等输入的模拟图像信号经过A/D转换,或将数字摄像机的输出信 ...

  3. 我与C++的不解情缘

    我是一个老实人,我当时报考C++真的全心是为了自己自考的免考,绝不是为了什么二级证,可是,进行到一半的时候,突然获悉,C++自我们这次开始,不作为免考科目了,当时我的心里啊,那个纠结,那个痛啊,随后, ...

  4. JAVA算术运算符、关系运算符和位运算符

    算术运算符 1.java的算数运算符包括+(加).-(减).*(乘)./(除).%(取余),在运算过程中出现的隐式转换原则和C语言一样:2. 高位数据向低位数据转化要使用强制转化: 关系运算符 1.j ...

  5. Netty4.x中文教程系列(三) ChannelHandler

    Netty4.x中文教程系列(四)  ChannelHandler 上一篇文章详细解释了Hello World示例的代码.里面涉及了一些Netty框架的基础. 这篇文章用以解释ChannelHandl ...

  6. 深入理解jvm

    Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来. 概述: 对于从事C.C++程序开发的开发人员来说,在内存管理领域,他们即是拥有最高权力的皇帝 ...

  7. haskell Types 和 Typeclasses

    Algebraic Data Types 入门 在前面的章节中,我们谈了一些 Haskell 内置的类型和 Typeclass.而在本章中,我们将学习构造类型和 Typeclass 的方法. 我们已经 ...

  8. asp.net web.config的学习笔记

    原文地址:http://www.cnblogs.com/Bulid-For-NET/archive/2013/01/11/2856632.html 一直都对web.config不太清楚.这几天趁着项目 ...

  9. [转载] python利用psutil遍历进程名字和exe所在目录

    本文转载自: http://www.duanzhihe.com/1594.html http://www.jianshu.com/p/64e265f663f6 import psutil,os,tim ...

  10. 百度编辑器上传视频音频的bug

    前言:UEditor是由百度web前端研发部开发所见即所得富文本web编辑器,具有轻量,可定制,注重用户体验等特点,开源基于MIT协议,允许自由使用和修改代码,百度Ueditor 支持多种后台语言上传 ...