本文假设你已经有一定的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 LoggerRCT_EXPORT_MODULE()RCT_EXPORT_METHOD(greet:(NSString *)name)
{NSLog(@"Hi, %@!", name);[_bridge.eventDispatcher sendAppEventWithName:@"greeted"body:@{ @"name": name }];
}@end

下面,我们主要看看代码里用到的两个宏定义:RCT_EXPORT_MODULERCT_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); }

它的作用:
* 首先它声明了RCTRegisterModuleextern方法,也就是说这个方法的实现在编译的时候不可知,而在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出来的全部信息:modulesmethods,而且这些全部发生在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桥接器初探相关推荐

  1. 如何在React Native中创建精美的动画加载器

    by Vikrant Negi 通过Vikrant Negi 如何在React Native中创建精美的动画加载器 (How to create a beautifully animated load ...

  2. 如何使用动态工具提示构建React Native图表

    by Vikrant Negi 通过Vikrant Negi 如何使用动态工具提示构建React Native图表 (How to build React Native charts with dyn ...

  3. 这是我最喜欢的使用React Native创建生产级应用程序的技巧

    Trust me when I say this, React Native is hard. And it's not the usual hard of what we think hard is ...

  4. React Native 0.44.3 ----------微信h5支付

    本篇记录ios使用ReactNative完成微信h5支付的一些注意事项. a. 支付完成或者取消微信h5支付,会跳转到手机默认的浏览器中(safari),无法回到原App. b. 如何获得支付状态?? ...

  5. RN:React Native原理以及新架构JSI、Fabric等概念

    说明 RN需要一个JS的运行环境, 在IOS上直接使用内置的javascriptcore, 在Android 则使用webkit.org官方开源的jsc.so. 此外还集成了其他开源组件,如fresc ...

  6. React Native初探

    React Native初探 转自:博客园 叶小钗  前言 很久之前就想研究React Native了,但是一直没有落地的机会,我一直认为一个技术要有落地的场景才有研究的意义,刚好最近迎来了新的APP ...

  7. React Native (一) react-native-video实现音乐播放器和进度条的功能

    React Native (一) react-native-video实现音乐播放器和进度条的功能 功能: 1.卡片滑动切歌 2.显示进度条 效果图: 第三方组件: 1.react-native-vi ...

  8. React Native 交互管理器InteractionManager

    一个可以提升用户体验和交互效果的模块InteractionMnager(交互管理器) 一.基本内容 使用InteractionManager可以让一些耗时的任务在交互操作或者动画完成之后进行执行,这样 ...

  9. react native实现兼容Android与ios的视频播放器

    呦吼-,我来啦,这是我的第一篇文章,当然是福利啦,项目代码可以直接运行使用的呀,这种便宜尽情挥霍去吧-.好了装嫩结束,来点实际的,这个项目其实是我主讲的一门视频课程(看文章就可以学到所有知识,若需要可 ...

最新文章

  1. as3 android白屏,Android 8.0中一些坑以及对应的解决方法
  2. C# checked、unchecked操作符
  3. eclipse中用maven多模块管理,然后主项目无法调用其他被依赖项目里的方法,解决办法
  4. 和 Gmail 先生面对面
  5. SVM: 实际中使用SVM的一些问题
  6. jset编写测试vue代码_使用 Jest 进行 Vue 单元测试
  7. error.html mp4,HTML Video error用法及代码示例
  8. SAP CRM WebClient UI运行时加载哪个configuration,到底是如何决定的
  9. 添加日志_第五章springboot2.0添加aop日志实现记录请求地址
  10. Asp.Net使用Yahoo.Yui.Compressor.dll压缩Js|Css
  11. Android 系统(226)---Android 阿拉伯语适配
  12. 最爱的文本编辑器_VS Code——插件推荐整理
  13. 两台电脑怎么互传文件?用它就能搞定!
  14. Mac Air USB接口 失效/不起作用 的修复方式
  15. html阅读器 怎么卸载,internetexplorer怎么卸载
  16. python 断言详细讲解用法及其案例_python断言_python 断言_python断言案例 - 云+社区 - 腾讯云...
  17. 51单片机——串口通信
  18. Qt 之转盘实现C++语言
  19. php liger 表格排序,LigerUI中通过加载服务端数据进行表格的分页显示
  20. Python自动化生成 word 文档

热门文章

  1. 生物基础-多组学联合分析
  2. varchar和char区别
  3. 作为潮汕人你了解肠粉么 附带家常肠粉做法
  4. ubuntu14.04下推荐的工具及插件
  5. Qt/C++ 加载数据库出现qt_sql_default_connection提醒
  6. phd计算机考试,21校计算机Phd详细申请经验
  7. Java网络爬虫(三)爬取网络小说
  8. python给列表的N个元素赋值-*表达式
  9. 赛门铁克为 Google 域名颁发证书
  10. 棋牌类游戏测试用例怎么写?我敢打赌你绝对不知道