react native js 与 native 的通信与交互方式

JS 的启动过程

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

问题是这个命令行是怎么启动起来的呢?实际上,Xcode 在 Build Phase 的最后一个阶段对此做了配置,其实就是增加了一个 sh 脚本,让小葱的在编译会自动去执行这个脚本,打开 npm,相当于你直接手动命令行切到 react-native 目录下,执行 npm start 命令的效果是一样的:

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

查看这个脚本中的内容,发现它主要是读取 XCode 带过来的环境变量,同时加载 npm 包使得 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"

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 时用户及时得到响应,由于我们公司的项目都比较大,一般都有十几个 RN 的模块,打开 RN 调试模式, commond + R 就像刷新浏览器页面一样,手机页面就会刷新,这是你会在手机顶部看到 localhost:8081/xxx/xxx/xxx ,因为这是调试模式,是打开 chrome 去运行 js 环境的,所以有些很多测试环境的关于 websocket 的 bug 你一般都可以不用解,在生产环境下是不会遇到这种 bug 的.

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

Native 启动过程

Native 端就是一个 iOS 程序,程序入口是 main 函数,像通常一样,它负责对应用程序做初始化,记住一定要仔细看appdelegate,任何应用他都是相当于桥梁一样的作用.

除了 main 函数之外, AppDelegate 也是一个比较重要的类,它主要用于做一些全局的控制。在应用程序启动之后,其中的 didFinishLaunchingWithOptions 方法会被调用,在这个方法中,主要做了几件事:

  • 定义了 JS 代码所在的位置,它在 dev 环境下是一个 URL,通过 development server 访问;在生产环境下则从磁盘读取,当然前提是已经手动生成过了 bundle 文件,这个 bundle 文件在你下载软件的时候已经下载下来了,他每次都会检查是否有新的 bundle 包更新,如果有就会从服务器下载最新的 bundle 包,这个不需要重新上传 appstore,所以可以做热更新之类的功能.
  • 创建了一个 RCTRootView 对象,该类继承于 UIView ,处于程序所有 View 的最外层;
  • 调用 RCTRootView 的 initWithBundleURL 方法。在该方法中,创建了 bridge 对象。顾名思义,bridge 起着两个端之间的桥接作用,其中真正工作的是类就是大名鼎鼎的 RCTBatchedBridge 。

RCTBatchedBridge 是初始化时通信的核心,我们重点关注的是 start 方法。在 start 方法中,会创建一个 GCD 线程,该线程通过串行队列调度了以下几个关键的任务。

loadSource

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

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

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 的方法:

RCTUIManager.measureLayoutRelativeToParent(   React.findNodeHandle(scrollComponent),   logError,   this._setScrollVisibleLength);

所有 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 时,顺便把之前保存的 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];  @try {    // 完成调用    [method invokeWithBridge:self module:moduleData.instance arguments:params];  }  @catch (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通信机制详解 一文中的“事件响应”一节。

总结

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

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

时间: 2024-12-06 16:57:03

react native js 与 native 的通信与交互方式的相关文章

【REACT NATIVE 系列教程之十二】REACT NATIVE(JS/ES)与IOS(OBJECT-C)交互通信

一用到跨平台的引擎必然要有引擎与各平台原生进行交互通信的需要.那么Himi先讲解React Native与iOS之间的通信交互. 本篇主要分为两部分讲解:(关于其中讲解的OC语法等不介绍,不懂的请自行学习) 1. React Native 访问iOS 2. iOS访问React Native     一:React Native 访问iOS 1. 我们想要JS调用OC函数,就要实现一个"RCTBridgeModule"协议的Objective-C类 所以首先我们先创建一个oc新类,  

iOS Js与native相互通信

js与navive相互通信的机制 js –> native 目前,截止至iOS7,iOS原生并没有提供js直接调用native的方式,只能通过UIWebView相关的UIWebViewDelegate协议的 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType 方法来

JavaScript强化教程——Native.js示例汇总

本文为 H5EDU 机构官方 HTML5培训 教程,主要介绍:JavaScript强化教程 -- Native.js示例汇总 Native.js虽然强大和开放,但很多web开发者因为不熟悉原生API而难以独立完成. 这篇帖子的目的就是汇总各种写好的NJS代码,方便web开发者. 众人拾柴火焰高,有能力的开发者多多提交NJS代码,大家都会给你点赞的, Android平台 在桌面创建和删除App快捷方式 见Hello H5+里Native.js部分演示及源码. 或在这里搜索"快捷方式",h

iOS JS 和 OC交互 / JS 和 native 相互调用

现在app 上越来越多需求是通过UIWebView 来展示html 或者 html5的内容, js 和 native OC代码交互 就非常常见了. js 调用 native  OC代码 第一种机制 (1)最常用的是 利用 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationT

H5+ 移动app学习之二 Native.js

Native.js技术,简称NJS,是一种将手机操作系统的原生对象转义,映射为JS对象,在JS里编写原生代码的技术.如果说Node.js把js扩展到服务器世界,那么Native.js则把js扩展到手机App的原生世界.Native.js不是一个js库,不需要下载引入到页面的script中,也不像node.js那样有单独的运行环境,Native.js的运行环境是集成在5+runtime里的. Native.js for Android封装一条通过JS语法直接调用Native Java接口通道,通过

js与native的交互

WebView与Javascript交互(Android): WebView与Javascript交互是双向的数据传递,1.H5网页的JS函数调用Native函数 2.Native函数调用JS函数,具体实现以下面例子为主: 1.)mainfest.xml中加入网络权限 <uses-permission android:name="android.permission.INTERNET"/> 2.)WebView开启支持JavaScript mWebView.getSetti

Electron + React + Node.js + ES6 开发本地 App

Electron + React + Node.js + ES6 开发本地 App 1.概述 近来工作上需要做一款 PC 上的软件,这款软件大体来讲是类似 PPT 的一款课件制作软件.由于我最近几年专注于移动 App 的开发,对 PC 端开发的了解有些滞后.所以我首先需要看看,在 PC 上采用什么框架能够顺利完成我的工作. 我的目标是,在完成这款软件的同时能够顺便学习一下比较流行的技术.在经过前期技术调研后,我明确了实现这款软件所需要的技术条件: 不采用 C++ 方面的类库,比如 MFC.Qt.

【React源码分析】组件通信、refs、key和ReactDOM

React源码系列文章,请多支持:React源码分析1 - 组件和对象的创建(createClass,createElement)React源码分析2 - React组件插入DOM流程React源码分析3 - React生命周期详解React源码分析4 - setState机制React源码分析5 -- 组件通信,refs,key,ReactDOMReact源码分析6 - React合成事件系统 1 组件间通信 父组件向子组件通信 React规定了明确的单向数据流,利用props将数据从父组件传

React子组件和父组件通信

React子组件和父组件通信包括以下几个方面: 子组件获取父组件属性:props或者state 子组件调用父组件的方法 父组件获取子组件的属性:props或者state 父组件调用子组件的方法 我们从下面这个例子来详细了解: 1 var Father=React.createClass({ 2 getDefaultProps:function(){ 3 return { 4 name:"父组件" 5 } 6 }, 7 MakeMoney:function(){ // 挣钱,供子组件调用