金沙棋牌官方平台

当前位置:金沙棋牌 > 金沙棋牌官方平台 > 构建跨平台的原生应用,通讯及消息循环代码剖

构建跨平台的原生应用,通讯及消息循环代码剖

来源:http://www.logblo.com 作者:金沙棋牌 时间:2019-11-05 18:51

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

2015/12/30 · JavaScript · React Native

原文出处: 淘宝前端团队(FED)- 乾秋   

金沙棋牌官方平台 1

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

肥皂V 2016 1.3

本文将从React Native的整体架构、通信机制、具体交互细节来深入介绍React Native在iOS平台上的工作原理。有不对之处,还望指点。

原文参考及摘录 - React Native的极简手册

JS 启动过程

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

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

因此,代码编译后,就会执行 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
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。它的调用过程为:

  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: 通过访问 可以看到内存中缓存的所有编译后的文件名及文件内容,如:
金沙棋牌官方平台 3

React Native 已经推出近一年时间了,近期也在研究iOS下用js写app的框架,从徘徊和犹豫中,最终还是选定React Native,她就像若隐若现的女神一样,想要下决心追到,可是不容易。要想把她应用的已存在的有一定体量的app中,更是不易,让我先把她里外都了解清楚,在分享一下合理应用到现有app的方案,这是深入React Native系列的第一篇,后续会继续分享使用过程中的一些认识。

一、整体架构介绍

组件的生命周期

组件的生命周期分成三个状态:

Mounting:已插入真实 DOM
Updating:正在被重新渲染
Unmounting:已移出真实 DOM

React 为每个状态都提供了两种处理函数,will 函数在进入状态之前调用,did 函数在进入状态之后调用,三种状态共计五种处理函数。

componentWillMount()
componentDidMount()
componentWillUpdate(object nextProps, object nextState)
componentDidUpdate(object prevProps, object prevState)
componentWillUnmount()

此外,React 还提供两种特殊状态的处理函数。

componentWillReceiveProps(object nextProps):已加载组件收到新的参数时调用
shouldComponentUpdate(object nextProps, object nextState):组件判断是否重新渲染时调用

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 线程,该线程通过串行队列调度了以下几个关键的任务。

本篇详细分析下React Native 中 Native和JS的互相调用的原理解析。之前bang的文章已经介绍过,本文从代码层面更深入的来讲解,分析基于 React Native 0.17.0 版本, RN在快速进化,其中的内容已和之前的旧版本有些不同

React提供了一套基于JS/JSX的UI库,而React Native是基于React可以生成Native控件的框架。

JS 和 Native 交互

Native 调用 JS 是通过发送消息到 Chrome 触发执行、或者直接通过 javascriptcore 执行 JS 代码的。在 JS 端调用 Native 一般都是直接通过引用模块名,JS 把(调用模块、调用方法、调用参数) 保存到队列中;Native 调用 JS 时,顺便把队列返回过来;Native 处理队列中的参数,同样解析出(模块、方法、参数),并通过 NSInvocation 动态调用;Native方法调用完毕后,再次主动调用 JS。JS 端通过 callbackID,找到对应JS端的 callback,进行一次调用。两端都保存了所有暴露的 Native 模块信息表作为通信的基础。

JS不会主动传递数据给OC,在调OC方法时,会把ModuleID,MethodID等数据加到一个队列里,等OC过来调JS的任意方法时,再把这个队列返回给OC,此时OC再执行这个队列里要调用的方法。native开发里,只在有事件触发的时候才执行代码。在React Native里,事件发生时OC都会调用JS相应的模块方法去处理,处理完这些事件后再执行JS想让OC执行的方法,而没有事件发生的时候,是不会执行任何代码的。

loadSource

该任务负责加载 JS 代码到内存中。和前面一致,如果 JS 地址是 URL 的形式,就通过网络去读取,如果是文件的形式,则通过读本地磁盘文件的方式读取。

作为初篇,先创建一个示例工程,以后的分享都以这个工程为基础。目前这个工程还很简单,main.js的讲解可以下载这里的代码

金沙棋牌官方平台 4

总结

React Native的通讯基础建立在传统的JS Bridge之上,不过对于Bridge处理的MessageQueue机制、模块定义、加载机制上的巧妙处理指的借鉴。对于上述的整个原理解析可以概括为以下四个部分:

  • 在启动阶段,初始化JS引擎,生成Native端模块配置表存于两端,其中模块配置是同步取得,而各模块的方法配置在该方法被真正调用时懒加载。
  • Native和JS端分别有一个bridge,发生调用时,调用端bridge查找模块配置表将调用转换成{moduleID, methodID, args(callbackID)},处理端通过同一份模块配置表转换为实际的方法实现。
  • Native->JS,原理上使用JSCore从Native执行JS代码,React-Native在此基础上给我们提供了通知发送的执行方式。
  • JS->Native,原理上JS并不主动调用Native,而是把方法和参数(回调)缓存到队列中,在Native事件触发并访问JS后,通过blocks回调Native。

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); }

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 宏进行声明,原理类似。

GitHub MGReactNativeTest工程里有直接改动main.jsbundle

我总结是两层,Native层提供了JS-OCBridge,与JavaScript进行交互。JavaScript中提供了一些列与Native一一对应的API。 注:网上看到有些是提供了Javascript Bridge,因为没有研究React代码,有待以后了解。

一点思考

通过原理性的探究RN通信机制,不难发现这是一种组件化思想很好的表达。我们在平时的代码结构中,经常会以模块化、组件化为目标去做架构,其中最重要的技术方案就是对“分发器”的设计。如RN机制的实现,本质上也是对JS端和Native端的各类调用的“分发”处理,通过模块配置表去匹配到要找的处理方法,也通过模块配置表去传递正确分发事件后所需的数据。

