React Native桥接器初探
本文假设你已经有一定的React Native基础,并且想要了解React Native的JS和原生代码之间是如何交互的。
React Native的工作线程
shadow queue
:布局在这个线程工作main thread
:UIKit在这里工作Javascript thread
:Js代码在这里工作
另外每一个原生模块都有自己的一个工作GCD queue
,除非你明确指定它的工作队列
*shadow queue*实际是一个GCD queue,而不是一个线程。
原生模块
如果你还不知道如何创建原声模块,我建议你看看官方文档。
下面是一个叫做Person
的原生模块,既可以被js调用,也可以调用js代码。
@interface Person : NSObject <RCTBridgeModule>
@end
@implementation Logger
RCT_EXPORT_MODULE()
RCT_EXPORT_METHOD(greet:(NSString *)name)
{
NSLog(@"Hi, %@!", name);
[_bridge.eventDispatcher sendAppEventWithName:@"greeted"
body:@{ @"name": name }];
}
@end
下面,我们主要看看代码里用到的两个宏定义:RCT_EXPORT_MODULE
和RCT_EXPORT_METHOD
。看看他们是如何工作的。
RCT_EXPORT_MODULE([js_name])
这个宏的功能就和它名字说的一样,到处一个模块。但是export是什么意思呢?它的意思是让React Native的bridge(桥接)感知到原生模块。
它的定义其实非常的简单:
#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString \*)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }
它的作用:
- 首先它声明了
RCTRegisterModule
为extern
方法,也就是说这个方法的实现在编译的时候不可知,而在link的时候才可知。 - 声明了一个方法
moduleName
,这个方法返回可选的宏定义参数js_name
,一般是你希望有一个专门的模块名称,而不是默认的ObjC类名的时候使用。 - 最后,声明了一个
load
方法(当app被加载进内存的时候,load方法也会被调用)。在这个方法里调用RCTRegisterModule
方法来让RN的bridge感知到这个模块。
RCT_EXPORT_METHOD(method)
这个宏更有意思,它并给你的模块添加任何实际的方法。它创建了一个新的方法,这个新的方法基本上是这样的:
+ (NSArray *)__rct_export__120
{
return @[@"", @"log: (NSString *)message"];
}
这个被load
方法生成的方法的名称由前缀(__rct_export__
)和一个可选的js_name
(现在是空的)和声明的行号(比如12)和__COUNTER__
宏拼接在一起组成。
这个新生成的方法的作用就是返回一个数组,这个数组包含一个可选的js_name
(在本例中是空的)和方法的签名。签名说的那一堆是为了避免方法崩溃。
即使是这么复杂的生成算法,如果你使用了*category*的话也难免会有两个方法的名称是一样的。不过这个概率非常低,并且也不会产生什么不可控的行为。虽然Xcode会这么警告。
Runtime
这一步只做一件事,那就是给React Native的桥接模块提供信息。这样它就可以找到原生模块里export出来的全部信息:modules和methods,而且这些全部发生在load的时候。
下图是React Native桥接的依赖图
初始化模块
方法RCTRegisterModule
方法就是用来把类添加到一个数组里,这样React Native桥接器实例创建之后可以找到这个模块。它会遍历模块数组,创建每个模块的实例,并在桥接器里保存它的引用,并且每个模块的实例也会保留桥接器的实例。并且该方法还会检查模块是否指定了运行的队列,如果没有指定那么就运行在一个新建的队列上,与其他队列分割。
NSMutableDictionary *modulesByName; // = ...
for (Class moduleClass in RCTGetModuleClasses()) {
// ...
module = [moduleClass new];
if ([module respondsToSelector:@selector(setBridge:)]) {
module.bridge = self;
}
modulesByName[moduleName] = module;
// ...
}
配置原生模块
一旦在后台线程里有了模块实例,我们就列出每个模块的全部方法,之后调用__rct_export__
开始的方法,这样我们就有一个该方法签名的字符串。这样我们后续就可以获得参数的实际类型。在运行时,我们只会知道参数的类型是id
,按照上面的方法就可以获得参数的实际类型,比如本例的是NSString*
。
unsigned int methodCount;
Method *methods = class_copyMethodList(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);
//...
[moduleMethods addObject:/* Object representing the method */];
}
}
初始化Javascript执行器
JavaScript执行器有一个setUp
方法。用这个方法可以执行很多耗费资源的任务,比如在后台线程里初始化JavaScriptCore
。由于只有active的执行器才可以接受到setUp
的调用,所以也节约了很多的资源。
JSGlobalContextRef ctx = JSGlobalContextCreate(NULL);
_context = [[RCTJavaScriptContext alloc] initWithJSContext:ctx];
注入Json配置
模块的配置都是Json形式的,如:
{
"remoteModuleConfig": {
"Logger": {
"constants": { /* If we had exported constants... */ },
"moduleID": 1,
"methods": {
"requestPermissions": {
"type": "remote",
"methodID": 1
}
}
}
}
}
这些都作为全局变量存储在JavaScript VM里,因此当桥接器的Js侧代码初始化完毕的时候它可以用这些信息来创建原生模块。
加载JavaScript代码
可以获得代码的地方只有两个,在开发的时候从packager下载代码,在产品环境下从磁盘加载代码。
执行JavaScript代码
一旦所有的准备工作就绪,我们就可以把App的代码都加载到JavaScript Core里解析,执行。在最开始执行的时候,所有的CommonJS模块都会被注册(现在你写的是ES6的模块,不是CommonJS,但是最后会转码为ES5),并require入口文件。
JSValueRef jsError = NULL;
JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script);
JSStringRef jsURL = JSStringCreateWithCFString((__bridge CFStringRef)sourceURL.absoluteString);
JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, &jsError);
JSStringRelease(jsURL);
JSStringRelease(execJSString);
JavaScript模块
这个时候,上例中的原生模块就可以在NativeModules
对象里调用了。
var { NativeModules } = require('react-native');
var { Person } = NativeModules;
Person.greet('Tadeu');
当你调用一个原生模块的方法的时候,它会在一个队列里执行。其中包含模块名、方法名和调用这个方法需要的全部参数。在JavaScript执行结束的时候原生代码继续执行。
调用周期
下面看看如果我们调用上面的代码会发生什么:
代码的调用从Js开始,之后开始原生代码的执行。Js传入的回调会通过桥接器(原生模块使用_bridge
实例调用enqueueJSCall:args:
)传回到JS代码。
注意:你如果看过文档,或者亲自实践过的话你就会知道也有从原生模块调用JS的情况。这个是用vSYNC实现的。但是这些为了改善启动时间被删除了。
参数类型
从原生调用JS的情况更简单一些,参数是做为JSON例的一个数组传递的。但是从JS到原生的调用里,我们需要原生的类型。但是,如上文所述,对于类的对象(结构体的对象),运行时并不能通过NSMethodSignature
给我们足够的信息,我们只有字符串类型。
我们使用正则表达式从方法的签名里提取类型,然后我们使用RCTConvert
工具类来实际转化参数的类型。这个工具类会把JSON里的数据转化成我们需要的类型。
我们使用objc_msgSend
来动态调用方法。如果是struct的话,则使用NSInvocation
来调用。
一旦我们得到了全部参数的类型,我们使用另外一个NSInvocation
来调用目标模块的方法,并传入全部的参数。比如:
// If you had the following method in a given module, e.g. `MyModule`
RCT_EXPORT_METHOD(methodWithArray:(NSArray *) size:(CGRect)size) {}
// And called it from JS, like:
require('NativeModules').MyModule.method(['a', 1], {
x: 0,
y: 0,
width: 200,
height: 100
});
// The JS queue sent to native would then look like the following:
// ** Remember that it's a queue of calls, so all the fields are arrays **
@[
@[ @0 ], // module IDs
@[ @1 ], // method IDs
@[ // arguments
@[
@[@"a", @1],
@{ @"x": @0, @"y": @0, @"width": @200, @"height": @100 }
]
]
];
// This would convert into the following calls (pseudo code)
NSInvocation call
call[args][0] = GetModuleForId(@0)
call[args][1] = GetMethodForId(@1)
call[args][2] = obj_msgSend(RCTConvert, NSArray, @[@"a", @1])
call[args][3] = NSInvocation(RCTConvert, CGRect, @{ @"x": @0, ... })
call()
线程
默认情况下,每一个模块都有自己的GCD queue
。除非在模块中通过-methodQueue
方法指定模块要运行的队列。有一个例外是View Managers
(就是继承了RCTViewManager
)的类,会默认运行在Shadow Queue里。
目前的线程规则是这样的:
-init
和-setBridge:
保证会在main thread里执行- 所有导出的方法都会在目标队列里执行
- 如果你实现了
RCTInvalidating
协议,invalidate
也会在目标队列里执行 -dealloc
方法在哪个线程执行被调用
当JS执行一堆的方法之后,这些方法会根据目标队列分组,之后被并行分发:
// group `calls` by `queue` in `buckets`
for (id queue in buckets) {
dispatch_block_t block = ^{
NSOrderedSet *calls = [buckets objectForKey:queue];
for (NSNumber *indexObj in calls) {
// Actually call
}
};
if (queue == RCTJSThread) {
[_javaScriptExecutor executeBlockOnJavaScriptQueue:block];
} else if (queue) {
dispatch_async(queue, block);
}
}
总结
本文还只是对桥接器如何工作的一个简单描述。希望对各位能有所帮助。
原文:https://tadeuzagallo.com/blog/react-native-bridge/
React Native桥接器初探的更多相关文章
- React Native 常用学习链接地址
Android Studio下载http://www.android-studio.org/ 第二章:Android Studio概述(一)http://ask.android-studio.org/ ...
- React Native初探
前言 很久之前就想研究React Native了,但是一直没有落地的机会,我一直认为一个技术要有落地的场景才有研究的意义,刚好最近迎来了新的APP,在可控的范围内,我们可以在上面做任何想做的事情. P ...
- React Native For Android 架构初探
版权声明:本文由王少鸣原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/171 来源:腾云阁 https://www.qclo ...
- 使用react native制作的一款网络音乐播放器
使用react native制作的一款网络音乐播放器 基于第三方库 react-native-video设计"react-native-video": "^1.0.0&q ...
- React Native实战系列教程之自定义原生UI组件和VideoView视频播放器开发
React Native实战系列教程之自定义原生UI组件和VideoView视频播放器开发 2016/09/23 | React Native技术文章 | Sky丶清| 4 条评论 | 1 ...
- React Native 初探
推荐文章 React Native 简介:用 JavaScript 搭建 iOS 应用 (1) React Native 简介:用 JavaScript 搭建 iOS 应用 (2) React Nat ...
- React Native IOS源码初探
原文链接 http://www.open-open.com/lib/view/open1465637638193.html 每个项目都有一个入口,然后进行初始化操作,React Native 也不例外 ...
- 最火移动端跨平台方案盘点:React Native、weex、Flutter
1.前言 跨平台一直是老生常谈的话题,cordova.ionic.react-native.weex.kotlin-native.flutter等跨平台框架的百花齐放,颇有一股推倒原生开发者的势头. ...
- React Native开发入门
目录: 一.前言 二.什么是React Native 三.开发环境搭建 四.预备知识 五.最简单的React Native小程序 六.总结 七.参考资料 一.前言 虽然只是简单的了解了一下Reac ...
随机推荐
- kubernetes学习笔记之十三:基于calico的网络策略入门
一..安装calico [root@k8s-master01 ~]# kubectl apply -f https://docs.projectcalico.org/v3.3/getting-star ...
- 六、框架<iframe>、背景、实体
HTML5框架 框架标签(frame) 框架对于页面的设计有着很大的作用 框架集的标签(<frameset>) 框架集标签定义如何将窗口分割为框架 每一个frameset定义一系列行或列 ...
- Openface 入门
Openface 简单入门 背景 Openface是一个开源的人脸识别框架,同类软件产品还有 seetaface ,DeepID等,当然,如果算上商业的产品,那就更多了. Openface人脸比对结果 ...
- Webform(条件查询)
<html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> &l ...
- git保存用户名和密码
git保存用户名和密码 简介:tortoiseGit(乌龟git)图形化了git,我们用起来很方便,但是我们拉取私有项目的时候,每次都要输入用户名和密码很麻烦,这里向大家介绍怎么避免多少输入 试验环境 ...
- WMS接口平台配置培训
供应链管理平台地址:https://twms.ninestargroup.com/ibus/#/processconfig?scShortcutld=3_17__1_303 WMS提供WSWMS固定的 ...
- Spring学习整理
Spring概述 将Spring理解为管理对象间依赖关系的容器 “解耦” 根据功能的不同,可以将一个系统中的代码分为 主业务逻辑 与 系统级业务逻辑 两类 ```Spring 是为了解决企业级开发的复 ...
- Linux firewalld使用教程+rhce课程实验
--timeout= 设置规则生效300秒 调试阶段使用,防止规则设置错误导致无法远程连接 实验:在server0机器上部署httpd服务,通过添加富规则,只允许172.25.0.10/32访问,并且 ...
- 小强学渲染之OpenGL状态机理解
状态机是理论上的一种机器,呃这个说法非常非常的抽象.通俗一点理解,状态机描述了一个对象在其生命周期内所经历的各种状态,状态间的转变,发生转变的动因,条件及转变中所执行的活动.或者说,状态机是一种行为, ...
- 基于python的selenium自动化测试环境搭建
Windows下的环境搭建: 1.安装python2.7.152.cmd里敲pip install selenium3.安装firefox47.geckodriver11(并将geckodriver. ...