混合开发: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 <Foundation/Foundation.h>
// 导入RCTBridgeModule头文件
#import <React/RCTBridgeModule.h>

// 遵循RCTBridgeModule协议
@interface CalendarManager : NSObject <RCTBridgeModule>

@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 <Foundation/Foundation.h>
// 导入RCTBridgeModule头文件
#import <React/RCTBridgeModule.h>
// 导入RCTEventEmitter头文件
#import <React/RCTEventEmitter.h>

// 遵循RCTBridgeModule协议
@interface CalendarManager : RCTEventEmitter <RCTBridgeModule>

@end
// CalendarManager.m

@implementation CalendarManager

{
  // 原生模块是否有监听者,用来优化无监听情况下造成的额外开销
  bool hasListeners;
}

// 所有支持的事件,和原生模块那边约定好的事件名
- (NSArray<NSString *> *)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原生模块有关多线程的知识其实就这么点。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,692评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,482评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,995评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,223评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,245评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,208评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,091评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,929评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,346评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,570评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,739评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,437评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,037评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,677评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,833评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,760评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,647评论 2 354

推荐阅读更多精彩内容