React Native通信机制详解

2015-3-30

React Native是facebook刚开源的框架,可以用javascript直接开发原生APP,先不说这个框架后续是否能得到大众认可,单从源码来说,这个框架源码里有非常多的设计思想和实现方式值得学习,本篇先来看看它最基础的JavaScript-ObjectC通信机制(以下简称JS/OC)。

概览

React Native用iOS自带的JavaScriptCore作为JS的解析引擎,但并没有用到JavaScriptCore提供的一些可以让JS与OC互调的特性,而是自己实现了一套机制,这套机制可以通用于所有JS引擎上,在没有JavaScriptCore的情况下也可以用webview代替,实际上项目里就已经有了用webview作为解析引擎的实现,应该是用于兼容iOS7以下没有JavascriptCore的版本。

普通的JS-OC通信实际上很简单,OC向JS传信息有现成的接口,像webview提供的-stringByEvaluatingJavaScriptFromString方法可以直接在当前context上执行一段JS脚本,并且可以获取执行后的返回值,这个返回值就相当于JS向OC传递信息。React Native也是以此为基础,通过各种手段,实现了在OC定义一个模块方法,JS可以直接调用这个模块方法并还可以无缝衔接回调。

举个例子,OC定义了一个模块RCTSQLManager,里面有个方法-query:successCallback:,JS可以直接调用RCTSQLManager.query并通过回调获取执行结果。:

//OC:
@implement RCTSQLManager
- (void)query:(NSString *)queryData successCallback:(RCTResponseSenderBlOCk)responseSender
{
     RCT_EXPORT();
     NSString *ret = @"ret"
     responseSender(ret);
}
@end
//JS:
RCTSQLManager.query("SELECT * FROM table", function(result) {
     //result == "ret";
});

接下来看看它是怎样实现的。

模块配置表

首先OC要告诉JS它有什么模块,模块里有什么方法,JS才知道有这些方法后才有可能去调用这些方法。这里的实现是OC生成一份模块配置表传给JS,配置表里包括了所有模块和模块里方法的信息。例:

{
    "remoteModuleConfig": {
        "RCTSQLManager": {
            "methods": {
                "query": {
                    "type": "remote",
                    "methodID": 0
                }
            },
            "moduleID": 4
        },
        ...
     },
}

OC端和JS端分别各有一个bridge,两个bridge都保存了同样一份模块配置表,JS调用OC模块方法时,通过bridge里的配置表把模块方法转为模块ID和方法ID传给OC,OC通过bridge的模块配置表找到对应的方法执行之,以上述代码为例,流程大概是这样(先不考虑callback):

ReactNative1

在了解这个调用流程之前,我们先来看看OC的模块配置表式怎么来的。我们在新建一个OC模块时,JS和OC都不需要为新的模块手动去某个地方添加一些配置,模块配置表是自动生成的,只要项目里有一个模块,就会把这个模块加到配置表上,那这个模块配置表是怎样自动生成的呢?分两个步骤:

1.取所有模块类

每个模块类都实现了RCTBridgeModule接口,可以通过runtime接口objc_getClassList或objc_copyClassList取出项目里所有类,然后逐个判断是否实现了RCTBridgeModule接口,就可以找到所有模块类,实现在RCTBridgeModuleClassesByModuleID()方法里。

2.取模块里暴露给JS的方法

一个模块里可以有很多方法,一些是可以暴露给JS直接调用的,一些是私有的不想暴露给JS,怎样做到提取这些暴露的方法呢?我能想到的方法是对要暴露的方法名制定一些规则,比如用RCTExport_作为前缀,然后用runtime方法class_getInstanceMethod取出所有方法名字,提取以RCTExport_为前缀的方法,但这样做恶心的地方是每个方法必须加前缀。React Native用了另一种黑魔法似的方法解决这个问题:编译属性__attribute__。

在上述例子中我们看到模块方法里有句代码:RCT_EXPORT(),模块里的方法加上这个宏就可以实现暴露给JS,无需其他规则,那这个宏做了什么呢?来看看它的定义:

