搜文章
推荐 原创 视频 Java开发 iOS开发 前端开发 JavaScript开发 Android开发 PHP开发 数据库 开发工具 Python开发 Kotlin开发 Ruby开发 .NET开发 服务器运维 开放平台 架构师 大数据 云计算 人工智能 开发语言 其它开发
Lambda在线 > 前端大全 > 使用 JS 构建跨平台的原生应用:React Native iOS 通信机制初探

使用 JS 构建跨平台的原生应用:React Native iOS 通信机制初探

前端大全 2017-11-28


来源:淘宝前端团队(FED)- 乾秋

网址:http://taobaofed.org/blog/2015/12/30/the-communication-scheme-of-react-native-in-ios/




在初识 React Native 时,非常令人困惑的一个地方就是 JS 和 Native 两个端之间是如何相互通信的。本篇文章对 iOS 端 React Native 启动时的调用流程做下简要总结,以此窥探其背后的通信机制。


JS 启动过程


React Native 的 iOS 端代码是直接从 Xcode IDE 里启动的。在启动时,首先要对代码进行编译,不出意外,在编译后会弹出一个命令行窗口,这个窗口就是通过 Node.js 启动的 development server。


问题是这个命令行是怎么启动起来的呢?实际上,Xcode 在 Build Phase 的最后一个阶段对此做了配置:


使用 JS 构建跨平台的原生应用:React Native iOS 通信机制初探


因此,代码编译后,就会执行 packager/react-native-xcode.sh 这个脚本。

查看这个脚本中的内容,发现它主要是读取 XCode 带过来的环境变量,同时加载 nvm 包使得 Node.js 环境可用,最后执行 react-native-cli 的命令:


react-native bundle \

--entry-file index.ios.js \

--platform ios \

--dev $DEV \

--bundle-output "$DEST/main.jsbundle" \

--assets-dest "$DEST"



  1. ReactPackager.createClientFor

  2. client.buildBundle

  3. processBundle

  4. saveBundleAndMap


上面四步完成的是 buildBundle 的功能,细节很多很复杂。总体来说,buildBundle 的功能类似于 browerify 或 webpack :


  1. 从入口文件开始分析模块之间的依赖关系;

  2. 对 JS 文件转化,比如 JSX 语法的转化等;

  3. 把转化后的各个模块一起合并为一个 bundle.js。


之所以 React Native 单独去实现这个打包的过程,而不是直接使用 webpack ,是因为它对模块的分析和编译做了不少优化,大大提升了打包的速度,这样能够保证在 liveReload 时用户及时得到响应。


Tips: 通过访问 http://localhost:8081/debug/bundles 可以看到内存中缓存的所有编译后的文件名及文件内容,如:


使用 JS 构建跨平台的原生应用:React Native iOS 通信机制初探


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



initModules


该任务会扫描所有的 Native 模块,提取出要暴露给 JS 的那些模块,然后保存到一个字典对象中。

一个 Native 模块如果想要暴露给 JS,需要在声明时显示地调用 RCT_EXPORT_MODULE。它的定义如下:


#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 示例,由于实际的对象太大,这里只截取了前面的部分:


使用 JS 构建跨平台的原生应用:React Native iOS 通信机制初探


JSON 里面就是所有暴露出来的模块信息。


executeSourceCode


该任务中会执行加载过来的 JS 代码,执行时传入之前注入的 JSON。

在调试模式下,会通过 WebSocket 给 Chrome 发送一条 message,内容大致为:


