广发手机证劵App计划在6.4版本中支持ReactNative。首页,必答等多个业务模块计划用RN替换H5实现。 因上述业务均由手机证劵外的其他小团队各自负责,故为了后续控制apk的包大小,以及提升RN模块首屏显示速度。我们预先做了JsBundle的拆包优化,后续再让各个业务模块按规范陆续接入。
2. 遇到的问题:
2.1 包大小问题
手机证劵首页模块接入RN以后,包大小从原来的17mb增加到了26mb。其中,首页模块的JsBundle文件大小为860k(860k中RN基础库占530k)。因此,随着业务的增多,每一个RN业务都会重复占用530k。
2.2 RN首屏显示问题
首页模块RN化后的性能并没有预期的快, 反而,首屏展示的实测效果远比离线状态下的h5慢。

如上图所示 JS Init+ Require的时间占RN应用启动时间的50%。 且JS Init+ Require所占的时间比例会随着JsBundle文件的增大而增大。因此,如何拆分JsBundle文件,动态加载业务Js是优化RN首屏展示性能的核心。
3.拆包方案简介
JsBunlde拆包的思路基本大同小异,本文的拆包思路同样是将各个业务的JsBundle拆分成Common.js+Business.js。Common.js在App启动时,用一个全局的ReactInstanceManager 预加载。进入某个业务时,再动态加载其Business.js文件。
本文的具体实现思路为:首先在Common.js中注册一个gfmobileRN组件。然后在RootView.startRNApp时传入需要显示的模块id。 gfmobileRN根据startModuleId 动态加载业务Business.js文件并显示该模块;
Common.js设计如下图:
Native启动时如下图:
4.拆包方案的具体实现:
拆包的工作主要分为打包和客户端加载。本节我们将分别从js和android native两块讲述拆包的具体实现细节。
4.1 打包
打包主要通过改RN的原有的packager打包模块实现。
具体步骤如下:
一.以Common.js文件为入口打出一个common.android.bundle基础包,并在生成基础包时将打包过程中的ModuleId的关联关系记录到common.json文件中。
具体实现思路为:在jsbundle 生成时,遍历其所有的module并将moduleid记录到本地文件中。
具体步骤如下:
1.在local-cli/bundle/buildCommandLineArgs.js 新增manifest-output参数。
2.在packager/src/Bundler/Bundle.js文件中新增获取所有Modules信息的接口。
3.在local-cli/bundle/output/bundle.js
中遍历module并写文件。
具体效果如下:

二.以业务的index.js文件为入口, 用步骤一生成的common.json为基础过滤common.js中存在的module后生成business.js文件。 并为每个不同的业务分配一个业务模块起始的startId 以此进行业务隔离。
具体步骤如下:
1.在local-cli/bundle/buildCommandLineArgs.js 新增manifest-file和start-id两个参数
manifest-file 记录了 业务bundle需要过滤的moduleId
start-id 用于设置业务bundle 起始module的id; 并依次递增
2.在packager/src/Bundler/Bundle.js
中读入common.json 相关的信息和module的startId
3.在createModuleId时,common.json中不存在的 moduleid 全部+startId。 common.json中存在的module保持不变。以保持其require时正确的依赖关系。

4.过滤common.json中存在的 module
具体效果如下:
4.2 business.js文件的加载
在做business.js加载前,先了解RN的require.js的逻辑。

以上文生成的10000.js的业务bundle为例。 在进入RN模块时,Native将传递 startModuleId为10000的参数给gfmobileRN 。 因此在 require(10000)时,RN首先会判断该module是否已经存在。如果不存在则调用nativeRequire让native加载该模块。
nativeRequire则是利用m_unbundle对象去加载指定的js模块。而上文示例的10000.js业务包是通过
react-native bundle命令生成的。 因此再来了解bundle 和unbundle在加载过程中有什么区别。

如上图代码所示,RN判断Js模块是否是unbundle的打包方式时,
只是判断 asset的js-modules目录下是否有一个固定字符的UNBUNDLE文件。
因此本文尝试在asset目录下加上了UNBUNDLE文件。
至此业务的jsbundle文件直接加载成功。
如果想给业务的business.js文件做预加载,思路如下:
方案一: 给JSC的 JSEvaluateScript 封装一整套上层调用的接口,让其去加载特定的js文件。
方案二:common.js加载时设置一个预加载业务js的监听。
5.热更新支持
RN的热更新本质为动态替换本地的jsbundle文件。 而之前本文使用的是unbundle的加载方式来实现业务business.js文件加载。但是unbunde只支持加载asset目录下的文件。因此需要扩展其能力,让其支持加载指定目录下的文件,具体实现如下:
然后在Builder
ReactInstanceManager时,设置加载路径。
实现后的性能测试如下:在小米5s上读一个334k的jsBundle文件耗时大约为12ms。 如果使用mmap方式读文件应该更快。
6.总结
至此基本的Bundle拆分就做完了, 但仍然有很多优化点可以继续深挖:
1. 业务的JsBundle文件逐步扩大以后,require加载新模块时耗时太长。 因此如何在业务无感知的情况下,将business.js拆分成多个更小的jsbundle文件打包是非常重要的。
2. 目前手机证劵采用了单引擎多容器方式集成RN,在多业务接入的情况下简单的依靠module的startId做业务隔离是否够用。