持续完善中...
一、拆包关键之bridge
1、bridge原理
RCTBridge是对JavaScriptCore中Bridge的封装,每个bridge都是一个独立的js环境。
RN的启动流程可以简单概括为:
- Native编译并启动
- 创建js虚拟机环境
- 创建 bridge,拥有独立的context js运行环境,并负责原生和js线程的通信(通过不同bridge加载的js代码,可以存在相同的全局变量,不会冲突)
- 通过 bridge 获取js线程来解析js代码(可以是远程包和离线包)
- 运行js代码,并根据参数创建 RootView
bridge在RN中起到承上启下的作用,在做RN拆包的时候是重点考虑的对象。目前RN拆包针对brdige有两种主流方案,分别是单bridge和多bridge。
2、单bridge和多bridge的选择
- 单bridge:react-native-multibundler
优势 | 劣势 |
---|---|
不用管理bridge的缓存和复用问题 | 不重启APP的情况下想要更新bundle需要做更多的配置,比较繁琐,且更新bundle并不会清除bridge中的旧bundle,存在少量内存浪费 |
占用内存更少 | 由于不同模块都是运行在同一个bridge环境中,如果存在相同的全局变量会造成代码污染 |
- 多bridge:携程CRN
优势 | 劣势 |
---|---|
不同模块之间使用了bridge隔离,不用担心全局变量污染的问题 | 由于bridge很占用内存,所以需要手动维护bridge的缓存和复用问题,避免APP内存溢出(CRN维护了5个上限的bridge) |
不重启APP的情况下更新bundle很方便,只需要重新指定路径加载或者执行reload |
占用内存多 |
二、基础包和业务包的拆分
1、metro 介绍和打包流程
react-native metro 分析
metro是一种支持ReactNative的打包工具,我们现在也是基于他来进行拆包的,metro打包流程分为以下几个步骤:
- Resolution:Metro需要从入口点构建所需的所有模块的图,要从另一个文件中找到所需的文件,需要使用Metro解析器。在现实开发中,这个阶段与Transformation阶段是并行的。
- Transformation:所有模块都要经过Transformation阶段,Transformation负责将模块转换成目标平台可以理解的格式(如React Naitve)。模块的转换是基于拥有的核心数量来进行的。
- Serialization:所有模块一经转换就会被序列化,Serialization会组合这些模块来生成一个或多个包,包就是将模块组合成一个JavaScript文件的包,序列化的时候提供了一些列的方法让开发者自定义一些内容,比如模块id,模块过滤等。
观察一下原生Metro代码的node_modules/metro/src/lib/createModuleIdFactory.js文件,代码为:
function createModuleIdFactory() {
const fileToIdMap = new Map();
let nextId = 0;
return path => {
let id = fileToIdMap.get(path);
if (typeof id !== "number") {
id = nextId++;
fileToIdMap.set(path, id);
}
return id;
};
}
module.exports = createModuleIdFactory;
逻辑比较简单,如果查到map里没有记录这个模块则id自增,然后将该模块记录到map中,所以从这里可以看出,官方代码生成moduleId的规则就是自增,所以这里要替换成我们自己的配置逻辑,我们要做拆包就需要保证这个id不能重复,但是这个id只是在打包时生成,如果我们单独打业务包,基础包,这个id的连续性就会丢失,所以对于id的处理,我们还是可以参考上述开源项目,每个包有十万位间隔空间的划分,基础包从0开始自增,业务A从1000000开始自增,又或者通过每个模块自己的路径或者uuid等去分配,来避免碰撞,但是字符串会增大包的体积,这里不推荐这种做法。所以总结起来js端拆包还是比较容易的,这里就不再赘述
2、Plain Bundle 分析
通过react-native bundle --platform android --dev false --entry-file index.common.js --bundle-output {输出bundle的路径} --assets-dest {资源路径} --config {自定义打包配置} --minify false 打出基础包(minify设为false便于查看源码)
function (global) {
"use strict";
global.__r = metroRequire;
global.__d = define;
global.__c = clear;
global.__registerSegment = registerSegment;
var modules = clear();
var EMPTY = {};
var _ref = {},
hasOwnProperty = _ref.hasOwnProperty;
function clear() {
modules = Object.create(null);
return modules;
}
function define(factory, moduleId, dependencyMap) {
if (modules[moduleId] != null) {
return;
}
modules[moduleId] = {
dependencyMap: dependencyMap,
factory: factory,
hasError: false,
importedAll: EMPTY,
importedDefault: EMPTY,
isInitialized: false,
publicModule: {
exports: {}
}
};
}
function metroRequire(moduleId) {
var moduleIdReallyIsNumber = moduleId;
var module = modules[moduleIdReallyIsNumber];
return module && module.isInitialized ? module.publicModule.exports : guardedLoadModule(moduleIdReallyIsNumber, module);
}
这里主要看__r,__d两个变量,赋值了两个方法metroRequire,define,具体逻辑也很简单,define相当于在表中注册,require相当于在表中查找,js代码中的import,export编译后就就转换成了__d与__r
三、拆包的后遗症
1、按序加载基础包和业务包
将RN的js业务拆出了公共模块之后,在bridge加载bundle的时候需要优先加载common包。这里需要考虑两个问题:
- RCTBridge需要叠加加载bundle
由于RCTBridge并没有提供多次加载bunlde的方法,但是其内部又一个私有方法实现了该功能(- (void)executeSourceCode:(NSData *)sourceCode sync:(BOOL)sync;
),在iOS中我们可以通过Category的方式将该方法暴露出来 - bundle加载完成获取回调
我们必须要在common bunlde加载完成之后再去加载业务模块,所以我们需要获取到bundle加载完成的回调。然而RCTBridge并没有提供回调入口,但是其有一个loading属性,我们可以使用一个do while
循环阻塞线程,直到loading为false代码再往下走
如果是多bridge方案,每个bridge都得先加载common包,再加载具体业务包,这样会很浪费内存。
2、热更新改造
单bridge热更新
单bridge的叠加加载问题已经解决了,但是叠加加载并不会覆盖已经加载过的bundle包,如果在不重启APP的情况下,单bridge将无法实现热更新。解决办法是在打更新包的时候,得更新需要热更的bundle包的模块ID,具体可参考:react-native实现不重启App的情况下更新分包。
第二个问题是热更之后资源路径发生变化。需要制定热更之后的bundle从沙盒加载资源,否则会出现资源文件找不到的问题。多bridge热更新
多bridge方案进行热更时,无需考虑单bridge reload影响全局的问题,只需要reload当前需要更新的bridge就行,如果模块划分比较细,这样做通常更有优势。
如果使用静默升级,那么可以在下载完bundle包之后先不做替换或者reload,而是等到下一次进入APP的时候从新的路径加载bundle,这样做可以使用户进行无感知的更新。
3、混合开发的路由方案
纯RN路由
适用于纯RN,使用react-navigation即可,仅需使用AppRegistry.registerComponent
注册一个根组件,只会存在一个VC或activity,所有的路由跳转其实都是在同一个VC或activity内跳转。如果后期要扩展混合路由,纯RN改造会比较大纯Native路由
每个RN页面,都使用AppRegistry.registerComponent
单独注册,然后在Native端利用注册的组件创建的单独的RootView,并最终创建单独的VC承载。由于都使用Native路由,所以可以很方便的进行Native和RN路由的统一,管理一套路由表即可。但是如果项目中需要引入其他团队开发的RN bundle包,其他团队如果使用的是纯RN路由,那么这个时候就不兼容了,所以纯Native路由方式不太适合需要引入其他团队开发的bundle的场景混合路由
混合路由指的是有一部分Native路由,有一部分RN路由,携程CRN目前走的就是混合路由路线。如果有些模块需要在其他App内复用,建议采用携程的模式,他们对路由进行了优化(没开源
),管理起来应该会方便些。
4、路由表的调整
拆包之后路由表怎么维护呢?由于拆分成了多个bundle,路由表散落在了多个bundle中,不同bundle之间如何跳转。如果路由名产生了冲突,就会导致跳转异常和错乱,所以这里就需要给每个路由加上一个所属bundle标识。
5、多bundle的debug
各种操作拆完包后,突然有个问题,怎么调试呢?起初还想着怎么让Native在初始化时直接加载全部bundle。但后来突然想明白,拆包的本质就是通过设置多个入口文件将代码给分割,那调试的时候我们直接将入口文件都在放在index.js里不就行了么。这样就实现了跟RN单包一样的调试。这个操作需要再js端提供一个引用所有模块入口的文件,然后Native端设置debug标识来做bundle加载区分。
多bundle的情况下还尝试过区分端口来独立启动和调试不同模块,暂时不调试的模块就加载本地一个提前打包好的bundle。但是实践过程发现当开启 Remote JS Debug
的时候,所有的bridge都会重新调用reload,那么这会导致什么问题吗?
这里要说下Remote JS Debug
的原理和command + R
和 command + D + Reload
的区别。
这是command + R 的源代码
#if RCT_DEV
RCTExecuteOnMainQueue(^{
RCTRegisterReloadCommandListener(self);
});
#endif
void RCTRegisterReloadCommandListener(id listener)
{
RCTAssertMainQueue(); // because registerKeyCommandWithInput: must be called on the main thread
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
listeners = [NSHashTable weakObjectsHashTable];
[[RCTKeyCommands sharedInstance] registerKeyCommandWithInput:@"r"
modifierFlags:UIKeyModifierCommand
action:
^(__unused UIKeyCommand *command) {
RCTTriggerReloadCommandListeners();
}];
});
[listeners addObject:listener];
}
void RCTTriggerReloadCommandListeners(void)
{
RCTAssertMainQueue();
// Copy to protect against mutation-during-enumeration.
// If listeners hasn't been initialized yet we get nil, which works just fine.
NSArray> *copiedListeners = [listeners allObjects];
for (id l in copiedListeners) {
[l didReceiveReloadCommand];
}
}
开发环境会监听command + R
键盘事件,一旦监听到指令就会遍历所有注册过得bridge,并执行其didReceiveReloadCommand
方法,最后调用reload方法。所以如果当前初始化了多个bridge,就会将注册的bridge全都reload一遍,即使加载的是离线包的bridge,也会触发一个8081端口的bridge,由于此时可能没有开启8081端口服务,那么屏幕就会爆红。
所以在多bridge方案中,如果要方便调试,要么在底层做改造,要么区分开发和正式场景,在开发场景使用单bridge方案。但这又造成了开发和正式环境的不一致问题,可能会出现开发环境正常,正式环境报错的问题,很难定位。
参考文章
- 一种强大、可靠的React Native拆包以及热更新方案,基于CodePush,Metro
- react-native metro 分析
- 分析一下metro下两种Bundle格式
- CRN的一些研究
- react-native实现不重启App的情况下更新分包