#define RCT_EXPORT(JS_name) __attribute__((used, section("__DATA,RCTExport" \
))) static const char *__rct_export_entry__[] = { __func__, #JS_name }

这个宏的作用是用编译属性__attribute__给二进制文件新建一个section,属于__DATA数据段,名字为RCTExport,并在这个段里加入当前方法名。编译器在编译时会找到__attribute__进行处理,为生成的可执行文件加入相应的内容。效果可以从linkmap看出来:

# Sections:
# Address Size Segment Section
0x100001670 0x000C0180 __TEXT __text
...
0x10011EFA0 0x00000330 __DATA RCTExport
0x10011F2D0 0x00000010 __DATA __common
0x10011F2E0 0x000003B8 __DATA __bss
...

0x10011EFA0 0x00000010 [ 4] -[RCTStatusBarManager setStyle:animated:].__rct_export_entry__
0x10011EFB0 0x00000010 [ 4] -[RCTStatusBarManager setHidden:withAnimation:].__rct_export_entry__
0x10011EFC0 0x00000010 [ 5] -[RCTSourceCode getScriptText:failureCallback:].__rct_export_entry__
0x10011EFD0 0x00000010 [ 7] -[RCTAlertManager alertWithArgs:callback:].__rct_export_entry__
...

可以看到可执行文件数据段多了个RCTExport段,内容就是各个要暴露给JS的方法。这些内容是可以在运行时获取到的,在RCTBridge.m的RCTExportedMethodsByModuleID()方法里获取这些内容,提取每个方法的类名和方法名,就完成了提取模块里暴露给JS方法的工作。

整体的模块类/方法提取实现在RCTRemoteModulesConfig()方法里。

调用流程

接下来看看JS调用OC模块方法的详细流程,包括callback回调。这时需要细化一下上述的调用流程图:

ReactNative2

看起来有点复杂,不过一步步说明,应该很容易弄清楚整个流程,图中每个流程都标了序号,从发起调用到执行回调总共有11个步骤,详细说明下这些步骤:

1.JS端调用某个OC模块暴露出来的方法。

2.把上一步的调用分解为ModuleName,MethodName,arguments,再扔给MessageQueue处理。

在初始化时模块配置表上的每一个模块都生成了对应的remoteModule对象,对象里也生成了跟模块配置表里一一对应的方法,这些方法里可以拿到自身的模块名,方法名,并对callback进行一些处理,再移交给MessageQueue。具体实现在BatchedBridgeFactory.js的_createBridgedModule里,整个实现区区24行代码,感受下JS的魔力吧。

3.在这一步把JS的callback函数缓存在MessageQueue的一个成员变量里,用CallbackID代表callback。在通过保存在MessageQueue的模块配置表把上一步传进来的ModuleName和MethodName转为ModuleID和MethodID。

4.把上述步骤得到的ModuleID,MethodId,CallbackID和其他参数argus传给OC。至于具体是怎么传的,后面再说。

5.OC接收到消息,通过模块配置表拿到对应的模块和方法。

实际上模块配置表已经经过处理了,跟JS一样,在初始化时OC也对模块配置表上的每一个模块生成了对应的实例并缓存起来,模块上的每一个方法也都生成了对应的RCTModuleMethod对象,这里通过ModuleID和MethodID取到对应的Module实例和RCTModuleMethod实例进行调用。具体实现在_handleRequestNumber:moduleID:methodID:params:。

6.RCTModuleMethod对JS传过来的每一个参数进行处理。

RCTModuleMethod可以拿到OC要调用的目标方法的每个参数类型,处理JS类型到目标类型的转换,所有JS传过来的数字都是NSNumber,这里会转成对应的int/long/double等类型,更重要的是会为block类型参数的生成一个block。

例如-(void)select:(int)index response:(RCTResponseSenderBlock)callback 这个方法,拿到两个参数的类型为int,block,JS传过来的两个参数类型是NSNumber,NSString(CallbackID),这时会把NSNumber转为int,NSString(CallbackID)转为一个block,block的内容是把回调的值和CallbackID传回给JS。

这些参数组装完毕后,通过NSInvocation动态调用相应的OC模块方法。

7.OC模块方法调用完,执行block回调。

8.调用到第6步说明的RCTModuleMethod生成的block。

9.block里带着CallbackID和block传过来的参数去调JS里MessageQueue的方法invokeCallbackAndReturnFlushedQueue。

10.MessageQueue通过CallbackID找到相应的JS callback方法。

11.调用callback方法,并把OC带过来的参数一起传过去,完成回调。

整个流程就是这样,简单概括下,差不多就是:JS函数调用转ModuleID/MethodID -> callback转CallbackID -> OC根据ID拿到方法 -> 处理参数 -> 调用OC方法 -> 回调CallbackID -> JS通过CallbackID拿到callback执行

事件响应

上述第4步留下一个问题,JS是怎样把数据传给OC,让OC去调相应方法的?

答案是通过返回值。JS不会主动传递数据给OC,在调OC方法时,会在上述第4步把ModuleID,MethodID等数据加到一个队列里,等OC过来调JS的任意方法时,再把这个队列返回给OC,此时OC再执行这个队列里要调用的方法。

一开始不明白,设计成JS无法直接调用OC,需要在OC去调JS时才通过返回值触发调用,整个程序还能跑得通吗。后来想想纯native开发里的事件响应机制,就有点理解了。native开发里,什么时候会执行代码?只在有事件触发的时候,这个事件可以是启动事件,触摸事件,timer事件,系统事件,回调事件。而在React Native里,这些事件发生时OC都会调用JS相应的模块方法去处理,处理完这些事件后再执行JS想让OC执行的方法,而没有事件发生的时候,是不会执行任何代码的,这跟native开发里事件响应机制是一致的。

说到OC调用JS,再补充一下,实际上模块配置表除了有上述OC的模块remoteModules外,还保存了JS模块localModules,OC调JS某些模块的方法时,也是通过传递ModuleID和MethodID去调用的,都会走到-enqueueJSCall:args:方法把两个ID和参数传给JS的BatchedBridge.callFunctionReturnFlushedQueue,跟JS调OC原理差不多,就不再赘述了。

总结

整个React Native的JS-OC通信机制大致就是这样了,关键点在于:模块化,模块配置表,传递ID,封装调用,事件响应,其设计思想和实现方法很值得学习借鉴。

评论

2015年3月30日 12:19

请问楼主的图是用什么软件做的?

2015年3月30日 13:41

keynote

2015年3月31日 16:30

赞,出手很快。

2015年3月31日 17:23

请问在MessageQueue.js中,js是利用什么机制将moduleID,methodID等参数通过javascriptcore传回给OC的

2015年3月31日 18:47

请看“事件响应”那一段

2015年4月1日 9:47

明白了,这种做法可以很容易的实现兼容并且结构上也很清晰

[…] React Native通信机制详解 原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0 […]

2015年4月2日 13:14

好文!虽然自己不懂OC的代码但是读了之后起码知道自己的代码是怎么被执行的了,感谢!

这个机制之前有没有在其他不同语言间的通信中实践过? 如果确实是比较好的话,是不是可以用来做更多的类似跨平台跨语言间的通讯?

2015年4月3日 17:23

要实现这个机制需要语言有动态反射的特性,即可以通过类/方法名字符串找到对应的类/方法进行调用,没有这特性就做不了。

2015年4月3日 14:02

厉害!

[…] 《React Native 通信机制详解》:React Native 是 facebook 刚开源的框架,可以用 javascript 直接开发原生 APP,先不说这个框架后续是否能得到大众认可,单从源码来说,这个框架源码里有非常多的设计思想和实现方式值得学习,本文介绍了它最基础的 JavaScript-ObjectC 通信机制。 […]

[…] 《React Native 通信机制详解》:React Native 是 facebook 刚开源的框架,可以用 javascript 直接开发原生 APP,先不说这个框架后续是否能得到大众认可,单从源码来说,这个框架源码里有非常多的设计思想和实现方式值得学习,本文介绍了它最基础的 JavaScript-ObjectC 通信机制。 […]

2015年4月12日 22:53

解释的很详细,赞一个!

[…] React Native通信机制详解 http://blog.cnbang.net/tech/2698/ […]

2015年4月14日 19:04

不明觉厉

2015年4月14日 20:34

这个通讯架构, 很久以前github上就有开源的实现啊, js-bridge
FB做的就是那个通过attribute来暴露方法而已

2015年4月14日 22:55

是指哪个js-bridge?求链接

2015年4月15日 1:40

https://github.com/newyankeecodeshop/GAJavaScript?files=1
在手机上翻了一个多小时终于翻出来了。我在之前的项目中用了这个库,但当时没全用,类名文件名都改掉了

2015年4月15日 1:41

因此记不住原来的库叫啥了。
它这个跟fb不同的是它运行在uiwebview上,仅仅是一个通讯框架。但是比其它jsbridge要完善强大。好多思路和fb的一样,因此看了你上边的介绍我立马就有印象了。

2015年4月15日 8:45

感谢翻出,大概浏览了下,好像通信机制跟react native的实现关系不大

2015年4月15日 1:42

手机上回复的,发重了。。。

2015年4月15日 11:15

> 感谢翻出,大概浏览了下,好像通信机制跟react native的实现关系不大

他的机制, 跟你上边那个图里边的11个步骤几乎是一样啊, 我是看了你那个图就想起这个库.

2015年4月15日 11:17

哦,,, 我的意思, 不是说FB抄袭了它, 或者直接用他的实现.

我是说, 他们两者的思想很像.

[…] React Native通信机制详解 http://blog.cnbang.net/tech/2698/ […]

2015年5月10日 15:47

感谢分享!感觉全文有个地方不解,就指导。最后说,js如何调用oc。也就是说,触发的事js事件,如何把这个事件传递到ocne?全文好像没有解释这一块。理解题主的意思,好像有循环机制去轮询js的方法么,拿到message Queue?

2015年11月6日 19:51

我一开始也没想通,但是React Native框架跟传统的Hybird是不一样的,包括界面响应都在OC端完成的,而且在JS里面没有线程概念,所以只在有事件触发的时候再去找JS要队列并没有什么关系。

[…] JS/OC不会频繁通信,会在事件触发时批量传递,提高效率。(详见React Native通信机制详解) […]

[…] JS/OC不会频繁通信,会在事件触发时批量传递,提高效率。(详见React Native通信机制详解) […]

2015年6月4日 11:20

[…] React Native通信机制详解 http://blog.cnbang.net/tech/2698/ […]

2015年6月18日 12:37

感謝樓主分享好文章
剛好這幾天把一直沒認真看的 React Native 文件看完
想說來回頭看樓主大大的文,看一下 React Native 裡面怎麼做的,發現有些做法好像變了
從 __DATA section 裡取東西的奇技淫巧已經改用 ObjC runtime 提供的方式
具體是從這個 commit 變更的 https://github.com/sahrens/react-native/commit/e9095b2f4277918c7b62d2e3dab3f77e79ca4776

參考一下,感謝

2015年6月19日 8:00

感谢知会~

2015年6月22日 21:34

通信的大体机制同WebViewJavascriptBridge比较像。但是WebViewJavascriptBridge无法有效的指定要获取的数据来源。React Native直接通过模块和方法来指定数据来源,使用起来会比WebViewJavascriptBridge更方便。

2015年9月25日 18:13

很棒!

2015年10月7日 11:35

[…] React Native通信机制详解 http://blog.cnbang.net/tech/2698/ […]

2015年10月12日 10:22

楼主讲得很详细,赞!能不能把js部分详细的说一下呢,还有就是目前出了安卓的部分,不知道安卓的实现和ios有什么区别呢?楼主能不能对比一下安卓和ios的原理呢?

[…] objective c和javascript的之间的通信是通过webview提供的一系列接口实现,oc定义了一系列的模块供javascript调用,定义好的模块通过一个模块配置表提供给javascript。流程简单概括为:JS函数调用转ModuleID/MethodID -> callback转CallbackID -> OC根据ID拿到方法 -> 处理参数 -> 调用OC方法 -> 回调CallbackID -> JS通过CallbackID拿到callback执行,具体学习参见:http://blog.cnbang.net/tech/2698/ […]

2015年10月28日 17:26

居然百度react-native乱入了bang的blog,牛逼啊。。

[…] 收藏一篇文章:转载自bang’s blog […]

2015年12月23日 18:41

[…] React Native通信机制详解 http://blog.cnbang.net/tech/2698/ […]

2016年1月4日 22:13

[…] React Native通信机制:网址:http://blog.cnbang.net/tech/2698/ […]

[…] React Native通信机制详解 http://blog.cnbang.net/tech/2698/ […]

[…] React Native通信机制详解 […]

2016年1月23日 20:43

[…] 1. 《React Native通信机制详解》 […]

[…] React Native通信机制详解 http://blog.cnbang.net/tech/2698/ […]

2016年3月17日 10:03

最新的ReactNative 实现 已经没有这样的 RCT_EXPORT 这样的黑魔法了
而是直接通过RCT_EXTERN_REMAP_METHOD 宏生成对应方法的映射元组. 然后再通过方法前缀获得方法映射表.再获得实际要公开的方法的签名及实现.

2016年3月21日 22:47

[…] React Native通信机制详解 http://blog.cnbang.net/tech/2698/ […]

2016年3月30日 11:27

新手请教。一个实现蓝牙的oc类有很多代理回调的方法,用来实时接收蓝牙返回的数据,请问如何把这些数据传到 js 上?

[…] React Native通信机制详解 […]

[…] JS/OC不会频繁通信,会在事件触发时批量传递,提高效率。(详见React Native通信机制详解) […]

2016年5月31日 13:25

[…] React Native通信机制详解 http://blog.cnbang.net/tech/2698/ […]

2016年6月13日 18:43

为什么不直接用moduleName,methodName呢,要用id来做中转?

2016年8月21日 10:25

为了防止名称冲突吧

[…] React Native通信机制详解 http://blog.cnbang.net/tech/2698/ […]

2016年8月11日 19:25

[…] React Native通信机制详解 http://blog.cnbang.net/tech/2698/ […]

2016年8月17日 14:56

看完大有益处 ?

2016年8月29日 16:33

看完后突然想了下,RN的思路和国人开发的JSPatch感觉有点类似?但好像又不太相似。。。。蒙逼了,?

2016年9月2日 15:47

JSPatch就是博主写的。。。?

[…] 提到 Objective-C 与 JavaScript 的交互,不得不推荐 bang神的这篇文章:React Native通信机制详解 。虽然其中不少细节都已经过时,但是整体的思路值得学习。 […]

[…] React Native通信机制详解 http://blog.cnbang.net/tech/2698/ […]

[…] 提到 Objective-C 与 JavaScript 的交互,不得不推荐 bang神的这篇文章:React Native通信机制详解 。虽然其中不少细节都已经过时,但是整体的思路值得学习。 […]

2017年3月7日 18:08

[…] 提到 Objective-C 与 JavaScript 的交互,不得不推荐 bang神的这篇文章:React Native通信机制详解 。虽然其中不少细节都已经过时,但是整体的思路值得学习。 […]

2017年3月13日 1:02

[…] React Native通信机制详解http://blog.cnbang.net/tech/2698/ […]

[…] 提到 Objective-C 与 JavaScript 的交互,不得不推荐 bang神的这篇文章:React Native通信机制详解 。虽然其中不少细节都已经过时,但是整体的思路值得学习。 […]

2017年6月18日 19:43

[…] React Native通信机制详解 http://blog.cnbang.net/tech/2698/ […]

[…] React Native通訊機制詳解:http://blog.cnbang.net/tech/2698/ […]

[…] React Native通信机制详解:http://blog.cnbang.net/tech/2698/ […]

[…] 參考文章:http://www.tuicool.com/articles/yEnQzu6參考文章:http://blog.cnbang.net/tech/2698/ […]

2018年9月11日 15:41

虽然过去这么久了,但是还是想请教楼主,RN里面是如何实现 Promise 协议的呢?
或者说,RN里面是如何返回一个 Promise Obj 给 js 端的呢?
希望楼主能够指点一下

[…] React Native通信机制详解 http://blog.cnbang.net/tech/2698/ […]

[…] 参考文章:http://www.tuicool.com/articles/yEnQzu6参考文章:http://blog.cnbang.net/tech/2698/ […]

[…] React Native通信机制详解 http://blog.cnbang.net/tech/2… […]

[…] React Native通信机制详解:http://blog.cnbang.net/tech/2698/ […]

[…] React Native通信机制详解 http://blog.cnbang.net/tech/2… […]

[…] React Native 通信机制详解 – bang’s blog […]

2019年10月6日 10:55

[…] React Native通信机制详解 […]

[…] React Native通信机制详解http://blog.cnbang.net/tech/2698/ […]

[…] React Native 通信机制详解 – bang’s blog […]

[…] React Native通信机制详解 http://blog.cnbang.net/tech/2698/ […]

[…] Virtual DOM:相对Browser环境下的DOM(文档对象模型)而言,Virtual DOM是DOM在内存中的一种轻量级表达方式(原话是lightweight representation of the document),可以通过不同的渲染引擎生成不同平台下的UI,JS和Native之间通过Bridge通信。 […]