混合开发:RN调用原生模块

目录

一. 前言
二. 示例: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类,其中RCTReaCT的缩写。

// 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 <===> NSIntegerfloatdoubleCGFloatNSNumber
  • boolean <===> BOOLNSNumber
  • array <===> NSArray
  • object <===> NSDictionary
  • function <===> RCTResponseSenderBlock
  • promise <===> RCTPromiseResolveBlockRCTPromiseRejectBlock

除以上几种之外,桥接通道就不能传递其它数据类型的数据了。

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(RCTPromiseResolveBlockRCTPromiseRejectBlock)两种方式来给原生模块传递数据。但Promise使用起来代码比较清晰,我们推荐使用Promise,所以就不去演示回调函数那种方式了,而仅仅演示Promise这种方式。

这种方式是指,如果OC方法的最后两个参数是RCTPromiseResolveBlockRCTPromiseRejectBlock(两者必须同时存在),那么桥接后的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原生模块有关多线程的知识其实就这么点。

你可能感兴趣的:(混合开发:RN调用原生模块)