{

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 一般都是直接通过引用模块名,然后就使用了,比如:


var RCTAlertManager = require('NativeModules').AlertManager


可见,NativeModules 是所有本地模块的操作接口,找到它的定义为:


var NativeModules = require('BatchedBridge').RemoteModules;


而BatchedBridge中是一个MessageQueue的对象:


let BatchedBridge = new MessageQueue(

__fbBatchedBridgeConfig.remoteModuleConfig,

__fbBatchedBridgeConfig.localModulesConfig,

);


在 MessageQueue 实例中,都有一个 RemoteModules 字段。在 MessageQueue 的构造函数中可以看出,RemoteModules 就是 __fbBatchedBridgeConfig.remoteModuleConfig 稍微加工后的结果。


class MessageQueue {

constructor(remoteModules, localModules, customRequire) {

this.RemoteModules = {};

this._genModules(remoteModules);

...

}

}


所以问题就变为: __fbBatchedBridgeConfig.remoteModuleConfig 是在哪里赋值的?


实际上,这个值就是 从 Native 端传过来的JSON 。如前所述,Executor 会把模块配置组装的 JSON 保存到内部:


[_javaScriptExecutor injectJSONText:configJSON

asGlobalObjectNamed:@"__fbBatchedBridgeConfig"

callback:onComplete];


configJSON 实际保存的字段为:_injectedObjects['__fbBatchedBridgeConfig']。


在 Native 第一次调用 JS 时,_injectedObjects 会作为传递消息的 inject 字段。

JS 端收到这个消息,经过下面这个重要的处理过程:


'executeApplicationScript': function(message, sendReply) {

for (var key in message.inject) {

self[key] = JSON.parse(message.inject[key]);

}

importScripts(message.url);

sendReply();

},


看到没,这里读取了 inject 字段并进行了赋值。self 是一个全局的命名空间,在浏览器里 self===window。


因此,上面代码执行过后,window.__fbBatchedBridgeConfig 就被赋值为了传过来的 JSON 反序列化后的值。


总之:


NativeModules = __fbBatchedBridgeConfig.remoteModuleConfig = JSON.parse(message.inject[‘__fbBatchedBridgeConfig’]) = 模块暴露出的所有信息


好,有了上述的前提之后,接下来以一个实际调用例子说明下 JS 调用 Native 的过程。

首先我们通过 JS 调用一个 Native 的方法:


'executeApplicationScript': function(message, sendReply) {

for (var key in message.inject) {

self[key] = JSON.parse(message.inject[key]);

}

importScripts(message.url);

sendReply();

},


所有 Native 方法调用时都会先进入到下面的方法中:


fn = function(...args) {

let lastArg = args.length > 0 ? args[args.length - 1] : null;

let secondLastArg = args.length > 1 ? args[args.length - 2] : null;

let hasSuccCB = typeof lastArg === 'function';

let hasErrorCB = typeof secondLastArg === 'function';

let numCBs = hasSuccCB + hasErrorCB;

let onSucc = hasSuccCB ? lastArg : null;

let onFail = hasErrorCB ? secondLastArg : null;

args = args.slice(0, args.length - numCBs);

return self.__nativeCall(module, method, args, onFail, onSucc);

};


也就是倒数后两个参数是错误和正确的回调,剩下的是方法调用本身的参数。


在 __nativeCall 方法中,会将两个回调压到 callback 数组中,同时把 (模块、方法、参数) 也单独保存到内部的队列数组中:


onFail && params.push(this._callbackID);

this._callbacks[this._callbackID++] = onFail;

onSucc && params.push(this._callbackID);

this._callbacks[this._callbackID++] = onSucc;

this._queue[0].push(module);

this._queue[1].push(method);

this._queue[2].push(params);


到这一步,JS 端告一段落。接下来是 Native 端,在调用 JS 时,经过如下的流程:


使用 JS 构建跨平台的原生应用:React Native iOS 通信机制初探


总之,就是在调用 JS 时,顺便把之前保存的 queue 作为返回值 一并返回,然后会对该返回值进行解析。

在 _handleRequestNumber 方法中,终于完成了 Native 方法的调用:


- (BOOL)_handleRequestNumber:(NSUInteger)i

moduleID:(NSUInteger)moduleID

methodID:(NSUInteger)methodID

params:(NSArray *)params

{

// 解析模块和方法

RCTModuleData *moduleData = _moduleDataByID[moduleID];

id<RCTBridgeMethod> method = moduleData.methods[methodID];

<a href='http://www.jobbole.com/members/xyz937134366'>@try</a> {

// 完成调用

[method invokeWithBridge:self module:moduleData.instance arguments:params];

}

<a href='http://www.jobbole.com/members/wx895846013'>@catch</a> (NSException *exception) {

}

NSMutableDictionary *args = [method.profileArgs mutableCopy];

[args setValue:method.JSMethodName forKey:@"method"];

[args setValue:RCTJSONStringify(RCTNullIfNil(params), NULL) forKey:@"args"];

}


与此同时,执行后还会通过 invokeCallbackAndReturnFlushedQueue 触发 JS 端的回调。具体细节在 RCTModuleMethod 的 processMethodSignature 方法中。


再小结一下,JS 调用 Native 的过程为 :


  • JS 把(调用模块、调用方法、调用参数) 保存到队列中;

  • Native 调用 JS 时,顺便把队列返回过来;

  • Native 处理队列中的参数,同样解析出(模块、方法、参数),并通过 NSInvocation 动态调用;

  • Native方法调用完毕后,再次主动调用 JS。JS 端通过 callbackID,找到对应JS端的 callback,进行一次调用


整个过程大概就是这样,剩下的一个问题就是,为什么要等待 Native 调用 JS 时才会触发,中间会不会有很长延时?

事实上,只要有事件触发,Native 就会调用 JS。比如,用户只要对屏幕进行触摸,就会触发在 RCTRootView 中注册的 Handler,并发送给JS:


[_bridge enqueueJSCall:@"RCTEventEmitter.receiveTouches"

args:@[eventName, reactTouches, changedIndexes]];


除了触摸事件,还有 Timer 事件,系统事件等,只要事件触发了,JS 调用时就会把队列返回。这块理解可以参看 React Native通信机制详解 一文中的“事件响应”一节。


总结


俗话说一图胜千言,整个启动过程用一张图概括起来就是:


使用 JS 构建跨平台的原生应用:React Native iOS 通信机制初探


本文简要介绍了 iOS 端启动时 JS 和 Native 的交互过程,可以看出 BatchedBridge 在两端通信过程中扮演了重要的角色。Native 调用 JS 是通过 WebSocket 或直接在 javascriptcore 引擎上执行;JS 调用 Native 则只把调用的模块、方法和参数先缓存起来,等到事件触发后通过返回值传到 Native 端,另外两端都保存了所有暴露的 Native 模块信息表作为通信的基础。由于对 iOS 端开发并不熟悉,文中如有错误的地方还请指出。


参考资料:


  • GCD Reference

  • BRIDGING IN REACT NATIVE

  • React Native 调研报告

  • React Native通信机制详解



【今日微信公号推荐↓】

版权声明:本站内容全部来自于腾讯微信公众号,属第三方自助推荐收录。《使用 JS 构建跨平台的原生应用:React Native iOS 通信机制初探》的版权归原作者「前端大全」所有,文章言论观点不代表Lambda在线的观点, Lambda在线不承担任何法律责任。如需删除可联系QQ:516101458

文章来源: 阅读原文

相关阅读

关注前端大全微信公众号

前端大全微信公众号:FrontDev

前端大全

手机扫描上方二维码即可关注前端大全微信公众号

前端大全最新文章

精品公众号随机推荐