这种“分发”正是我们设计组件化架构的核心,无论我们的“乐高积木”(各个组件)是塑料还是木头所制,只要按照图纸所定义的“插槽”去设计,通过某种规范,玩家就知道如何将这块“积木”去分发到哪个部位。无论OC原生也好,JS也好甚至利用webview去自定义处理各种url也好,我们最需要的是如何设计好“分发器”来支持不同指令的分发处理,而对于组件本身实现语言的要求并不严格。

setupExecutor

这里设置的是 JS 引擎,同样分为调试环境和生产环境:
在调试环境下,对应的 Executor 为 RCTWebSocketExecutor,它通过 WebSocket 连接到 Chrome 中,在 Chrome 里运行 JS;
在生产环境下,对应的 Executor 为 RCTContextExecutor,这应该就是传说中的 javascriptcore

示例工程的代码

 render: function() { return ( <View style={styles.container}> <Text style={styles.welcome}> Welcome to React Native! </Text> <Text style={styles.instructions}> {this.state.changeText} </Text> <Text style={styles.welcome} onPress={this._onPress}> Change </Text> </View> ); },

金沙棋牌官方平台 511.png

)

二、初始化流程

moduleConfig

根据保存的模块信息,组装成一个 JSON ,对应的字段为 remoteModuleConfig。

Native 与 JS的互相调用

0.17版本的React Native JS引擎已经全部使用的是iOS自带的JavaScriptCore,在JSContext提供的Native与js互相调用的基础上,封装出了自己的互调方法。下面是一张结构图

金沙棋牌官方平台 6金沙棋牌官方平台,架构图.png

初始化流程牵涉几个部分:

injectJSONConfiguration

该任务将上一个任务组装的 JSON 注入到 Executor 中。
下面是一个 JSON 示例,由于实际的对象太大,这里只截取了前面的部分:
金沙棋牌官方平台 7
JSON 里面就是所有暴露出来的模块信息。

App启动过程中 Native和JS互相调用的日志

[Log] N->JS : RCTDeviceEventEmitter.emit(["appStateDidChange",{"app_state":"active"}]) (main.js, line 638)[Log] N->JS : RCTDeviceEventEmitter.emit(["networkStatusDidChange",{"network_info":"wifi"}]) (main.js, line 638)[Log] N->JS : AppRegistry.runApplication(["MGReactNative",{"rootTag":1,"initialProps":{}}]) (main.js, line 638)[Log] Running application "MGReactNative" with appParams: {"rootTag":1,"initialProps":{}}. __DEV__ === true, development-level warning are ON, performance optimizations are OFF (main.js, line 638)[Log] JS->N : RCTUIManager.createView([2,"RCTView",1,{"flex":1}]) (main.js, line 638)[Log] JS->N : RCTUIManager.createView([3,"RCTView",1,{"flex":1}]) (main.js, line 638)[Log] JS->N : RCTUIManager.createView([4,"RCTView",1,{"flex":1,"justifyContent":"center","alignItems":"center","backgroundColor":4294311167}]) (main.js, line 638)[Log] JS->N : RCTUIManager.createView([5,"RCTText",1,{"fontSize":20,"textAlign":"center","margin":10,"accessible":true,"allowFontScaling":true}]) (main.js, line 638)[Log] JS->N : RCTUIManager.createView([6,"RCTRawText",1,{"text":"Welcome to React Native!"}]) (main.js, line 638)[Log] JS->N : RCTUIManager.manageChildren([5,null,null,[6],[0],null]) (main.js, line 638)[Log] JS->N : RCTUIManager.createView([7,"RCTText",1,{"textAlign":"center","color":4281545523,"marginBottom":5,"accessible":true,"allowFontScaling":true}]) (main.js, line 638)[Log] JS->N : RCTUIManager.createView([8,"RCTRawText",1,{"text":"soap1"}]) (main.js, line 638)[Log] JS->N : RCTUIManager.manageChildren([7,null,null,[8],[0],null]) (main.js, line 638)[Log] JS->N : RCTUIManager.createView([9,"RCTText",1,{"fontSize":20,"textAlign":"center","margin":10,"accessible":true,"allowFontScaling":true,"isHighlighted":false}]) (main.js, line 638)[Log] JS->N : RCTUIManager.createView([10,"RCTRawText",1,{"text":"Change"}]) (main.js, line 638)[Log] JS->N : RCTUIManager.manageChildren([9,null,null,[10],[0],null]) (main.js, line 638)[Log] JS->N : RCTUIManager.manageChildren([4,null,null,[5,7,9],[0,1,2],null]) (main.js, line 638)[Log] JS->N : RCTUIManager.manageChildren([3,null,null,[4],[0],null]) (main.js, line 638)[Log] JS->N : RCTUIManager.createView([12,"RCTView",1,{"position":"absolute"}]) (main.js, line 638)[Log] JS->N : RCTUIManager.manageChildren([2,null,null,[3,12],[0,1],null]) (main.js, line 638)[Log] JS->N : RCTUIManager.manageChildren([1,null,null,[2],[0],null]) (main.js, line 638)

日志显示了启动React Native 界面 Native与JS的调用过程,我们从最简单的例子入手,慢慢脱下女神的面纱。

1、加载JS代码(RCTJavaScriptLoader)

executeSourceCode

该任务中会执行加载过来的 JS 代码,执行时传入之前注入的 JSON。
在调试模式下,会通过 WebSocket 给 Chrome 发送一条 message,内容大致为:

JavaScript

{ id = 10305; inject = {remoteJSONConfig...}; method = executeApplicationScript; url = ""; }

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 的虚拟环境去执行相关代码,效果类似。

Native调用JS (Native->JS)

