目錄
一. 前言
二. 示例:RN調(diào)用原生模塊(場景五)的詳細(xì)開發(fā)步驟
?1. 基本使用
?2. 原生模塊與OC類的數(shù)據(jù)交互
??2.1 數(shù)據(jù)類型轉(zhuǎn)換
??2.2 原生模塊傳遞數(shù)據(jù)給OC類
??2.3 OC類傳遞數(shù)據(jù)給原生模塊
??2.4 OC類主動傳遞數(shù)據(jù)給原生模塊(本質(zhì)是OC給JS發(fā)送事件)
三. 多線程
一. 前言
RN和iOS混合開發(fā)的幾種場景。
- 原生項目中炬守,調(diào)用部分RN頁面毁枯。
- 原生頁面中边败,調(diào)用部分RN組件袱衷。
- RN項目中,調(diào)用部分原生頁面笑窜。
- RN頁面中致燥,調(diào)用部分原生View。
- RN項目中排截,調(diào)用部分原生模塊嫌蚤。
場景一和場景二其實是一樣的,因為在RN看來断傲,頁面和組件在廣義上都是組件脱吱,對應(yīng)于原生里的View。
場景三和場景四是一樣的认罩,因為無論RN要調(diào)用原生的頁面還是View箱蝠,我們最終都是把原生的View交給它調(diào)用。還是那句話垦垂,RN那邊的組件對應(yīng)原生里的View宦搬,而沒法對應(yīng)ViewController。
場景五和場景三劫拗、場景四的區(qū)別在于间校,RN調(diào)用原生頁面或View是指調(diào)用原生視圖層面的東西來做UI布局的(當(dāng)然這些視圖也可能會有操作事件),而RN調(diào)用原生模塊是指調(diào)用原生功能層面的東西來實現(xiàn)某個功能(例如調(diào)用日歷页慷、通訊錄等模塊憔足,調(diào)用分享、三方登錄酒繁、支付等三方SDK四瘫,調(diào)用我們自己的某些功能代碼塊,等等)欲逃。
上上一篇講解了原生調(diào)用RN頁面或組件(場景一和場景二)的詳細(xì)開發(fā)步驟,上一篇講解了RN調(diào)用原生頁面或View(場景三和場景四)的詳細(xì)開發(fā)步驟饼暑,這一篇我們RN調(diào)用原生模塊(場景五)的詳細(xì)開發(fā)步驟稳析。
我們在開發(fā)RN App的時候,有可能會遇到
- App需要實現(xiàn)某些功能(例如調(diào)用日歷弓叛、通訊錄等模塊彰居,調(diào)用分享、三方登錄撰筷、支付等三方SDK)陈惰,但RN還沒有對相應(yīng)的OC模塊進行封裝,三方SDK也只提供了支持iOS開發(fā)的Api毕籽,而沒有提供支持JS開發(fā)的Api抬闯。
- 或者你想要復(fù)用某些OC井辆、Swift、Java的功能代碼塊溶握,而不是在RN項目里再用JS實現(xiàn)一遍杯缺。
- 又或者你想要實現(xiàn)某些高性能、多線程的處理(例如圖片處理睡榆、數(shù)據(jù)庫操作等)萍肆。
以上幾種情況下,我們就得使用混合開發(fā)了胀屿,讓RN調(diào)用原生模塊塘揣,但這是一個相對高級的特性,不應(yīng)當(dāng)在日常開發(fā)中經(jīng)常出現(xiàn)宿崭。
二. 示例:RN調(diào)用原生模塊(場景五)的詳細(xì)開發(fā)步驟
該示例實現(xiàn)的是:模擬RN調(diào)用原生日歷模塊亲铡。
其實很簡單的,無非就是讓一個OC類實現(xiàn)RCTBridgeModule
協(xié)議劳曹,并導(dǎo)出一些需要的方法奴愉,這樣在RN項目里我們就可以通過NativeModules
這個組件獲取到一個同OC類名的原生模塊來使用了。
1. 基本使用
RN對應(yīng)iOS铁孵,JS對應(yīng)OC锭硼,那RN中的原生模塊 = iOS中實現(xiàn)了RCTBridgeModule
協(xié)議的OC類,其中RCT
是ReaCT
的縮寫蜕劝。
// CalendarManager.h
#import <Foundation/Foundation.h>
// 導(dǎo)入RCTBridgeModule頭文件
#import <React/RCTBridgeModule.h>
// 遵循RCTBridgeModule協(xié)議
@interface CalendarManager : NSObject <RCTBridgeModule>
@end
為了實現(xiàn)RCTBridgeModule
協(xié)議檀头,我們的OC類里面需要包含RCT_EXPORT_MODULE()
這個宏,也就是說只要我們在OC類里包含了這個宏岖沛,就是為這個類實現(xiàn)了RCTBridgeModule
協(xié)議暑始。同時這個宏還用來導(dǎo)出這個OC類生成RN的原生模塊,它可以添加一個參數(shù)用來指定在RN項目中訪問該原生模塊時的名字婴削,如果不指定廊镜,則默認(rèn)就是這個OC類名——即CalendarManager
。
// CalendarManager.m
#import "CalendarManager.h"
@implementation CalendarManager
// 實現(xiàn)RCTBridgeModule協(xié)議
// 導(dǎo)出該原生模塊
RCT_EXPORT_MODULE();
@end
我們已經(jīng)成功地得到了一個原生模塊唉俗,那我們?nèi)绾螢樵撛K添加一些方法呢嗤朴?很簡單,在OC類里用RCT_EXPORT_METHOD()
宏導(dǎo)出一些方法就可以了虫溜。
// CalendarManager.m
// 導(dǎo)出該原生模塊的方法
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)
{
NSLog(@"事件名:%@雹姊,地點:%@", name, location);
}
完事了,現(xiàn)在我們就可以去RN項目里使用這個原生模塊并調(diào)用它的方法了衡楞,很簡單吧吱雏。(記得要用Xcode重新運行下,而不僅僅是Reload項目,否則該原生模塊是加載不到項目里的)
// 某個RN文件
import {NativeModules} from 'react-native';
const CalendarManager = NativeModules.CalendarManager;
CalendarManager.addEvent('生日聚會', '六國飯店');
2. 原生模塊與OC類的數(shù)據(jù)交互
注意:
RCT_EXPORT_METHOD()
這個宏其實是建立了一個橋接通道歧杏,把OC方法橋接為JS方法镰惦,并且要求被橋接的OC方法返回值類型必須為void
,橋接操作是異步的得滤。我們也正是通過橋接操作實現(xiàn)了原生模塊和OC類的數(shù)據(jù)交互陨献,原生模塊調(diào)用橋接出來的JS方法,通過參數(shù)把數(shù)據(jù)傳遞給OC類懂更,而OC類則通過橋接前OC方法的Promise把數(shù)據(jù)傳遞給原生模塊眨业。但是請記住:在這種數(shù)據(jù)交互過程中沮协,原生模塊都是主動方龄捡。
我們可以看到,第1節(jié)里OC類的方法是
addEvent: location:
慷暂,方法名有兩部分聘殖,方法有兩個參數(shù),而橋接出的原生模塊方法是addEvent
行瑞,只有一部分奸腺,帶兩個參數(shù)。這就表明RCT_EXPORT_METHOD()
宏在將OC方法橋接為JS方法時血久,僅僅會橋接OC的方法名的第一部分突照。那如果我們有多個OC方法要橋接為JS方法,并且它們的第一部分是一樣的氧吐,那橋接出來的JS方法豈不是都一樣嘛讹蘑,該怎么辦呢?RN還定義了一個RCT_REMAP_METHOD()
宏筑舅,它可以用來指定原生模塊那邊對應(yīng)的方法名座慰,下面我們會有例子。
2.1 數(shù)據(jù)類型轉(zhuǎn)換
RCT_EXPORT_METHOD
創(chuàng)建的橋接通道支持原生模塊與OC類之間互相傳輸指定數(shù)據(jù)類型的數(shù)據(jù)翠拣,并且會自動完成數(shù)據(jù)類型的轉(zhuǎn)換版仔,包括:
-
string
<===>NSString
-
number
<===>NSInteger
、float
误墓、double
邦尊、CGFloat
、NSNumber
-
boolean
<===>BOOL
优烧、NSNumber
-
array
<===>NSArray
-
object
<===>NSDictionary
-
function
<===>RCTResponseSenderBlock
-
promise
<===>RCTPromiseResolveBlock
、RCTPromiseRejectBlock
除以上幾種之外链峭,橋接通道就不能傳遞其它數(shù)據(jù)類型的數(shù)據(jù)了畦娄。
2.2 原生模塊傳遞數(shù)據(jù)給OC類
原生模塊調(diào)用橋接出來的JS方法,通過參數(shù)把數(shù)據(jù)傳遞給OC類。
接著上面的CalendarManager
例子熙卡,現(xiàn)在我們需要把事件的日期由原生模塊傳遞給OC類杖刷,但是在調(diào)用JS方法時不能直接傳遞Date
對象(因為橋接通道不支持這種數(shù)據(jù)類型),所以我們需要把日期轉(zhuǎn)化為數(shù)字——時間戳——來傳遞給OC方法驳癌,OC方法再使用RCTConvert
把時間戳轉(zhuǎn)換為日期使用滑燃。于是有:
// CalendarManager.m
// 導(dǎo)出該原生模塊的方法
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()為獲取當(dāng)前時間的時間戳(就是當(dāng)前時區(qū)的)
CalendarManager.addEvent('生日聚會', '六國飯點', new Date().getTime());
隨著CalendarManager.addEvent
方法變得越來越復(fù)雜表窘,參數(shù)的個數(shù)越來越多,我們應(yīng)該考慮修改一下我們的API甜滨,用一個dictionary
來存放所有的事件參數(shù)乐严,像這樣:
// CalendarManager.m
// 導(dǎo)出該原生模塊的方法
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類傳遞數(shù)據(jù)給原生模塊
OC類通過橋接前OC方法的Promise把數(shù)據(jù)傳遞給原生模塊昂验。
OC類可以通過回調(diào)函數(shù)(RCTResponseSenderBlock
)和Promise(RCTPromiseResolveBlock
、RCTPromiseRejectBlock
)兩種方式來給原生模塊傳遞數(shù)據(jù)艾扮。但Promise使用起來代碼比較清晰既琴,我們推薦使用Promise,所以就不去演示回調(diào)函數(shù)那種方式了泡嘴,而僅僅演示Promise這種方式甫恩。
這種方式是指,如果OC方法的最后兩個參數(shù)是RCTPromiseResolveBlock
和RCTPromiseRejectBlock
(兩者必須同時存在)磕诊,那么橋接后的JS方法就會返回一個Promise對象填物,我們就可以通過這個Promise對象把數(shù)據(jù)由OC類傳遞給原生模塊。
// CalendarManager.m
// 我們定義兩個block霎终,用來記錄OC方法那兩個block滞磺,因為我們不確定具體要在哪里調(diào)用這兩個block
@property (nonatomic, copy) RCTPromiseResolveBlock resolve;
@property (nonatomic, copy) RCTPromiseRejectBlock reject;
RCT_REMAP_METHOD(findEvents,
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
// 記錄OC方法這兩個block,以便在合適的地方調(diào)用
self.resolve = resolve;
self.reject = reject;
// 假設(shè)我們這里是打開日歷模塊莱褒,讀取事件
[self _openCalendarAndFindEvents];
}
- (void)_openCalendarAndFindEvents {
// 讀取事件
NSArray *events = @[@{
@"name": @"生日聚會",
@"details": @{
@"location": @"六國飯點",
@"date": @"2019-08-13 06:14:52",
}
}];
if (events) {// 假設(shè)這里是讀取事件成功的回調(diào)
// 調(diào)用讀取事件成功的block
self.resolve(events);
} else {// 假設(shè)這里是讀取事件失敗的回調(diào)
// 調(diào)用讀取事件失敗的block
self.reject(@"failure", @"讀取日歷事件出錯", nil);
}
}
// 某個RN文件
CalendarManager.findEvents()
.then(events => {
console.log(events);
})
.catch(error => {
console.log(error);
});
這樣就能順利地完成OC類給原生模塊傳遞數(shù)據(jù)了击困,但是此處再說一遍這種OC類給原生模塊傳遞數(shù)據(jù)的方式中,OC類是被動的广凸,即只有原生模塊調(diào)用了某個JS方法阅茶,從而才觸發(fā)了OC類的某個方法,OC類才把數(shù)據(jù)給人傳遞過去了谅海。
那有沒有一種方式脸哀,OC類是主動的呢?即我OC類就是要給你原生模塊傳遞數(shù)據(jù)扭吁,你在那給我等著撞蜂。
2.4 OC類主動傳遞數(shù)據(jù)給原生模塊(本質(zhì)是OC給JS發(fā)送事件)
有的場景下盲镶,即便原生模塊沒有調(diào)用JS方法向我們OC類要數(shù)據(jù),但我們OC類就是有錢啊蝌诡,想給你啊溉贿。此時最好的實現(xiàn)方案就是繼承RCTEventEmitter
,實現(xiàn)suppportEvents
方法浦旱,并調(diào)用[self sendEventWithName: body:]
方法發(fā)送數(shù)據(jù)給原生模塊就可以了宇色。
// CalendarManager.h
#import <Foundation/Foundation.h>
// 導(dǎo)入RCTBridgeModule頭文件
#import <React/RCTBridgeModule.h>
// 導(dǎo)入RCTEventEmitter頭文件
#import <React/RCTEventEmitter.h>
// 遵循RCTBridgeModule協(xié)議
@interface CalendarManager : RCTEventEmitter <RCTBridgeModule>
@end
// CalendarManager.m
@implementation CalendarManager
{
// 原生模塊是否有監(jiān)聽者,用來優(yōu)化無監(jiān)聽情況下造成的額外開銷
bool hasListeners;
}
// 所有支持的事件颁湖,和原生模塊那邊約定好的事件名
- (NSArray<NSString *> *)supportedEvents
{
return @[@"EventReminder"];
}
// 原生模塊添加第一個監(jiān)聽者時會觸發(fā)該方法
- (void)startObserving
{
hasListeners = YES;
}
// 原生模塊的最后一個監(jiān)聽者移除時會觸發(fā)該方法
- (void)stopObserving
{
hasListeners = NO;
}
- (void)calendarEventReminderReceived:(NSNotification *)notification
{
if (hasListeners) {// 如果有監(jiān)聽者再發(fā)出事件
[self sendEventWithName:@"EventReminder" body:@{
@"name": @"生日聚會",
@"details": @{
@"location": @"六國飯點",
@"date": @"2019-08-13 06:14:52",
}
}];
}
}
@end
然后我們就可以去RN項目里宣蠕,用JS代碼創(chuàng)建一個包含該原生模塊的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);
}
);
// 不要忘了移除監(jiān)聽
componentWillUnmount(){
subscription.remove();
}
三. 多線程
RN在一個獨立的串行GCD隊列中調(diào)用原生模塊的方法爷狈。我們在為RN自定義原生模塊時植影,如果發(fā)現(xiàn)有耗時的操作(如文件讀寫、網(wǎng)絡(luò)操作等)涎永,就需要為這些操作新開辟一個線程來執(zhí)行思币,不然的話,這些耗時的操作會阻塞RN項目的線程羡微。
在OC類中實現(xiàn)- (dispatch_queue_t)methodQueue
方法就可以指定原生模塊的方法在哪個隊列中被執(zhí)行谷饿。比如一個原生模塊的所有操作都必須在主線程執(zhí)行,那應(yīng)當(dāng)這樣指定:
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
而如果一個操作需要花費很長時間妈倔,原生模塊不應(yīng)該阻塞住博投,而是應(yīng)當(dāng)聲明一個用于執(zhí)行操作的獨立隊列。舉個例子盯蝴,RCTAsyncLocalStorage
模塊創(chuàng)建了自己的一個queue
毅哗,這樣它在做一些較慢的磁盤操作的時候就不會阻塞住React本身的消息隊列:
- (dispatch_queue_t)methodQueue
{
return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
}
但是- (dispatch_queue_t)methodQueue
方法指定的隊列會被你模塊里的所有方法共享。所以如果你的方法中“只有一個”是耗時較長的(或者是由于某種原因必須在不同的隊列中運行的)捧挺,你可以專門在該函數(shù)體內(nèi)用dispatch_async
方法來在另一個隊列執(zhí)行虑绵,而不影響其他方法:
RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 在這里執(zhí)行長時間的操作
...
// 你可以在任何線程/隊列中執(zhí)行回調(diào)函數(shù)
callback(@[...]);
});
}
此外,我們要知道如果原生模塊中需要更新UI闽烙,我們也需要獲取主線程翅睛,然后在主線程中更新UI:
RCT_EXPORT_METHOD(updateUI)
{
dispatch_async(dispatch_get_main_queue(), ^{
// 刷新UI
});
}
RN原生模塊有關(guān)多線程的知識其實就這么點。