自从Facebook提出了react之后,这个框架的关注度一直居高不下,它所引入的一些东西还是值得学习,比如组件化的开发方式,virtual dom的性能提升方式等,最近为了改进现有的跨平台方案也在研究react,在这边也做下相关的记录。
pre
在开始使用react之前我们需要搭建相应的环境,这个就不在探讨了,具体可以查看官方文档,由于react需要使用javascript语言,所以可能需要去简单了解一下相关的语法(比如:ES6 标准入门),另外iOS在7.0之后引入了javascriptcore框架,极大的方便了js跟oc之间de通信,之前有一篇博客简单的介绍了javascriptcore,有兴趣的也可以去了解一下。
整体框架
react的整体示意图可以用下面图表示,我们所编写的js代码可以在各个平台上运行,这让我们有web的开发效率的同时又有了原生应用的体验。
不过这里面包含的东西对于有点多,尤其很多web相关的东西对于没接触过的人还是有些难度的,要想快速的研究透彻可能不太现实,至少对于我是这样的,因此我们首先研究一下react中js跟native之间的通信方法,其它的有待后面在分析。
假设你已经搭建好相关环境,通过下面的命令创建一个新工程:1
react-native init TestDemo
运行xcodeproj工程,Xcode在编译完后会执行打包脚本,将js文件都打包到一个main.jsbundle文件里,我们可以选择将该文件放到服务器上或者应用内部,如果放到服务器上需要先下载该文件,接着加载执行该文件可以看到demo页面了。
通信过程
所谓的通信其实就是js和oc两者如何相互调用传参等,为了更方便的揭示两者的通信过程,我们可以设置messagequeue.js文件中的SPY_MODE标志为true:1
2
3//MessageQueue.js,需要处于dev模式
//http://localhost:8081/index.ios.bundle?platform=ios&dev=true为true
let SPY_MODE = true; //
现在重新reload js你就可以看到如下的日志输出,下面的日志可以比较直观的揭示两者的调用方式,’JS->N’即JS调用native代码,反之亦然。可以看到程序一开始native会调用js的RCTDeviceEventEmitter.emit方法,分别发送’appStateDidChange’ 和’networkStatusDidChange’两个事件,接着调用js的AppRegistry.runApplication方法启动js应用,然后js层就可以通过native提供的方法来 RCTUIManager.createView来创建视图了。1
2
3
4
5
6
7
8
9
10N->JS : RCTDeviceEventEmitter.emit(["appStateDidChange",{"app_state":"active"}])
N->JS : RCTDeviceEventEmitter.emit(["networkStatusDidChange",{"network_info":"wifi"}])
N->JS : AppRegistry.runApplication(["TestDemo",{"rootTag":1,"initialProps":{}}])
Running application "TestDemo" with appParams: {"rootTag":1,"initialProps":{}}. __DEV__ === true, development-level warning are ON, performance optimizations are OFF
JS->N : RCTUIManager.createView([2,"RCTView",1,{"flex":1}])
JS->N : RCTUIManager.createView([3,"RCTView",1,{"flex":1}])
JS->N : RCTUIManager.createView([4,"RCTView",1,{"flex":1,"justifyContent":"center","alignItems":"center","backgroundColor":4294311167}])
S->N : RCTUIManager.createView([5,"RCTText",1,{"fontSize":20,"textAlign":"center","margin":10,"accessible":true,"allowFontScaling":true}])
JS->N : RCTUIManager.createView([6,"RCTRawText",1,{"text":"Welcome to React Native!"}])
JS->N : RCTUIManager.setChildren([5,[6]])
JS->Native,JS调用Native
让我们先来看看native如何创建一个模块然后暴露给js层调用的,具体的可以参考官方文档,我们这里举个简单的🌰,创建一个MyModule模块:
1 | @interface MyModule : NSObject <RCTBridgeModule> |
我们先来看看这上面的两个宏定义:
- RCT_EXPORT_MODULE()
在native层创建的模块需要通过这个宏定义将该模块暴露给js,该宏定义的具体实现也很简单,如下:
1 | #define RCT_EXPORT_MODULE(js_name) \ |
首先它将RCTRegisterModule这个函数定义为extern,这样该函数的实现对编译器不可见,但会在链接的时候可以获取到;同时声明一个moduleName函数,该函数返回该模块的js名称,如果你没有指定,默认使用类名;最后声明一个load函数(当应用载入后会加载所有的类,load函数在类初始化加载的时候就调用),然后调用RCTRegisterModule函数注册该模块,该模块会被注册添加到一个全局的数组RCTModuleClasses中。
- RCT_EXPORT_METHOD()
要暴露给js调用的API接口需要通过该宏定义声明,该宏定义会额外创建一个函数,形式如下:
1 | + (NSArray *)__rct_export__230 |
该函数名称以 rct_export 开头,同时加上该函数所在的代码行数,该函数返回一个包含可选的js名称以及一个函数签名的数组,他们的作用后面会说到。
- RCTBatchedBridge
为了桥接js跟native,native层引入了RCTBridge这个类负责双方的通信,不过真正起作用的是RCTBatchedBridge这个类,这个类应该算是比较重要的一个类了,让我们来看看这个类主要做啥事情:
1 | //RCTBatchedBridge.m |
简单解释一下其中几个步骤的具体内容:
initModules
1 | - (void)initModules |
当创建完模块的实例对象之后,会将该实例保存到一个RCTModuleData对象中,RCTModuleData里包含模块的类名,名称,方法列表,实例对象、该模块代码执行的队列以及配置信息等,js层就是根据这个对象来查询该模块的相关信息。
setUpExecutor
reactnative的js引擎在初始化的时候会创建一个新的线程,该线程的优先级跟主线层的优先级一样,同时创建一个runloop,这样线程才能循环执行不会退出。所以执行js代码不会影响到主线程,而且RCTJSCExecutor使用的是JavaScriptCore框架,所以react只支持iOS7及以上的版本。
1 | //RCTJSCExecutor.m |
可以看到setup的时候会注册几个方法到js的上下文中供后面js调用,比如’nativeFlushQueueImmediate’ 和 ‘nativeRequireModuleConfig’方法等,当js调用相应方法时会执行对应的block,javascriptcore框架会负责js function和block的转换。
moduleConfig
1 | - (NSString *)moduleConfig |
从实现可以看出仅仅该过程是将模块的名称保存到一个数组中,然后生成一个json字符串的配置信息,包含所有的模块名称,类似如下:1
{"remoteModuleConfig":[["MyModule"],["RCTStatusBarManager"],["RCTSourceCode"],["RCTAlertManager"],["RCTExceptionsManager"],...]}
injectJSONConfiguration
1 | - (void)injectJSONConfiguration:(NSString *)configJSON |
当我们生成配置信息之后,通过上面的函数将该json信息保存到js的全局对象__fbBatchedBridgeConfig中,这样js层就可以知道我们提供了哪些模块,不过细心的话你可能会发现给js的信息只有这些模块的名称,那js怎么调用native的方法的,其实这是react为了懒加载而采用的方式,具体我们下面说明。
当我们配置好native模块后,js层要想调用该native模块的方法如下示例:1
2var myModule = require('react-native').NativeModules.MyModule;
myModule.addEvent('Birthday Party', '4 Privet Drive, Surrey');
可以看出native模块是保存在NativeModules中,所以让我们到NativeModules.js文件中看看:
1 | const BatchedBridge = require('BatchedBridge'); |
从上面的代码可以看出,假如你在js层没有使用到native模块,那么这些模块是不会加载到js层的,只有使用到了该模块,react才会去获取该模块的具体配置信息然后加载到js,这是react懒加载的一个方式,让我们能够节约内存,让我们看看如何获取模块的配置信息:
1 | //只会导出有__rct_export__前缀的方法,也就是之前RCT_EXPORT_METHOD这个宏定义提到的 |
导出的配置信息如下所示,可以看到config里包含的模块名称,导出的常量以及导出的函数等,推荐通过调试工具React Developer Tools打断点来查看:
- BatchedBridge
上面我们说过获取到模块的具体配置信息之后会交给BatchedBridge处理,之前我们说的是native的bridge,不过js为了桥接native层也引入了BatchedBridge:
1 | //BatchedBridge.js |
我们看到BatchedBridge是MessageQueue的一个实例,而且是全局唯一的一个实例,作为桥接native的一个关键点,我们来具体深入看一下它的内部实现“。
看一下传递给messageQueue的两个参数1
2__fbBatchedBridgeConfig.remoteModuleConfig,
__fbBatchedBridgeConfig.localModulesConfig,
fbBatchedBridgeConfig我们之前提到过,是一个全局的js变量,fbBatchedBridgeConfig.remoteModuleConfig就是之前我们在native层导出的模块配置表.
messageQueue
首先看一下messageQueue里的一些实例变量以及API
1 | //存储native提供的各个模块信息, |
可以看到这个队列里保存着js跟native的模块交互的所有信息。先看一下_genModules方法,该方法会根据config解析每个模块的信息并保存到this.RemoteModules中:
1 | _genModules(remoteModules) { |
_genModules会历遍所有的remoteModules,根据每个模块的配置信息(如何生成配置信息下面会提到)和module索引ID来创建每个模块
1 | _genModule(config, moduleID) { |
_genMethod方法如下,假如方法的type为remoteAsync,也就是异步方法,其实就是用一个promise对象(promise是js中的一种异步编程方式)来包装普通的方法,这里我们只看下普通方法的处理过程:
1 | _genMethod(module, method, type) { |
可以看到该方法也比较简单,只是在参数列表中提取onFail和onSucc回调函数,并最终调用__nativeCall方法。
1 | __nativeCall(module, method, params, onFail, onSucc) { |
__nativeCall方法中,假如有回调参数onFail或onSucc,会将对应的callbackID保存到参数中,并将它们压入到_callbacks栈中;接着将模块,名称以及参数分别保存到_queue的三个数组中;接下来的关键就是调用nativeFlushQueueImmediate方法,该方法是之前RCTJSCExecutor setup的时候注册到js global的方法,因此它会执行相应的native block方法(javascriptcore框架会负责js function和block的转换),可以看出_queue中的模块、方法以及参数信息最终会传递给native层,由native解析并执行相应的native方法。
我们也可以注意到这里react为了性能的优化,当js两次调用方法的间隔小于MIN_TIME_BETWEEN_FLUSHES_MS(5ms)时间,会将调用信息先缓存到_queue中,等待下次在一并提交给native层执行,可能这也就是这些参数设置成数组形式保存的原因。
让我们在接下去看看handleBuffer,handleBuffer会将调用信息先按模块的队列分好,
1 | //RCTBatchedBridge.m |
_handleRequestNumber根据模块的ID、方法ID以及参数来调用具体的函数:1
2
3
4
5
6
7
8
9- (BOOL)_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];
}
其中如何根据函数签名来调用函数可以具体查阅-(void)processMethodSignature函数,这里就不去细谈了。
- 回调函数
当有回调函数的时候,之前看到__nativeCall会将callbackID放置在参数中,对应的回调函数插入到_callbacks中保存,js将该ID传递给native,native就是通过该ID来找到对应的回调函数的。
1 | //MyModule.m |
比如MyModule定义的回调函数,当通过函数签名如果发现参数的类型是RCTResponseSenderBlock,则js传递过来的参数就是回调函数的ID,native层就会根据该ID以及RCTResponseSenderBlock提供的参数来回调相应的js回调函数,整个调用过程可以简单的用下图表示。
Native->JS,Native调用JS
假如你有自己创建的js模块想要被native层调用,也需要将该js模块注册添加到messagequeue的_callableModules中,比如reactjs的事件发送模块:1
2
3
4
5//RCTEventEmitter.js
BatchedBridge.registerCallableModule(
'RCTEventEmitter',
ReactNativeEventEmitter
);
native层调用的js方法类似如下:1
2 [_bridge enqueueJSCall:@"RCTEventEmitter.receiveEvent"
args:body ? @[body[@"target"], name, body] : @[body[@"target"], name]];
不过让我们来看看真正执行js代码的地方,里面其实就是用到javascriptcore框架,为了方便断点调试我把宏去掉了,不过不影响,简单示例如下:
1 | - (void)_executeJSCall:(NSString *)method |
可以看出native调用js的代码借助JavaScriptCore框架变的非常简单。
总结
上面简单的说明了react之间的通信过程,至于跟view相关的内容下次在讨论,这个其实才是react比较重要的内容,如果有兴趣欢迎一起交流~