可以看到,启动开始之后,Native调用了JS的 RCTDeviceEventEmitter.emit 广播了两个事件 appStateDidChange,networkStatusDidChange随后调用 AppRegistry.runApplication(["MGReactNative",{"rootTag":1,"initialProps":{}}]) 启动了React Native引擎。下面我们一点点分析,是如果从Native调用到JS的函数AppRegistry.runApplication的

JSContext *context = [[JSContext alloc] init];[context evaluateScript:@"function add { return a + b; }"];JSValue *add = context[@"add"];NSLog(@"Func: %@", add); JSValue *sum = [add callWithArguments:@[@]];NSLog(@"Sum: %d",[sum toInt32]);//OutPut:// Func: function add { return a + b; }// Sum: 28

JSContext 是运行 JavaScript 代码的环境。一个 JSContext 是一个全局环境的实例,我们可以从 JSContext全局变量中用下标的方式取出JS代码中定义的函数 add,它用JSValue类型包装了一个 JS 函数, 如果你确定JSValue是一个JS函数类型,可以使用callWithArguments 来调用它。更详细的介绍可以学习这篇文章 Java​Script​Core

聪明的你一定想到,React Native 的也是用同样方式调用到AppRegistry.runApplication,是的,不过是通过一个通用接口来调用的RCTJavaScriptContext 封装了OC方法callFunction,

- callFunctionOnModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args callback:(RCTJavaScriptCallback)onComplete{ // TODO: Make this function handle first class instead of dynamically dispatching it. #9317773 [self _executeJSCall:@"callFunctionReturnFlushedQueue" arguments:@[module, method, args] callback:onComplete];}

_executeJSCall 执行的具体代码是

 method = @“callFunctionReturnFlushedQueue” JSStringRef moduleNameJSStringRef = JSStringCreateWithUTF8CString("__fbBatchedBridge"); JSValueRef moduleJSRef = JSObjectGetProperty(contextJSRef, globalObjectJSRef, moduleNameJSStringRef, &errorJSRef); JSStringRef methodNameJSStringRef = JSStringCreateWithCFString((__bridge CFStringRef)method); JSValueRef methodJSRef = JSObjectGetProperty(contextJSRef, (JSObjectRef)moduleJSRef, methodNameJSStringRef, resultJSRef = JSObjectCallAsFunction(contextJSRef, (JSObjectRef)methodJSRef, (JSObjectRef)moduleJSRef, 0, NULL, 

可以看到Native 从JSContext中拿出JS全局对象 __fbBatchedBridge,然后调用了其callFunctionReturnFlushedQueue函数

在RCTBatchedBridge的- (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad中,调用RCTJavaScriptLoader加载相关数据:

JS 调用 Native

前面我们看到, Native 调用 JS 是通过发送消息到 Chrome 触发执行、或者直接通过 javascriptcore 执行 JS 代码的。而对于 JS 调用 Native 的情况,又是什么样的呢?

在 JS 端调用 Native 一般都是直接通过引用模块名,然后就使用了,比如:

JavaScript

var RCTAlertManager = require('NativeModules').AlertManager

1
var RCTAlertManager = require('NativeModules').AlertManager

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

JavaScript

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

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

而BatchedBridge中是一个MessageQueue的对象:

JavaScript

let BatchedBridge = new MessageQueue( __fbBatchedBridgeConfig.remoteModuleConfig, __fbBatchedBridgeConfig.localModulesConfig, );

1
2
3
4
let BatchedBridge = new MessageQueue(
  __fbBatchedBridgeConfig.remoteModuleConfig,
  __fbBatchedBridgeConfig.localModulesConfig,
);

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

JavaScript

class MessageQueue { constructor(remoteModules, localModules, customRequire) { this.RemoteModules = {}; this._genModules(remoteModules); ... } }

1
2
3
4
5
6
7
8
class MessageQueue {
 
  constructor(remoteModules, localModules, customRequire) {
    this.RemoteModules = {};
    this._genModules(remoteModules);
    ...
    }
}

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

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

JavaScript

[_javaScriptExecutor injectJSONText:configJSON asGlobalObjectNamed:@"__fbBatchedBridgeConfig" callback:onComplete];

1
2
3
[_javaScriptExecutor injectJSONText:configJSON
                  asGlobalObjectNamed:@"__fbBatchedBridgeConfig"
                             callback:onComplete];

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

在 Native 第一次调用 JS 时,_injectedObjects 会作为传递消息的 inject 字段。
JS 端收到这个消息,经过下面这个重要的处理过程:

JavaScript

'executeApplicationScript': function(message, sendReply) { for (var key in message.inject) { self[key] = JSON.parse(message.inject[key]); } importScripts(message.url); sendReply(); },

1
2
3
4
5
6
7
'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 的方法:

JavaScript

'executeApplicationScript': function(message, sendReply) { for (var key in message.inject) { self[key] = JSON.parse(message.inject[key]); } importScripts(message.url); sendReply(); },

1
2
3
4
5
6
7
'executeApplicationScript': function(message, sendReply) {
    for (var key in message.inject) {
      self[key] = JSON.parse(message.inject[key]);
    }
    importScripts(message.url);
    sendReply();
  },

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

JavaScript

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); };

1
2
3
4
5
6
7
8
9
10
11
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 数组中,同时把 (模块、方法、参数) 也单独保存到内部的队列数组中:

JavaScript

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);

1
2
3
4
5
6
7
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 时,经过如下的流程:

金沙棋牌官方平台 8

总之,就是在调用 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='; { // 完成调用 [method invokeWithBridge:self module:moduleData.instance arguments:params]; } <a href='; (NSException *exception) { } NSMutableDictionary *args = [method.profileArgs mutableCopy]; [args setValue:method.JSMethodName forKey:@"method"]; [args setValue:RCTJSONStringify(RCTNullIfNil(params), NULL) forKey:@"args"]; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (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:

JavaScript

[_bridge enqueueJSCall:@"RCTEventEmitter.receiveTouches" args:@[eventName, reactTouches, changedIndexes]];

1
2
[_bridge enqueueJSCall:@"RCTEventEmitter.receiveTouches"
                  args:@[eventName, reactTouches, changedIndexes]];

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

是时候克服心中的恐惧,开始掀裙子了 ,来看看main.jsbundle中的JS代码

上文Natvie调用JS的路径到了 __fbBatchedBridge.callFunctionReturnFlushedQueue js代码这一步,demo工程中,我们自己写的index.ios.js 只有区区几行,去Node Server转一圈或React Native的预编译之后,竟然产生了1.3M,近5W行JS代码的main.jsbundle 文件,对于终端同学来说,简直是一座五指山。不要害怕,我们一起探寻其中的奥妙。

继续跟踪js代码中的 __fbBatchedBridge

//main.js__d('BatchedBridge',function(global, require, module, exports) { 'use strict'; var MessageQueue=require('MessageQueue'); var BatchedBridge=new MessageQueue( __fbBatchedBridgeConfig.remoteModuleConfig, __fbBatchedBridgeConfig.localModulesConfig); Object.defineProperty(global,'__fbBatchedBridge',{value:BatchedBridge}); module.exports=BatchedBridge;});

我们发现这段JS代码中有这句 Object.defineProperty(global,'__fbBatchedBridge',{value:BatchedBridge});

else if (self.bundleURL) {    [RCTJavaScriptLoaderloadBundleAtURL:self.bundleURLonComplete:onSourceLoad];  }

总结

俗话说一图胜千言,整个启动过程用一张图概括起来就是:
金沙棋牌官方平台 9

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

参考资料:

  • GCD Reference
  • BRIDGING IN REACT NATIVE
  • React Native 调研报告
  • React Native通信机制详解

    1 赞 1 收藏 评论

金沙棋牌官方平台 10

准备知识,对JS很熟的同学可以略过或指正

这段JS代码怎么理解呢,这个是nodejs的模块代码,当打包成main.js之后,含义又有变化,我们简单可以这样理解,__d() 是一个定义module的JS函数,其就等于下面这段代码中的 define 函数,从代码上很容易可以理解,它定义一个module,名字Id为BatchedBridge,同时传递了一个工厂函数,另一个模块的代码可以通过调用require获取这个module,例如var BatchedBridge=require('BatchedBridge');

这是一个懒加载机制,当有人调用require时,工厂函数才执行,在代码最后,把这个模块要导出的内容赋值给module.exports。

 function define(id,factory){ modules[id]={ factory:factory, module:{exports:{}}, isInitialized:false, hasError:false};} function require{ var mod=modules[id]; if(mod&&mod.isInitialized){ return mod.module.exports;}

好,我们抓紧回来,在上段代码中当BatchedBridge module创建时,通过这句 Object.defineProperty(global,'__fbBatchedBridge',{value:BatchedBridge}); 把自己定义到JSContext的全局变量上。所以在Native代码中可以通过 JSContext[@"__fbBatchedBridge"]获取到,

从代码中也可以看到BatchedBridge 是JS类MessageQueue的实例,并且它导出的时候并没有导出构造函数MessageQueue,而是导出的实例BatchedBridge,所以它是React Native JS引擎中全局唯一的。它也是Natvie和JS互通的关键桥梁。

 __fbBatchedBridge.callFunctionReturnFlushedQueue("AppRegistr","runApplication",["MGReactNative",{"rootTag":1,"initialProps":{}}])

我们继续看MessageQueue 类的callFunctionReturnFlushedQueue 函数,它最终调用到__callFunction(module, method, args)函数

__callFunction(module, method, args) { var moduleMethods = this._callableModules[module]; if (!moduleMethods) { moduleMethods = require; } moduleMethods[method].apply(moduleMethods, args); }

看起来__callFunction就是最终的分发函数了,首先它从this._callableModules中找到模块对象,如果它还没有加载,就动态加载它,如果找到就执行最终的JS函数。

先看下AppRegistry是如何暴露给Natvie的

__d('AppRegistry',function(global, require, module, exports) { 'use strict'; var BatchedBridge=require('BatchedBridge'); var ReactNative=require('ReactNative'); var AppRegistry={ runApplication:function(appKey,appParameters){ runnables[appKey].run(appParameters); }, } BatchedBridge.registerCallableModule( 'AppRegistry', AppRegistry); module.exports=AppRegistry;});

有前面的讲解,现在看这个应该不态费劲了,可以看到AppRegistry模块工厂函数中,执行了 BatchedBridge.registerCallableModule('AppRegistry',AppRegistry);,把自己注册到BatchedBridge的CallableModule中,所以在上一节中,__callFunction才能在_callableModules找到AppRegistry实例,才能调用其runApplication函数。自己写的模块代码可以用React Native这种方式暴露给Natvie调用,和直接暴露的区别是,符合React Natvie的模块化原则,另外一个直观的好处是你的模块可以是懒加载的,并且不会污染全局空间。

目前终于把从N-JS的整个路径跑通了,我们梳理下整个流程看看。

1 [RCTBatchedBridge enqueueJSCall:@“AppRegistry.runApplication” args:["MGReactNative",{"rootTag":1,"initialProps":{}}]];2 RCTJavaScriptContext callFunctionOnModule:@"AppRegistr" method:@"runApplication" arguments:["MGReactNative",{"rootTag":1,"initialProps":{}}] callback:(RCTJavaScriptCallback)onComplete//main.js3 __fbBatchedBridge.callFunctionReturnFlushedQueue("AppRegistr","runApplication",["MGReactNative",{"rootTag":1,"initialProps":{}}])//main.js4 BatchedBridge.__callFunction("AppRegistr","runApplication",["MGReactNative",{"rootTag":1,"initialProps":{}}])//main.js5 var moduleMethods = BatchedBridge._callableModules[module]; if (!moduleMethods) { moduleMethods = require; } moduleMethods[method].apply(moduleMethods, args);

接下来我们看看从JS如何调用Native,换句话说Native如何开放API给JS

我们以弹Alert框的接口为例,这是Native的OC代码,导出RCTAlertManager类的alertWithArgs:(NSDictionary *)argscallback:(RCTResponseSenderBlock)callback)方法

@interface RCTAlertManager() : NSObject <RCTBridgeModule, RCTInvalidating>...@end@implementation RCTAlertManagerRCT_EXPORT_MODULE()RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args callback:(RCTResponseSenderBlock)callback){...}#end
  • OC类实现RCTBridgeModule协议
  • 在.m的类实现中加入RCT_EXPORT_MODULE(),帮助你实现RCTBridgeModule协议
  • 要导出的函数用RCT_EXPORT_METHOD()宏括起来,不用这个宏,不会导出任何函数

现在从JS里可以这样调用这个方法:

var RCTAlertManager=require('react-native').NativeModules.AlertManager;RCTAlertManager.alertWithArgs({message:'JS->Native Call',buttons:[{k1:'button1'},{k2:'button1'}]},function {console.log('RCTAlertManager.alertWithArgs() id:' + id +' v:' + v)});

执行之后的效果,弹出一个Alert

金沙棋牌官方平台 11alert.png

对于详细的如何导出函数推荐阅读Native Modules

我们今天的目的不是和女神喝茶聊天,是深入女神内心,是内心咳咳。来看看今天的重点

在JS中可以直接使用RCTAlertManager.alertWithArgs来调用,说明JS中已经定义了和OC对象相对应的JS对象,我们从导出一个Native类开始,完整跟踪下这个过程。

RCTAlertManager类实现了RCTBridgeModule协议,并且在类的实现里包含了RCT_EXPORT_MODULE() 宏

@protocol RCTBridgeModule <NSObject> #define RCT_EXPORT_MODULE RCT_EXTERN void RCTRegisterModule; + (NSString *)moduleName { return @#js_name; } + load { RCTRegisterModule; }// Implemented by RCT_EXPORT_MODULE+ (NSString *)moduleName;@optional

在OC里,一个类所在文件被引用时,系统会调用其+load函数,当RCTAlertManager所在文件被引用时,系统调用load 函数,函数里简单的调用RCTRegisterModule 把自己注册到一个全局数组RCTModuleClasses,这样系统中导出的类都会自动注册到这个全局变量数组里。

在JS中有一个BatchedBridge用来和Native通讯,在Natvie中也有一个RCTBatchedBridge类,它封装了JSContext即JS引擎在RCTBatchedBridge start 函数中,做了5件事

  1. jsbundle文件的下载或本地读取
  2. 初始化导出给JS用的Native模块
  3. 初始化JS引擎
  4. 生成配置表,并注入到JS引擎中,
  5. 执行jsbundle文件。
 //伪代码 - start{ //1 jsbundle文件的下载或本地读取 NSData *sourceCode; [self loadSource:^(NSError *error, NSData *source) {sourceCode = source}]; //2 初始化导出给JS用的Native模块 [self initModules]; //3 初始化JS引擎 [self setUpExecutor]; //4 生成Native模块配置表 把配置表注入到JS引擎中 NSSting* config = [self moduleConfig]; [self injectJSONConfiguration:config onComplete:^(NSError *error) {}); //5 最后执行jsbundle [self executeSourceCode:sourceCode];}

现在我们最关心第二步初始化Native模块 initModules 和moduleConfig 到底是什么

//伪代码- initModules{ //遍历上节讲到的RCTGetModuleClasses全局数组,用导出模块的类或者实例创建RCTModuleData for (Class moduleClass in RCTGetModuleClasses { NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass); //这里一个很有意思的地方,如果导出的类或其任何父类重写了init方法,或者类中有setBridge方法 //则React Native假设开发者期望这个导出模块在Bridge第一次初始化时实例化,否则怎么样,大家想想 if ([moduleClass instanceMethodForSelector:@selector] != objectInitMethod || [moduleClass instancesRespondToSelector:setBridgeSelector]) { module = [moduleClass new]; } // 创建RCTModuleData RCTModuleData *moduleData; if  { moduleData = [[RCTModuleData alloc] initWithModuleInstance:module]; } else { moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass bridge:self]; } //保存到数组中,数组index就是这个模块的索引 [_moduleDataByID addObject:moduleData]; }}

initModules里根据是否重写init或添加了setBridge来决定是不是要马上实例化RCTGetModuleClasses里的导出类,然后用实例或类创建RCTModuleData,缓存到本地,以便JS调用时查询。

再来看第四步导出的 NSSting* config = [self moduleConfig] 是什么内容

 {"remoteModuleConfig": [["RCTStatusBarManager"], ["RCTSourceCode"], ["RCTAlertManager"], ["RCTExceptionsManager"], ["RCTDevMenu"], ["RCTKeyboardObserver"], ["RCTAsyncLocalStorage"], . . . ]} 

它仅仅是一个类名数组。

生产配置表后,通过下面的方法把这个类名数组注入到JSContext,赋值给JS全局变量__fbBatchedBridgeConfig

 [_javaScriptExecutor injectJSONText:configJSON asGlobalObjectNamed:@"__fbBatchedBridgeConfig" callback:onComplete];

在JS端,当有人引用了BatchedBridge var BatchedBridge=require('BatchedBridge');,其工厂函数会通过 __fbBatchedBridgeConfig配置表创建MessageQueue的实例BatchedBridge

 var MessageQueue=require('MessageQueue'); var BatchedBridge=new MessageQueue( __fbBatchedBridgeConfig.remoteModuleConfig, __fbBatchedBridgeConfig.localModulesConfig);

我们看看MessageQueue的构造函数,构造函数里为每个导出类创建了一个对应的module对象,因为此时config里只有一个导出类的名字,所以这里只为这个对象增加了一个成员变量 module.moduleID,并把module保存到this.RemoteModules数组里

 _genModule(config, moduleID) { let module = {}; if (!constants && !methods && !asyncMethods) { module.moduleID = moduleID; } this.RemoteModules[moduleName] = module; }

接着我们顺藤摸瓜看看那里使用的BatchedBridge.RemoteModules

NativeModules在初始化时,用BatchedBridge.RemoteModules保存的类名列表,为每个JS对象增加了函数等属性

__d('NativeModules',function(global, require, module, exports) { 'use strict'; var RemoteModules=require('BatchedBridge').RemoteModules; var NativeModules={}; //遍历NativeModules中导出类名 Object.keys(RemoteModules).forEach(function(moduleName){ //把类名定义为NativeModules的一个属性,比如AlertManager类,定义只有就可以用NativeModules.AlertManager 访问 Object.defineProperty(NativeModules,moduleName,{ //这个属性(AlertManager)是可以遍历的,当然属性也是个对象里面有属性和函数 enumerable:true, //属性都有get和set函数,当调用访问这个属性时,会调用get函数 NativeModules.AlertManager get:function(){ var module=RemoteModules[moduleName]; if(module&&typeof module.moduleID==='number'&&global.nativeRequireModuleConfig){ //调用Native提供的全局函数nativeRequireModuleConfig查询AlertManager 导出的常量和函数 var json=global.nativeRequireModuleConfig(moduleName); module=config&&BatchedBridge.processModuleConfig(JSON.parse,module.moduleID); RemoteModules[moduleName]=module; } return module; } }); }); module.exports=NativeModules;});

React Native 把所有的Native导出类定义在一个NativeModules模块里,所以使用Natvie接口时也可以直接这样拿到对应的JS对象var RCTAlertManager=require('NativeModules').AlertManager;

代码里我加了注释思考一个问题,为什么React Natvie搞的那么麻烦,为什么不在上一个步骤里(MessageQueue的构造函数)里就创建出完整的JS对象。

没错,就是模块的懒加载,虽然Native导出了Alert接口,在JS引擎初始化后,JS里只存在一个名字为AlertManager的空对象

金沙棋牌官方平台 12906C7A90-0A85-4FD6-B433-39CE041D4445.png

当调用了RCTAlertManager.alertWithArgs({message:'JS->Native Call',buttons:[{k1:'button1'}时,才会调用AlertManager 的get函数到Native里查询导出的常量和函数,并定义到AlertManager中。

金沙棋牌官方平台 137RGT1@Z}N19_9{KQ~P_SDFE.jpg

RCTAlertManager.alertWithArgs 这个函数是如何调用到Native里的呢,在BatchedBridge.processModuleConfig函数中,用_genMethod创建了一个闭包fn为每个函数赋值,这个函数转调self.__nativeCall(module, method, args, onFail, onSucc); 我们调用RCTAlertManager.alertWithArgs函数,其实都是调用的这个fn闭包。

 _genMethod(module, method, type) { fn = function { return self.__nativeCall(module, method, args, onFail, onSucc); }; return fn; }

__nativeCall,好熟悉的名字,

 __nativeCall(module, method, params, onFail, onSucc) { this._queue[MODULE_IDS].push; this._queue[METHOD_IDS].push; this._queue[PARAMS].push; global.nativeFlushQueueImmediate(this._queue); this._queue = [[],[],[]]; this._lastFlush = now; }

global.nativeFlushQueueImmediate 是Native提供的接口,__nativeCall把需要调用的module,method,params都塞到队列里,然后传递到Native,

我们在回到Native 找到上文提到的两个关键接口,Native模块查询接口:global.nativeRequireModuleConfig和调用接口global.nativeFlushQueueImmediate,他们是在JS引擎(JSContext)初始化时,定义到全局变量的。

//RCTContextExecutor setUP//简化过的代码- setUp{ ... self->_context.context[@"nativeRequireModuleConfig"] = ^NSString *(NSString *moduleName) { NSArray *config = [weakBridge configForModuleName:moduleName]; return RCTJSONStringify(config, NULL); }; self->_context.context[@"nativeFlushQueueImmediate"] = ^(NSArray<NSArray *> *calls){ [weakBridge handleBuffer:calls batchEnded:NO]; }; ...}

[weakBridge handleBuffer:calls batchEnded:NO]; 经过一系列传递,调用到_handleRequestNumber 中,用moduleID找到RCTModuleData,再用methodID 找到id<RCTBridgeMethod> method 然后在moduleData.instance实例中执行

- _handleRequestNumber:(NSUInteger)i moduleID:(NSUInteger)moduleID methodID:(NSUInteger)methodID params:(NSArray *)params{ RCTModuleData *moduleData = _moduleDataByID[moduleID]; id<RCTBridgeMethod> method = moduleData.methods[methodID]; [method invokeWithBridge:self module:moduleData.instance arguments:params];}

这里有必要再强调一次moduleData.instance 这个地方。

- (id<RCTBridgeModule>)instance{ if (!_instance) { _instance = [_moduleClass new]; ... } return _instance;}

还记的前面BatchedBridge 初始化时的initModules吗

 //这里一个很有意思的地方,如果导出的类或其任何父类重写了init方法,或者类中有setBridge方法 //则React Native假设开发者期望这个导出模块在Bridge第一次初始化时实例化,否则怎么样,大家想想 if ([moduleClass instanceMethodForSelector:@selector] != objectInitMethod || [moduleClass instancesRespondToSelector:setBridgeSelector]) { module = [moduleClass new]; }

否则就是在用户真正调用时,在moduleData.instance里实例化,React Native已经懒到骨髓了。

RCTModuleData中每个函数的封装 RCTModuleMethod里还有一个优化点,JS传递到Native的参数需要进行响应的转换,RCTModuleMethod在调用函数只前,先预解析一下,创建每个参数转换的block,缓存起来,这样调用时,就直接使用对应函数指针进行参数转换了,大要详细了解可以看

  • processMethodSignature函数。

前面我们为了直观,忽略了回调函数,alertWithArgs的第二个参数是一个JS回调函数,用来指示用户点击了哪个button,并打印出一行日志。

RCTAlertManager.alertWithArgs({message:'JS->Native Call',buttons:[{k1:'button1'},{k2:'button1'}]},function {console.log('RCTAlertManager.alertWithArgs() id:' + id +' v:' + v)});

回调函数的调用和直接从Native调用JS是差不多的,再回头看看__nativeCall 函数我们忽略的部分

 __nativeCall(module, method, params, onFail, onSucc) { //Native接口最多支持两个回调 if (onFail || onSucc) { onFail && params.push(this._callbackID); this._callbacks[this._callbackID++] = onFail; onSucc && params.push(this._callbackID); this._callbacks[this._callbackID++] = onSucc; } this._queue[MODULE_IDS].push; this._queue[METHOD_IDS].push; this._queue[PARAMS].push; global.nativeFlushQueueImmediate(this._queue); this._queue = [[],[],[]]; this._lastFlush = now; }

可以看到把onFail,onSucc两个函数类型转化为两个数字ID插入到参数列表后面,并把函数函数缓存起来。从Native调用过来也比较简单了,传递过callbackID到JS,就可以执行到回调函数。

JS传递的参数仅仅是个整形ID,Native如何知道这个ID就是个回调函数呢?

答案是和其他参数一样通过Native的函数签名,如果发现对应的参数是个block,则从JS传递过来的ID就是对应的回调ID,把其转化为RCTResponseSenderBlock的闭包。

RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args callback:(RCTResponseSenderBlock)callback)

到此为止,我们已经把整个JS->Natvie的流程都走通了,梳理一下整个流程。

金沙棋牌官方平台 14调用图.png

总结一下

  1. Native初始化时, Native生成要导出模块的名字列表,仅仅是模块名字列表, ModuleConfig
  1. 在React Native 的JS引擎初始化完成后,向JSContext注入ModuleConfig,赋值到JS全局变量 __fbBatchedBridgeConfig
  2. 还记得那个N->JS大使---JS对象BatchedBridge吗,BatchedBridge创建的时候会用__fbBatchedBridgeConfig变量里Native模块名字列表定义一个同名的JS对象,但是是一个没有任何方法的空对象,只增加了一个获取方法数组的get函数。此时初始化的操作已完成。
  3. 很久很久之后,有人用RCTAlertManager.alertWithArgs 调用了Native的代码,咳咳,这人是我,此时JS去获取RCTAlertManager方法列表时,发现是空的,就调用Native提供的查询函数nativeRequireModuleConfig 获取RCTAlertManager对象的详细的导出信息,并定义成同名的JS函数,此函数转调到OC的实现
  4. 此时RCTAlertManager对应的JS对象才定义完整,JS找到了alertWithArgs函数,每个对应的JS函数都是一个封装了调用__nativeCall的闭包,JS通过此函数转发到Native

可以看出,Native导出的配置表并不是在一开始就完整的注入JS并定义对应的JS对象,而是仅仅注入了一个模块名字,当运行期间有人调用的时候,才再从Native查询调用模块的详细配置表,这种懒加载机制缓解了一个大型的app导出的Api很多,全部导入JS导致初始化时内存占用过大的问题。

2、配置native module(RCTBatchedBridge moduleConfig)

线程问题

React Native为JS引擎创建了一个独立的线程

//RCTJavaScriptContext- (instancetype)init{ NSThread *javaScriptThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(runRunLoopThread) object:nil]; javaScriptThread.name = @"com.facebook.React.JavaScript"; [javaScriptThread start]; return [self initWithJavaScriptThread:javaScriptThread context:nil];}

所有的JS代码都运行在"com.facebook.React.JavaScript"后台线程中,所有的操作都是异步,不会卡死主线程UI。并且JS调用到Native中的接口中有强制的线程检查,如果不是在React线程中则抛出异常。这样有一个问题,从JS调用Native中的代码是执行在这个后台线程中,我们上文的RCTAlertManager.alertWithArgs明显是个操作UI的接口,执行在后台线程会crash,在导出RCTAlertManager时,通过实现方法- (dispatch_queue_t)methodQueue,原生模块可以指定自己想在哪个队列中被执行

- (dispatch_queue_t)methodQueue{ return dispatch_get_main_queue();}

类似的,如果一个操作需要花费很长时间,原生模块不应该阻塞住,而是应当声明一个用于执行操作的独立队列。举个例子,RCTAsyncLocalStorage模块创建了自己的一个queue,这样它在做一些较慢的磁盘操作的时候就不会阻塞住React本身的消息队列:

- (dispatch_queue_t)methodQueue{ return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);}

React Native通过以下方式可以把native 接口提供给JS调用:

React的消息循环

这是典型的事件驱动机制和消息循环,当无任何事件时,runloop处于睡眠状态,当有事件时,比如用户操作,定时器到时,网络事件等等,触发一此消息循环,最总表现为UI的改变或数据的变化。

金沙棋牌官方平台 15消息循环.png

这里要注意的是,以上我们讲到从 JS调用到Native是调用global.nativeFlushQueueImmediate 立即执行的。React消息循环这里做了一次缓存,比如用户点击一次,所有触发的JS->N的调用都缓存到MessageQueue里,当N->JS调用完成时,以返回值的形式返回MessageQueue, 减少了N->JS的交互次数。缓存时间是 MIN_TIME_BETWEEN_FLUSHES_MS = 5毫秒内的调用。

 __nativeCall(module, method, params, onFail, onSucc) { this._queue[MODULE_IDS].push; this._queue[METHOD_IDS].push; this._queue[PARAMS].push; var now = new Date().getTime(); if (global.nativeFlushQueueImmediate && now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS) { global.nativeFlushQueueImmediate(this._queue); this._queue = [[],[],[]]; this._lastFlush = now; } }

MIN_TIME_BETWEEN_FLUSHES_MS时间内的调用都会缓存到this._queue,以返回值的形式返回给Native,形成一次消息循环

 callFunctionReturnFlushedQueue(module, method, args) { guard => { this.__callFunction(module, method, args); this.__callImmediates; return this.flushedQueue(); } flushedQueue() { this.__callImmediates(); let queue = this._queue; this._queue = [[],[],[]]; return queue[0].length ? queue : null; }

本篇的内容就是这些,想看懂容易,想尽量简洁明了的总结成文字真是一件很不容易的事情,特别是这里很多JS的代码。有问题大家留言指正。下一篇将介绍ReactNative的渲染原理。

1)继承 RCTBridgeModule,如

@interfaceCalendarManager :NSObject

2)调用宏RCT_EXPORT_MODULE() & RCT_EXPORT_METHOD() ,如

RCT_EXPORT_MODULE();RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location){RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);}

其中,RCT_EXPORT_METHOD是把接口封装成 “__rct_export__”+ “__COUNTER__”形式,COUNTER是数字累加值,从0开始。

3)所有的module都保存在RCTModuleMap中,每个module通过遍历method中是否包含__rct_export__前缀来获取公开给JS的接口

unsigned int methodCount;

Method *methods = class_copyMethodList(object_getClass(_moduleClass), &methodCount);

for (unsigned int i = 0; i < methodCount; i++) {

Method method = methods[i];

SEL selector = method_getName(method);

if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) {

IMP imp = method_getImplementation(method);

NSArray *entries = ((NSArray *(*)(id, SEL))imp)(_moduleClass, selector);

id moduleMethod =

[[RCTModuleMethod alloc] initWithObjCMethodName:entries[1]

JSMethodName:entries[0]

moduleClass:_moduleClass];

[moduleMethods addObject:moduleMethod];

}

}

