在初识 React Native 时,非常令人困惑的一个地方就是 JS 和 Native 两个端之间是如何相互通信的。本篇文章对 iOS 端 React Native 启动时的调用流程做下简要总结,以此窥探其背后的通信机制。
JS 启动过程
React Native 的 iOS 端代码是直接从 Xcode IDE 里启动的。在启动时,首先要对代码进行编译,不出意外,在编译后会弹出一个命令行窗口,这个窗口就是通过 Node.js 启动的 development server。
问题是这个命令行是怎么启动起来的呢?实际上,Xcode 在 Build Phase 的最后一个阶段对此做了配置:
因此,代码编译后,就会执行 packager/react-native-xcode.sh
这个脚本。
查看这个脚本中的内容,发现它主要是读取 XCode 带过来的环境变量,同时加载 nvm 包使得 Node.js 环境可用,最后执行 react-native-cli 的命令:
1 2 3 4 5 6 |
react-native bundle \ --entry-file index.ios.js \ --platform ios \ --dev $DEV \ --bundle-output "$DEST/main.jsbundle" \ --assets-dest "$DEST" |
react-native
命令是全局安装的,在我本机上它的地址是 /usr/local/bin/react-native
。查看该文件,它调用了 react-native 包里的local-cli/cli.js
中的 run 方法,最终进入了 private-cli/src/bundle/buildBundle.js
。它的调用过程为:
- ReactPackager.createClientFor
- client.buildBundle
- processBundle
- saveBundleAndMap
上面四步完成的是 buildBundle 的功能,细节很多很复杂。总体来说,buildBundle 的功能类似于 browerify 或 webpack :
- 从入口文件开始分析模块之间的依赖关系;
- 对 JS 文件转化,比如 JSX 语法的转化等;
- 把转化后的各个模块一起合并为一个
bundle.js
。
之所以 React Native 单独去实现这个打包的过程,而不是直接使用 webpack ,是因为它对模块的分析和编译做了不少优化,大大提升了打包的速度,这样能够保证在 liveReload 时用户及时得到响应。
Tips: 通过访问 http://localhost:8081/debug/bundles 可以看到内存中缓存的所有编译后的文件名及文件内容,如:
Native 启动过程
Native 端就是一个 iOS 程序,程序入口是 main 函数,像通常一样,它负责对应用程序做初始化。
除了 main 函数之外,AppDelegate
也是一个比较重要的类,它主要用于做一些全局的控制。在应用程序启动之后,其中的 didFinishLaunchingWithOptions
方法会被调用,在这个方法中,主要做了几件事:
- 定义了 JS 代码所在的位置,它在 dev 环境下是一个 URL,通过 development server 访问;在生产环境下则从磁盘读取,当然前提是已经手动生成过了 bundle 文件;
- 创建了一个
RCTRootView
对象,该类继承于UIView
,处于程序所有 View 的最外层; - 调用 RCTRootView 的
initWithBundleURL
方法。在该方法中,创建了bridge
对象。顾名思义,bridge 起着两个端之间的桥接作用,其中真正工作的是类就是大名鼎鼎的 RCTBatchedBridge。
RCTBatchedBridge 是初始化时通信的核心,我们重点关注的是 start 方法。在 start 方法中,会创建一个 GCD 线程,该线程通过串行队列调度了以下几个关键的任务。
loadSource
该任务负责加载 JS 代码到内存中。和前面一致,如果 JS 地址是 URL 的形式,就通过网络去读取,如果是文件的形式,则通过读本地磁盘文件的方式读取。
initModules
该任务会扫描所有的 Native 模块,提取出要暴露给 JS 的那些模块,然后保存到一个字典对象中。
一个 Native 模块如果想要暴露给 JS,需要在声明时显示地调用 RCT_EXPORT_MODULE
。它的定义如下:
1 2 3 4 |
#define RCT_EXPORT_MODULE(js_name) \ RCT_EXTERN void RCTRegisterModule(Class); \ + (NSString *)moduleName { return @#js_name; } \ + (void)load { RCTRegisterModule(self); } |
可以看到,这就是一个宏,定义了 load 方法,该方法会自动被调用,在方法中对当前类进行注册。
模块如果要暴露出指定的方法,需要通过 RCT_EXPORT_METHOD 宏进行声明,原理类似。
setupExecutor
这里设置的是 JS 引擎,同样分为调试环境和生产环境:
在调试环境下,对应的 Executor 为 RCTWebSocketExecutor,它通过 WebSocket 连接到 Chrome 中,在 Chrome 里运行 JS;
在生产环境下,对应的 Executor 为 RCTContextExecutor,这应该就是传说中的 javascriptcore
。
moduleConfig
根据保存的模块信息,组装成一个 JSON ,对应的字段为 remoteModuleConfig。
injectJSONConfiguration
该任务将上一个任务组装的 JSON 注入到 Executor 中。
下面是一个 JSON 示例,由于实际的对象太大,这里只截取了前面的部分:
JSON 里面就是所有暴露出来的模块信息。
executeSourceCode
该任务中会执行加载过来的 JS 代码,执行时传入之前注入的 JSON。
在调试模式下,会通过 WebSocket 给 Chrome 发送一条 message,内容大致为:
1 2 3 4 5 6 |
{ id = 10305; inject = {remoteJSONConfig...}; method = executeApplicationScript; url = "http://localhost:8081/index.ios.bundle?platform=ios&dev=true"; } |
JS 接收消息后,执行打包后的代码。如果是非调试模式,则直接通过 javascriptcore 的虚拟环境去执行相关代码,效果类似。
JS 调用 Native
前面我们看到, Native 调用 JS 是通过发送消息到 Chrome 触发执行、或者直接通过 javascriptcore 执行 JS 代码的。而对于 JS 调用 Native 的情况,又是什么样的呢?
在 JS 端调用 Native 一般都是直接通过引用模块名,然后就使用了,比如:
1 |
var RCTAlertManager = require('NativeModules').AlertManager |
可见,NativeModules 是所有本地模块的操作接口,找到它的定义为:
1 |
var NativeModules = require('BatchedBridge').RemoteModules; |
而BatchedBridge中是一个MessageQueue的对象:
1 2 3 4 |
let BatchedBridge = new MessageQueue( __fbBatchedBridgeConfig.remoteModuleConfig, __fbBatchedBridgeConfig.localModulesConfig, ); |
在 MessageQueue 实例中,都有一个 RemoteModules 字段。在 MessageQueue 的构造函数中可以看出,RemoteModules 就是 __fbBatchedBridgeConfig.remoteModuleConfig 稍微加工后的结果。
1 2 3 4 5 6 7 8 |
class MessageQueue { constructor(remoteModules, localModules, customRequire) { this.RemoteModules = {}; this._genModules(remoteModules)/span>{}; this._genModules(remoteModules)Native 启动时的调用流程做下简要总结,以此窥探其背后的通信机制。
JS 启动过程React Native 的 iOS 端代码是直接从 Xcode IDE 里启动的。在启动时,首先要对代码进行编译,不出意外,在编译后会弹出一个命令行窗口,这个窗口就是通过 Node.js 启动的 development server。 问题是这个命令行是怎么启动起来的呢?实际上,Xcode 在 Build Phase 的最后一个阶段对此做了配置: 因此,代码编译后,就会执行
上面四步完成的是 buildBundle 的功能,细节很多很复杂。总体来说,buildBundle 的功能类似于 browerify 或 webpack :
之所以 React Native 单独去实现这个打包的过程,而不是直接使用 webpack ,是因为它对模块的分析和编译做了不少优化,大大提升了打包的速度,这样能够保证在 liveReload 时用户及时得到响应。 Tips: 通过访问 http://localhost:8081/debug/bundles 可以看到内存中缓存的所有编译后的文件名及文件内容,如: Native 启动过程Native 端就是一个 iOS 程序,程序入口是 main 函数,像通常一样,它负责对应用程序做初始化。 除了 main 函数之外,
RCTBatchedBridge 是初始化时通信的核心,我们重点关注的是 start 方法。在 start 方法中,会创建一个 GCD 线程,该线程通过串行队列调度了以下几个关键的任务。 loadSource该任务负责加载 JS 代码到内存中。和前面一致,如果 JS 地址是 URL 的形式,就通过网络去读取,如果是文件的形式,则通过读本地磁盘文件的方式读取。 initModules该任务会扫描所有的 Native 模块,提取出要暴露给 JS 的那些模块,然后保存到一个字典对象中。
可以看到,这就是一个宏,定义了 load 方法,该方法会自动被调用,在方法中对当前类进行注册。 模块如果要暴露出指定的方法,需要通过 RCT_EXPORT_METHOD 宏进行声明,原理类似。 setupExecutor这里设置的是 JS 引擎,同样分为调试环境和生产环境: moduleConfig根据保存的模块信息,组装成一个 JSON ,对应的字段为 remoteModuleConfig。 injectJSONConfiguration该任务将上一个任务组装的 JSON 注入到 Executor 中。 executeSourceCode该任务中会执行加载过来的 JS 代码,执行时传入之前注入的 JSON。
JS 接收消息后,执行打包后的代码。如果是非调试模式,则直接通过 javascriptcore 的虚拟环境去执行相关代码,效果类似。 JS 调用 Native前面我们看到, Native 调用 JS 是通过发送消息到 Chrome 触发执行、或者直接通过 javascriptcore 执行 JS 代码的。而对于 JS 调用 Native 的情况,又是什么样的呢? 在 JS 端调用 Native 一般都是直接通过引用模块名,然后就使用了,比如:
可见,NativeModules 是所有本地模块的操作接口,找到它的定义为:
而BatchedBridge中是一个MessageQueue的对象:
在 MessageQueue 实例中,都有一个 RemoteModules 字段。在 MessageQueue 的构造函数中可以看出,RemoteModules 就是 __fbBatchedBridgeConfig.remoteModuleConfig 稍微加工后的结果。
|