【原】Android热更新开源项目Tinker源码解析系列之二:资源文件热更新
上一篇文章介绍了Dex文件的热更新流程,本文将会分析Tinker中对资源文件的热更新流程。
同Dex,资源文件的热更新同样包括三个部分:资源补丁生成,资源补丁合成及资源补丁加载。
本系列将从以下三个方面对Tinker进行源码解析:
- Android热更新开源项目Tinker源码解析系列之一:Dex热更新
- Android热更新开源项目Tinker源码解析系列之二:资源热更新
- 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源码解析系列之二:资源文件热更新的更多相关文章
- 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新
本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...
- 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新
[原]Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代. Tinker github地址:http ...
- 【安卓网络请求开源框架Volley源码解析系列】定制自己的Request请求及Volley框架源码剖析
通过前面的学习我们已经掌握了Volley的基本用法,没看过的建议大家先去阅读我的博文[安卓网络请求开源框架Volley源码解析系列]初识Volley及其基本用法.如StringRequest用来请求一 ...
- Python优秀开源项目Rich源码解析
这篇文章对优秀的开源项目Rich的源码进行解析,OMG,盘他.为什么建议阅读源码,有两个原因,第一,单纯学语言很难在实践中灵活应用,通过阅读源码可以看到每个知识点的运用场景,印象会更深,以后写代码的时 ...
- 【安卓网络请求开源框架Volley源码解析系列】初识Volley及其基本用法
在安卓中当涉及到网络请求时,我们通常使用的是HttpUrlConnection与HttpClient这两个类,网络请求一般是比较耗时,因此我们通常会在一个线程中来使用,但是在线程中使用这两个类时就要考 ...
- Android热更新开源项目Tinker集成实践总结
前言 最近项目集成了Tinker,开始认为集成会比较简单,但是在实际操作的过程中还是遇到了一些问题,本文就会介绍在集成过程大家基本会遇到的主要问题. 考虑一:后台的选取 目前后台功能可以通过三种方式实 ...
- 开源项目Telegram源码 Telegram for Android Source
背景介绍 Telegram 是一款跨平台的即时通信软件,它的客户端是自由及开放源代码软件.用户可以相互交换加密与自毁消息,发送照片.影片等所有类型文件.官方提供手机版.桌面版和网页版等多种平台客户端. ...
- Android源码解析系列
转载请标明出处:一片枫叶的专栏 知乎上看了一篇非常不错的博文:有没有必要阅读Android源码 看完之后痛定思过,平时所学往往是知其然然不知其所以然,所以为了更好的深入Android体系,决定学习an ...
- Android进阶:五、RxJava2源码解析 2
上一篇文章Android进阶:四.RxJava2 源码解析 1里我们讲到Rxjava2 从创建一个事件到事件被观察的过程原理,这篇文章我们讲Rxjava2中链式调用的原理.本文不讲用法,仍然需要读者熟 ...
随机推荐
- 如何一步一步用DDD设计一个电商网站(七)—— 实现售价上下文
阅读目录 前言 明确业务细节 建模 实现 结语 一.前言 上一篇我们已经确立的购买上下文和销售上下文的交互方式,传送门在此:http://www.cnblogs.com/Zachary-Fan/p/D ...
- 一个免费的、跨平台的、开源音频编辑器Audacity
Audacity 是一个免费的开源程序,用于编辑音频录制.它可在多个平台(windows/linux)上运行.Audacity 基于 GUI,是一个具有多种选项的强大程序.它支持您录制各种类型的声音. ...
- ABP文档 - 嵌入的资源文件
文档目录 本节内容: 简介 创建嵌入的文件 暴露嵌入的文件 使用嵌入的文件 简介 一个web应用里,客户端包含javascript,css,xml等文件,这此文件被添加到一个web项目后,发布成独立的 ...
- Git Bash的一些命令和配置
查看git版本号: git --version 如果是第一次使用Git,你需要设置署名和邮箱: $ git config --global user.name "用户名" $ gi ...
- Android 剪贴板详解
版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/Clipboard 如本文有助于你理解 Android 剪贴板,不妨给我一个 Star.对于码农而言, ...
- 高频交易算法研发心得--MACD指标算法及应用
凤鸾宝帐景非常,尽是泥金巧样妆. 曲曲远山飞翠色:翩翩舞袖映霞裳. 梨花带雨争娇艳:芍药笼烟骋媚妆. 但得妖娆能举动,取回长乐侍君王. [摘自<封神演义>纣王在女娲宫上香时题的诗] 一首定 ...
- 代码的坏味道(22)——不完美的库类(Incomplete Library Class)
坏味道--不完美的库类(Incomplete Library Class) 特征 当一个类库已经不能满足实际需要时,你就不得不改变这个库(如果这个库是只读的,那就没辙了). 问题原因 许多编程技术都建 ...
- 记录一次bug解决过程:数据迁移
一 总结 不擅长语言表达,勤于沟通,多锻炼 调试MyBatis中SQL语法:foreach 问题:缺少关键字VALUES.很遗憾:它的错误报的让人找不着北. 二 BUG描述:MyBatis中批量插入数 ...
- ABAP单元测试最佳实践
本文包含了我在开发项目中经历过的实用的ABAP单元测试指导方针.我把它们安排成为问答的风格,欢迎任何人添加更多的Q&A's,以完成这个列表. 在我的项目中,只使用传统的ABAP report. ...
- maven打包插件:appassembler
1.打包成bat 打包命令:mvn clean package appassembler:assemble <plugin> <groupId>org.codehaus.moj ...