3、配置JavaScript运行环境(RCTJavaScriptExecutor)

在iOS中有两种JS运行环境,分别是UIWebView(iOS7以下)和 JSGlobalContext(ios7及其以上)。在RCTJavaScriptExecutor setup进行创建,

if(!strongSelf->_context) {      JSGlobalContextRef ctx = JSGlobalContextCreate(NULL);      strongSelf->_context =[[RCTJavaScriptContext alloc]initWithJSContext:ctx];

}

4、把API列表注入JS环境中(injectJSONConfiguration)

在moduleConfig中生成一个JSON内容,注入到JS执行环境中。这个JSON内容如下:

“CalendarManager":{"methods":{"addEvent":{"type":"remote","methodID":0},"findEvents":{"type":"remote","methodID":1}},"moduleID":46},"RCTPickerManager":{"constants":{"ComponentWidth":320,"ComponentHeight":216},"moduleID":47,"methods":{}},…. ….

可以看到,每个module都有一个ID对应,如CalendarManager的moduleID是46;每个方法也有一个methodID对应。

二、通信机制

在网上看到一个交互图,基本描述整个过程是如何进展的:

金沙棋牌官方平台 16

JS和OC之间、以及各个组件之间交互都是通过RCTEventDispatcher来传递的。 以点击为例:

