ReactNative的humble bundlee怎么处理有利于热更新

[React Native] 加载、维护 bundle 的正确姿势 - 简书
[React Native] 加载、维护 bundle 的正确姿势
前言:React Native 的其中一个卖点是程序可热更新,当前官方和非官方对这类实操的完整指导不多,所以在我们的项目实践中,我们做了一套自己的方案,iOS 侧已经上线运行,理论上和实践上没啥问题,这里梳理出来,一方面作为后续我们在 Android 的对齐基准,另一方面与大家共享思路方便探讨调优。
要做好 React Native 的热更新,主要需要处理好如下几个情况:
本地启动:为保证启动速度,不能全部依赖线上的 bundle,需保证还未下载到 bundle 的时候,能如常载入 bundle 并启动,所以初始化 RCTBridge 或 RCTRootView 时用的 bundleURL 得指向本地而非网络;
及时更新:为实现所用 bundle 能够及时更新,需要在合适时机拉取最新版的 bundle 存放到本地,细则如下:在 app 启动时,在 app 从后台切到前台后,以及在网络状态发生变化后,发起请求拉取最新的配置信息,根据配置信息确定是否需要下载 bundle 以及后续处理。
流量节约:为实现可控的流量节约,配置信息中包含了要使用的 bundle 信息如下:
url:bundle 文件的存放地址;
token:bundle 文件的标识字符串,每次将 bundle 文件成功保存到本地后,都同时在本地保存该值,以作下次拉取到配置时的比较依据,当配置中的 token 与本地的一致,那就无需做后续的下载和更多相关操作;
urging:更新该 bundle 的紧急程度,可选值如下:
1:有 WIFI 就下载,下好后重启 app 时启用 // 不紧急的时候用这个
2:有 WIFI 就下载,下载好后,从后台切回前台的时候启用 // 免流量,界面刷新柔和,推荐这个
3:不管有没有 WIFI 都下载,下载好后,从后台切回前台的时候启用 // 耗点流量,界面刷新柔和,次推荐这个
4:不管有没有 WIFI 都下载,下载好后,立马启用 // 杀很大,一般不用这个
当读取到上述信息后,基于配置中的 token 与本地值比较是否一致确认是否结束流程,如果不一致则以配置中的 url 发起一个请求,得到 bundle 后,保存到本地,同时把配置中的 token 也保存到本地。
版本并存:为实现多版本同时并存,提供 A/B Test、灰度发布等能力,需要做到:
约定每次发布 bundle,都以新文件形式发布,新老文件并存于服务器端,客户端根据配置情况按需拉取、使用;
实现因应不同情况输出不同配置信息的能力,有两种做法:a. 搭个动态 server,提供个接口,接受表达客户端情况的几个参数,根据这些参数的不同输出不同的配置信息,客户端读取配置信息时,都通过访问 server 上的这个接口来;b. 写个 JavaScript 文件,在其中写个函数,接受表达客户端情况的几个参数,根据这些参数的不同输出不同的配置信息,把这个 JavaScript 文件作为静态资源部署到 server,客户端读取配置信息时,都通过访问 server 拉取这个 JavaScript 文件,然后将其中的内容作为 JavaScriptCore 的 code 执行一下,然后调用其中的函数来获取配置信息;由于懒得搭动态 server,我们选择了 b 做法,关键代码如下;
// versionControl.js,
// 实际上这是个全局通用的资源版本控制配置文件,
// react-native bundle 作为其中一种资源存于其中。
// 注意:这里的代码是要放到 JavaScriptCore 中直接执行的,所以高级的 ES6 语法不能用。
var latestReactNativeBundleMetas = {
url: '/react-native/1.1.0/.ios.bundle',
token: 'a69cc86aef4bd8c0a8241'
android: {
url: '/react-native/1.0.3c.android.bundle',
var versionControlGetters = {
production: function(platform, appVer, innerId) {
// 每次在测试环境测试通过后,请将上边的 latestReactNativeBundleMetas.ios 的值复制到这里。
var meta = {
url: '/react-native/1.1.0/.ios.bundle',
token: 'a69cc86aef4bd8c0a8241'
"react-native": {
meta: meta,
test: function(platform, appVer, innerId) {
"react-native": {
// 这里的值一般维持不变,使用 latestReactNativeBundleUrls.ios 的值即可。
meta: latestReactNativeBundleMetas[platform],
function getVersionControl(envType, platform, appVer, innerId) {
return versionControlGetters[envType](platform, appVer, innerId);
- (void)getVersionControl:(void(^)(NSDictionary *data))callback
if (callback) {
NSString *url = @"/config/versionControl.js";
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
[manager GET:url
parameters:nil
success:^(AFHTTPRequestOperation * _Nonnull operation, id _Nonnull responseObject) {
NSString *code = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
JSContext *context = [JSContext new];
[context evaluateScript:code withSourceURL:[NSURL URLWithString:url]];
NSArray *args = @[[PlatFormUtil isNormalService] ? @"production" : @"test", @"ios", [PlatFormUtil AppVer], @(getCurrentInnerId())];
NSDictionary *data = [[context[@"getVersionControl"] callWithArguments:args] toDictionary];
callback([data objectForKey:@"react-native"]);
failure:^(AFHTTPRequestOperation * _Nonnull operation, NSError * _Nonnull error) {
callback(nil);
错误跟踪:为实现诸如错误上报版本跟踪、问题反馈版本跟踪等需求,需在代码中提供版本号和 Build 号信息,为此,提供一个 version 模块,考虑到 iOS、Android 并存,提供了一个公共的 version.base 模块,在 version.ios 和 version.android 中分别引用并扩展平台相关的信息;
// version.base.js
'use strict';
export default class Version {
= '1.1.0';
= '/react-native/';
platformCode = 'unknown';
// version.ios.js
'use strict';
import Version from './version.base';
export default new Version({
platformCode: 'ios'
// version.android.js // 预留,尚未启用
'use strict';
import Version from './version.base';
export default new Version({
platformCode: 'android'
鉴于 version.ios 和 version.android 的代码是固定的,所以版本升级时,主要维护的是 version.base,
发布流程自动化;
一般来说,一个发布过程应该包括如下过程:
修改 version.base 内的代码,为 version 设置新的 code 和 build 信息;
通过 react-native bundle 把 bundle 生成出来,过程中注意命名,确保不与既有文件重名,输出新文件,发布之;
将上述生成的 bundle 复制一份,覆盖到 iOS、Android 项目的内嵌 bundle 文件所在位置;
然后根据新文件的路径,调整 controlVersion.js,发布之
这么个流程,人工搞是可以,不过未免过于琐碎繁琐、易于出错,所以建议搞脚本,把这流程自动化起来。这个话题的细节比较多,后边会单独撰文详述。ReactNative热更新的实现(0.39.2)
全量热更新实现方式:
RN在打包的时候,会将我所写的js文件打包成一个叫index.android.bundle(ios的是index.ios.jsbundle)的文件,所有的js代码(包括rn源代码、第三方库、业务逻辑的代码)都在这一个文件里,启动App时会第一时间加载bundle文件,所以脚本热更新要做的事情就是替换掉这个bundle文件。安装包中的bundle文件是在asset目录下的,而asset目录我们是没有写入权限的,所以我们不能修改安装包中的bundle文件。好在RN中提供了修改读取bundle路径的方法。以android为例(ios的类似),在MainApplication类中的
privatefinal ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
RN图片资源的加载:
RN图片加载的逻辑是如果有离线脚本,那么就从该脚本所在目录里寻找图片资源,否则就从asset中读取图片资源,所谓离线脚本就是我们重服务器下载下来的新版bundle文件,如果在我们存放新版jsbundle文件的目录下没有图片资源,那么我们更新jsbundle后会发现所有的图片都看不见了。
所以我们在使用bundle命令生成bundle文件的时候也将图片资源输出出来了,在打包bundle文件的时候我们可以将所有图片资源也一并打包进zip,客户端下载zip并解压缩后,客户端可写目录下也就有了所有的图片资源,这样就即实现了脚本的热更新又实现了图片的热更新。
如果每次更新jsbundle都时候我们都要将所有都图片资源都打入zip包未免有点太任性。
生成bundle命令:react-native bundle --platform android --dev false --entry-file index.android.js \ --bundle-output ./android/app/src/main/assets/index.android.bundle \ --assets-dest ./android/app/src/main/res/
解决办法:
在更新JSbundle之前我们将asset文件下的本地已有的图片资源拷贝到我们存放新bundle文件的目录下。那么每次我们放入zip包的图片资源只需是新增的图片和需改变的图片。在实现脚本增量热更新的时候我们可以把asset中的index.android.bundle也拷贝过来,这样通过服务器下载两个版本的之间的差异补丁和本地bundle融合得到新版jsbundle,不用每次都下载整个jsbundle文件。文件比较生成补丁使用google的;
拷贝asset 下的文件 通过调用原生代码封装的方法来实现
下载zip包的流程:下载 &解压 &删除
下载使用:
transfer 和zip 依赖
通过 reactnative link 命令 导入对应的依赖;例:react-native link react-native-fs
fs不仅有删除的zip包的方法,还有获取目录等很多好用的API;
解压到的目录可以 通过fs获取 RNFS.ExternalDirectoryPath
/sdcard/android/data/com.app名/files
或者其他的有写入权限的目录,要和前面在MainApplication类中的写入的bundle路径要一致。
立即加载新的jsbundle:通过调用原生代码封装的方法来实现
在 中 引入 react-native-zip 编译工程时报错:
需要在Build Phases & Link Binary With Libraries 添加 libc.tbd 、 libz.1.2.8.tbdReact Native热更新 - goodyboy6 - 博客园
作为技术方向选型的重点,热更新/热修复是一个绕不过去的问题。本文将介绍目前的React Native(简称RN)解决方案,之后重点介绍我们即将采用的方案(包括源代码)。
React Native热更新分析
React Native热更新核心的问题是如何进行js代码的动态更新。如果不考虑更新包的大小,完全可以将整个js代码包(即编译后的jsbundle)放到服务器,由客户端来进行更新,可如果为了修复一个bug,要下载所有的js代码,接受不了...
怎么办?拆分!RN热更新最核心的一点是编译后的jsbundle是稳定的,即如果代码不变的情况下,每次编译后的jsbundle是一样的;而如果只是改动了部分代码,编译后前后差异就在这改动的代码。当然前提是RN的版本是不变的情况。
基于这点,目前公开的热更新方案一个是微软的Code Push,一个是React Native中文网中的react-native-pushy。通过这篇对这两个方案的实际分析应用来看,比较麻烦,容易出错。58同城也有对这两个方案进行了,并根据自身的业务特点实现了自己的热更新方案。
我们的方案
热更新的方案是基于jsbundle的稳定性,我们的方案也是这样。我们方案的确定包括两个主要的点:1)jsbundle差异化处理;2)jsbundle的加载逻辑理解。理解了这两点,就理解了我们的方案:即在一个迭代周期中,上线版本包括所有的js代码(基础jsbundle),随后产生的多个热更新都将和这个基础jsbundle进行差异化处理,产生多个补丁,每个新的补丁覆盖前一个补丁,即对每一个线上版本始终需要加载一个补丁。客户端下载补丁后,重新加载基础jsbundle,在加载过程中将最新下载的jsbundle合并到基础jsbundle中,实现热更新。
我们的这个方案并非一个完美方案,大家的方案都不是。原因在于,热更新中的js代码依赖线上的RN环境,依赖客户端提供的桥接接口,一旦出现当前的接口环境不支持新业务的开发,客户端版本就需要迭代。因此可以说,RN可以减少发版的次数,并不是说完全不用发版了。
这个问题清楚了,对于补丁可能很大的顾虑就可以消除了。据测试发现,修改了一个文件,补丁小于1KB。
1) 生成补丁
差异化代码的拆分和合并使用了,支持各个平台。以下示例代码为iOS,安卓/js对应的方法类似。
基本的思路是,将基础jsbundle和包含热更新jsbundle转为string,然后对string进行比较,最后将差异代码存储为文件放到server端。
这里是生成补丁的代码:
+ (NSString *)getDiffOfOldString:(NSString *)oldString newString:(NSString *)newString
DiffMatchPatch
*diffMatchPatch = [[DiffMatchPatch alloc] init];
NSMutableArray *diff = [diffMatchPatch diff_mainOfOldString:oldString andNewString:newString];
NSMutableArray *patchDiff = [diffMatchPatch patch_makeFromDiffs:diff];
NSString *patchString = [diffMatchPatch patch_toText:patchDiff];
return patchS
实际操作中建议写一个简单的生成补丁的web页面,支持上传基础jsbundle和新jsbundle,这样可以方便的获取补丁文件、测试以及上传。
2) 合并补丁
客户端检测到server端有新的补丁后进行下载,下载后通知RN重新加载基础jsbundle,在加载的过程中将新的补丁合并到基础jsbundle中,随后一并加载到内存中。合并的过程需要hack jsbundle的加载类:
@interface RCTBatchedBridge (RN)
@implementation RCTBatchedBridge (RN)
+ (void)load
NSError *error =
[self jr_swizzleMethod:@selector(executeSourceCode:) withMethod:@selector(wb_executeSourceCode:) error:&error];
if (error) {
NSLog(@"inject patch code fail: %@", error);
- (void)wb_executeSourceCode:(NSData *)sourceCode
//合并patch
if ([WBBridgeManager sharedManager].hasNewPatch) {
sourceCode = [WBPatchManger combinePatchWithSourceCode:sourceCode];
[self wb_executeSourceCode:sourceCode];
上面的sourceCode就是jsbundle加载到内存中的二进制数据,将其转为string,补丁也转为string,将这两个string进行合并(代码如下),再转换为新的二进制数据,最后将新的二进制数据加入到加载流程中,从而实现热更新。
+ (NSString *)combinePatch:(NSString *)patchString withOldString:(NSString *)oldString
DiffMatchPatch
*diffMatchPatch = [[DiffMatchPatch alloc] init];
NSError *error =
NSMutableArray *patchs = [diffMatchPatch patch_fromText:patchString error:&error];
if (error) {
NSLog(@"diff error: %@", error);
NSArray *result = [diffMatchPatch patch_apply:patchs toString:oldString];
result && result.count & 0 ? result[0] : oldS
如果合并出现
AssertMacros: hash &= (~(UniChar)0x00), Hash value has exceeded UniCharMax! file: /Users/…/Pods/Google-Diff-Match-Patch/DiffMatchPatchCFUtilities.c, line: 391
错误,请检查存\取文件的数据类型是否一致。
涉及热更新最核心的部分已经介绍完了。
回头看看我们的热更新方案,生成补丁的方法简单、补丁小、客户端热更新下载/合并逻辑简单,基本满足了我们对于热更新的需求。开篇之前,先讲一个自己开发中的一个小插曲:
今天周日,iOS版 App 周一提交,周三审核通过上架,很给力.不过,中午11:30的时候,运营就反应某个页面有一个很明显的问题,页面没法拉到底部,部分信息显示不全;那个页面是基于react-native写的,项目中本身已经有了热更新的相关机制;原因很简单,13:00左右,解决问题,发了一个补丁,测试环境自测完毕;补丁发给Leader,他可以提交到线上;出去吃饭,13:00 回来午休;14:00,Leader回到工位,补丁提交到线上;确认补丁生效,问题解决.
不要吐槽说,流程可以更优化,解决的问题更快,这涉及到另一个话题,改日有心情再聊.
如果按照没有热更新能力的解决流程,大致会是: 11:30 发现问题,13:00 解决,确认测试环境生效;生成测试包,上传 提交;人品好的话,可以走紧急审核;3~5天后,问题修复.3~5天的审核期,有人认为很长,有人早已习以为常.
小插曲而已,看看就好.我只是想让大家明白,react-native本身,可能对你的业务,确实是一个很有意义的工具,仅此而已.许多人,也是认同 react-native 的价值的,但是可能并没有在自己的项目中应用,而没有应用的原因,相对一部分原因,是很难驾驭.从我目前的实践来看,没有一个能够同时自由驾驭Native和react栈的技术人员存在,一个技术组是很难有可能把react-native应用起来的.因为前期,必须有 native 技术栈的人,去填补一些可能用react比较难实现的功能;中后期,又必须 有 react 技术栈的人,来深入地利用react本身的技术栈,来提高开发效率,比如redux的应用等.
类似的例子,我是见过一些,有死在 node 环境配置的,有卡在 native 已有应用无法集成的,当然,也有卡在不知道 如何下手使用 react-native的 的热更新能力的.
热更新,本身机制的设计,网上讨论的也是有一些,一个最简化的模型是: react-native 是基于 main.bundle 加载的; main.bundle 本身是一个文件夹;每次打开app,都去查看有无最新的 main.bundle,有就下载更新本地文件即可.blablalba.....会涉及到许多细节问题,但我相信,一个搞Native开发的人,是都可以独立解决的.
今天,要说的问题是, main.bundle 里,是包含所有的资源文件的,现在发补丁,我是整个 把最新的 完整的 main.bundle 发出去了,本身压缩后,不到 1M,和一个大图片也差不多,基本用户无感;但我现在是需要逐步把原生的部分代码,逐渐迁移到 react 来的,其中的比较基础也比较关键的一步是,把 原先Native代码中的资源文件,迁移到 main.bundle 里,使用 main.bundle 管理.
好吧,不要又吐槽我说, main.bundle 里,是不会打包未使用的图片的; 我的确是,手动把图片放到 main.bundle 里的,里面新建个 native 文件夹,用于放置 native 代码需要的一些资源,这样 native 代码,也可以部分使用 热更新的逻辑了.现在项目中,热更新的逻辑有两部分: JSPatch 和 react-native,我是通过 一个 补丁类型字段来区分的.如果为 native 和 react单独分开设计 热更新机制,想想都心累--或者说,有点太懒,有些代码,还不想去动.--别怪我话多,这是一个很有价值的策略,如果你也是基于Native来混编react-native的话,或许有种茅塞顿开或者英雄所见略同的感觉,虽然我只在iOS上试验过.
有点跑题了,再次试图回归正题.说到两个main.bundle 比较diff出一个差集,网上讨论的很多,大家搜下,勉强多少有些可以借鉴的.index.jsbundle文件本身的diff,我暂时不考虑,感觉没必要,压缩后 只有 300 k的东西,还不值得我去改热更新的实现代码,而且 jsbundle 本身的机制一直在变,比如最近的 jsbundle 都有个了一个对应的 index.jsbundle.meta 文件,原来的设计,可能是有问题的;我今天要讨论的只是,文件级别的 对比操作--简单说,就是 找到两个文件夹中不相同的文件,放到第三个文件夹中,就这样.
有人说,可以比较 md5 什么的--当然也是可以的;但是,我现在不想去知道这个原理,或者说,原理我是知道的,我不想去实现这段代码,没写过,谁知道有什么坑呢?比如,文件目录结构如何保留什么的.我想知道的是,有没有一种简单的方法,一个ctrl+c ctrl+v,就可以直接得到答案问题的方法?
当然是有的, shell 脚本嘛,什么不可以搞,如下:
rsync -aHxv --progress
--compare-dest=$(pwd)/main_old.bundle/ $(pwd)/main_new.bundle/ $(pwd)/main.bundle/
find $(pwd)/main.bundle/ -type d -empty -delete
好吧,脚本本身确实不难,只是我自己刚好需要用到,google出来,再分享给大家而已.我相信,一个深度使用 react-native 到项目中,并且比较依赖其可以热更新特性的人,是肯定有这个需求的;而且,我也知道,他们相当一部分,要么不能准确地问出问题,要么傻傻地自己去写 文件夹对比的代码...我不能说那不对,我想说的是: 编程这种东西, 多学点总是好的.此处奉上原始google参考链接,与原始答案有细微不同,懂shell的人,一眼就看的出来,不懂的,估计就算搜到答案,也有很大几率弄不出来结果.链接奉上:
还有就是,react-native 我很看好它,虽然它很有可能将来把我自己的饭碗给砸了.大势所趋,没办法;浪潮之下,要么开车,要么被压平成路,硬着头皮上吧,万一大家以后都用这个搞了呢...
阅读(...) 评论()

我要回帖

更多关于 adt bundle 无法更新 的文章

 

随机推荐