目录
一. 前言
二. 示例:RN调用原生模块(场景五)的详细开发步骤
1. 基本使用
2. 原生模块与OC类的数据交互
2.1 数据类型转换
2.2 原生模块传递数据给OC类
2.3 OC类传递数据给原生模块
2.4 OC类主动传递数据给原生模块(本质是OC给JS发送事件)
三. 多线程
一. 前言
RN和iOS混合开发的几种场景。
- 原生项目中,调用部分RN页面。
- 原生页面中,调用部分RN组件。
- RN项目中,调用部分原生页面。
- RN页面中,调用部分原生View。
- RN项目中,调用部分原生模块。
场景一和场景二其实是一样的,因为在RN看来,页面和组件在广义上都是组件,对应于原生里的View。
场景三和场景四是一样的,因为无论RN要调用原生的页面还是View,我们最终都是把原生的View交给它调用。还是那句话,RN那边的组件对应原生里的View,而没法对应ViewController。
场景五和场景三、场景四的区别在于,RN调用原生页面或View是指调用原生视图层面的东西来做UI布局的(当然这些视图也可能会有操作事件),而RN调用原生模块是指调用原生功能层面的东西来实现某个功能(例如调用日历、通讯录等模块,调用分享、三方登录、支付等三方SDK,调用我们自己的某些功能代码块,等等)。
上上一篇讲解了原生调用RN页面或组件(场景一和场景二)的详细开发步骤,上一篇讲解了RN调用原生页面或View(场景三和场景四)的详细开发步骤,这一篇我们RN调用原生模块(场景五)的详细开发步骤。
我们在开发RN App的时候,有可能会遇到
- App需要实现某些功能(例如调用日历、通讯录等模块,调用分享、三方登录、支付等三方SDK),但RN还没有对相应的OC模块进行封装,三方SDK也只提供了支持iOS开发的Api,而没有提供支持JS开发的Api。
- 或者你想要复用某些OC、Swift、Java的功能代码块,而不是在RN项目里再用JS实现一遍。
- 又或者你想要实现某些高性能、多线程的处理(例如图片处理、数据库操作等)。
以上几种情况下,我们就得使用混合开发了,让RN调用原生模块,但这是一个相对高级的特性,不应当在日常开发中经常出现。
二. 示例:RN调用原生模块(场景五)的详细开发步骤
该示例实现的是:模拟RN调用原生日历模块。
其实很简单的,无非就是让一个OC类实现RCTBridgeModule
协议,并导出一些需要的方法,这样在RN项目里我们就可以通过NativeModules
这个组件获取到一个同OC类名的原生模块来使用了。
1. 基本使用
RN对应iOS,JS对应OC,那RN中的原生模块 = iOS中实现了RCTBridgeModule
协议的OC类,其中RCT
是ReaCT
的缩写。
// CalendarManager.h
#import
// 导入RCTBridgeModule头文件
#import
// 遵循RCTBridgeModule协议
@interface CalendarManager : NSObject
@end
为了实现RCTBridgeModule
协议,我们的OC类里面需要包含RCT_EXPORT_MODULE()
这个宏,也就是说只要我们在OC类里包含了这个宏,就是为这个类实现了RCTBridgeModule
协议。同时这个宏还用来导出这个OC类生成RN的原生模块,它可以添加一个参数用来指定在RN项目中访问该原生模块时的名字,如果不指定,则默认就是这个OC类名——即CalendarManager
。
// CalendarManager.m
#import "CalendarManager.h"
@implementation CalendarManager
// 实现RCTBridgeModule协议
// 导出该原生模块
RCT_EXPORT_MODULE();
@end
我们已经成功地得到了一个原生模块,那我们如何为该原生模块添加一些方法呢?很简单,在OC类里用RCT_EXPORT_METHOD()
宏导出一些方法就可以了。
// CalendarManager.m
// 导出该原生模块的方法
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)
{
NSLog(@"事件名:%@,地点:%@", name, location);
}
完事了,现在我们就可以去RN项目里使用这个原生模块并调用它的方法了,很简单吧。(记得要用Xcode重新运行下,而不仅仅是Reload项目,否则该原生模块是加载不到项目里的)
// 某个RN文件
import {NativeModules} from 'react-native';
const CalendarManager = NativeModules.CalendarManager;
CalendarManager.addEvent('生日聚会', '六国饭店');
2. 原生模块与OC类的数据交互
注意:
RCT_EXPORT_METHOD()
这个宏其实是建立了一个桥接通道,把OC方法桥接为JS方法,并且要求被桥接的OC方法返回值类型必须为void
,桥接操作是异步的。我们也正是通过桥接操作实现了原生模块和OC类的数据交互,原生模块调用桥接出来的JS方法,通过参数把数据传递给OC类,而OC类则通过桥接前OC方法的Promise把数据传递给原生模块。但是请记住:在这种数据交互过程中,原生模块都是主动方。
我们可以看到,第1节里OC类的方法是
addEvent: location:
,方法名有两部分,方法有两个参数,而桥接出的原生模块方法是addEvent
,只有一部分,带两个参数。这就表明RCT_EXPORT_METHOD()
宏在将OC方法桥接为JS方法时,仅仅会桥接OC的方法名的第一部分。那如果我们有多个OC方法要桥接为JS方法,并且它们的第一部分是一样的,那桥接出来的JS方法岂不是都一样嘛,该怎么办呢?RN还定义了一个RCT_REMAP_METHOD()
宏,它可以用来指定原生模块那边对应的方法名,下面我们会有例子。
2.1 数据类型转换
RCT_EXPORT_METHOD
创建的桥接通道支持原生模块与OC类之间互相传输指定数据类型的数据,并且会自动完成数据类型的转换,包括:
-
string
<===>NSString
-
number
<===>NSInteger
、float
、double
、CGFloat
、NSNumber
-
boolean
<===>BOOL
、NSNumber
-
array
<===>NSArray
-
object
<===>NSDictionary
-
function
<===>RCTResponseSenderBlock
-
promise
<===>RCTPromiseResolveBlock
、RCTPromiseRejectBlock
除以上几种之外,桥接通道就不能传递其它数据类型的数据了。
2.2 原生模块传递数据给OC类
原生模块调用桥接出来的JS方法,通过参数把数据传递给OC类。
接着上面的CalendarManager
例子,现在我们需要把事件的日期由原生模块传递给OC类,但是在调用JS方法时不能直接传递Date
对象(因为桥接通道不支持这种数据类型),所以我们需要把日期转化为数字——时间戳——来传递给OC方法,OC方法再使用RCTConvert
把时间戳转换为日期使用。于是有:
// CalendarManager.m
// 导出该原生模块的方法
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull NSNumber *)secondsSinceUnixEpoch)
{
NSDate *date = [RCTConvert NSDate:secondsSinceUnixEpoch];
NSLog(@"事件名:%@,地点:%@,日期:%@", name, location, date);
}
// 某个RN文件
// new Date().getTime()为获取当前时间的时间戳(就是当前时区的)
CalendarManager.addEvent('生日聚会', '六国饭点', new Date().getTime());
随着CalendarManager.addEvent
方法变得越来越复杂,参数的个数越来越多,我们应该考虑修改一下我们的API,用一个dictionary
来存放所有的事件参数,像这样:
// CalendarManager.m
// 导出该原生模块的方法
RCT_EXPORT_METHOD(addEvent:(NSString *)name details:(NSDictionary *)details)
{
NSString *location = [RCTConvert NSString:details[@"location"]];
NSDate *date = [RCTConvert NSDate:details[@"date"]];
NSLog(@"事件名:%@,地点:%@,日期:%@", name, location, date);
}
// 某个RN文件
CalendarManager.addEvent('生日聚会', {
location: '六国饭点',
date: new Date().getTime(),
});
2.3 OC类传递数据给原生模块
OC类通过桥接前OC方法的Promise把数据传递给原生模块。
OC类可以通过回调函数(RCTResponseSenderBlock
)和Promise(RCTPromiseResolveBlock
、RCTPromiseRejectBlock
)两种方式来给原生模块传递数据。但Promise使用起来代码比较清晰,我们推荐使用Promise,所以就不去演示回调函数那种方式了,而仅仅演示Promise这种方式。
这种方式是指,如果OC方法的最后两个参数是RCTPromiseResolveBlock
和RCTPromiseRejectBlock
(两者必须同时存在),那么桥接后的JS方法就会返回一个Promise对象,我们就可以通过这个Promise对象把数据由OC类传递给原生模块。
// CalendarManager.m
// 我们定义两个block,用来记录OC方法那两个block,因为我们不确定具体要在哪里调用这两个block
@property (nonatomic, copy) RCTPromiseResolveBlock resolve;
@property (nonatomic, copy) RCTPromiseRejectBlock reject;
RCT_REMAP_METHOD(findEvents,
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
// 记录OC方法这两个block,以便在合适的地方调用
self.resolve = resolve;
self.reject = reject;
// 假设我们这里是打开日历模块,读取事件
[self _openCalendarAndFindEvents];
}
- (void)_openCalendarAndFindEvents {
// 读取事件
NSArray *events = @[@{
@"name": @"生日聚会",
@"details": @{
@"location": @"六国饭点",
@"date": @"2019-08-13 06:14:52",
}
}];
if (events) {// 假设这里是读取事件成功的回调
// 调用读取事件成功的block
self.resolve(events);
} else {// 假设这里是读取事件失败的回调
// 调用读取事件失败的block
self.reject(@"failure", @"读取日历事件出错", nil);
}
}
// 某个RN文件
CalendarManager.findEvents()
.then(events => {
console.log(events);
})
.catch(error => {
console.log(error);
});
这样就能顺利地完成OC类给原生模块传递数据了,但是此处再说一遍这种OC类给原生模块传递数据的方式中,OC类是被动的,即只有原生模块调用了某个JS方法,从而才触发了OC类的某个方法,OC类才把数据给人传递过去了。
那有没有一种方式,OC类是主动的呢?即我OC类就是要给你原生模块传递数据,你在那给我等着。
2.4 OC类主动传递数据给原生模块(本质是OC给JS发送事件)
有的场景下,即便原生模块没有调用JS方法向我们OC类要数据,但我们OC类就是有钱啊,想给你啊。此时最好的实现方案就是继承RCTEventEmitter
,实现suppportEvents
方法,并调用[self sendEventWithName: body:]
方法发送数据给原生模块就可以了。
// CalendarManager.h
#import
// 导入RCTBridgeModule头文件
#import
// 导入RCTEventEmitter头文件
#import
// 遵循RCTBridgeModule协议
@interface CalendarManager : RCTEventEmitter
@end
// CalendarManager.m
@implementation CalendarManager
{
// 原生模块是否有监听者,用来优化无监听情况下造成的额外开销
bool hasListeners;
}
// 所有支持的事件,和原生模块那边约定好的事件名
- (NSArray *)supportedEvents
{
return @[@"EventReminder"];
}
// 原生模块添加第一个监听者时会触发该方法
- (void)startObserving
{
hasListeners = YES;
}
// 原生模块的最后一个监听者移除时会触发该方法
- (void)stopObserving
{
hasListeners = NO;
}
- (void)calendarEventReminderReceived:(NSNotification *)notification
{
if (hasListeners) {// 如果有监听者再发出事件
[self sendEventWithName:@"EventReminder" body:@{
@"name": @"生日聚会",
@"details": @{
@"location": @"六国饭点",
@"date": @"2019-08-13 06:14:52",
}
}];
}
}
@end
然后我们就可以去RN项目里,用JS代码创建一个包含该原生模块的NativeEventEmitter
实例来订阅这些事件了。
// 某个RN文件
import {NativeModules, NativeEventEmitter} from 'react-native';
const CalendarManager = NativeModules.CalendarManager;
const calendarManagerEmitter = new NativeEventEmitter(CalendarManager);
const subscription = calendarManagerEmitter.addListener(
'EventReminder',// 和OC类那边约定好的事件名
(notification) => {
console.log(notification);
}
);
// 不要忘了移除监听
componentWillUnmount(){
subscription.remove();
}
三. 多线程
RN在一个独立的串行GCD队列中调用原生模块的方法。我们在为RN自定义原生模块时,如果发现有耗时的操作(如文件读写、网络操作等),就需要为这些操作新开辟一个线程来执行,不然的话,这些耗时的操作会阻塞RN项目的线程。
在OC类中实现- (dispatch_queue_t)methodQueue
方法就可以指定原生模块的方法在哪个队列中被执行。比如一个原生模块的所有操作都必须在主线程执行,那应当这样指定:
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
而如果一个操作需要花费很长时间,原生模块不应该阻塞住,而是应当声明一个用于执行操作的独立队列。举个例子,RCTAsyncLocalStorage
模块创建了自己的一个queue
,这样它在做一些较慢的磁盘操作的时候就不会阻塞住React本身的消息队列:
- (dispatch_queue_t)methodQueue
{
return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
}
但是- (dispatch_queue_t)methodQueue
方法指定的队列会被你模块里的所有方法共享。所以如果你的方法中“只有一个”是耗时较长的(或者是由于某种原因必须在不同的队列中运行的),你可以专门在该函数体内用dispatch_async
方法来在另一个队列执行,而不影响其他方法:
RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 在这里执行长时间的操作
...
// 你可以在任何线程/队列中执行回调函数
callback(@[...]);
});
}
此外,我们要知道如果原生模块中需要更新UI,我们也需要获取主线程,然后在主线程中更新UI:
RCT_EXPORT_METHOD(updateUI)
{
dispatch_async(dispatch_get_main_queue(), ^{
// 刷新UI
});
}
RN原生模块有关多线程的知识其实就这么点。