1、在RCTTouchHandler模块,把参数封装后传递[RCTBridge enqueueJSCall:args:]在JSThread中执行。

金沙棋牌官方平台 17

具体是存储在RCTBatchedBridge的_scheduledCalls中,然后触发[RCTBatchedBridge updateJSDisplayLinkState],由CADisplayLink的回调方法[RCTBatchedBridge_jsThreadUpdate]执行_scheduledCalls中所有操作。

2、经过处理后,会回调[RCTBathedBridge _handleBuffer],把参数buffer转成module名称、方法名和参数,并在该module所对应的queue进行回调处理。

idmethod= moduleData.methods[methodID];  @try{    [methodinvokeWithBridge:self module:moduleData.instance arguments:params];  }

3、如果要回调JS,则通过[RCTEventDispatcher sendAppEventWithName:body:]发送消息。

四、多线程的作用

1、JavascriptThread:”com.facebook.React.JavaScript”, 与RCTContextExecutor关联,JS方法的处理和调用都在该线程处理。

2、RCTBridgeQueue:”com.facebook.react.RCTBridgeQueue”,处理OC对JS提供的接口,如遍历接口、封装在moduleData中等。

3、Module Queue:每个module都有一个queue,串行执行。不过为什么要在每个module自己的queue来执行?

4、Shadow Queue: ”com.facebook.React.ShadowQueue”,主要处理RCTUIManager相关业务;

五、框架可能的问题

1、启动耗时&内存:RN是把所有js打成一个bundle,一次加载并执行,所以对启动耗时和内存都会有影响。

2、JSContext的消耗问题:看到网上有人提到JSContext消耗比较大,还需要继续研究。

六、有待研究的内容

1、CADisplayLink在RN(React Native)是如何处理业务的;

2、JS参数到Native API参数的转换;

3、扩展能力:JS如何调用RN没有的组件?

参考:

http://tadeuzagallo.com/blog/react-native-bridge/

http://blog.cnbang.net/tech/2698/

本文由金沙棋牌发布于金沙棋牌官方平台,转载请注明出处:构建跨平台的原生应用,通讯及消息循环代码剖

关键词: