混合開發(fā):RN調(diào)用原生模塊

目錄

一. 前言
二. 示例: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類,其中RCTReaCT的縮寫蜕劝。

// 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 <===> NSIntegerfloat误墓、double邦尊、CGFloatNSNumber
  • boolean <===> BOOL优烧、NSNumber
  • array <===> NSArray
  • object <===> NSDictionary
  • function <===> RCTResponseSenderBlock
  • promise <===> RCTPromiseResolveBlockRCTPromiseRejectBlock

除以上幾種之外链峭,橋接通道就不能傳遞其它數(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(RCTPromiseResolveBlockRCTPromiseRejectBlock)兩種方式來給原生模塊傳遞數(shù)據(jù)艾扮。但Promise使用起來代碼比較清晰既琴,我們推薦使用Promise,所以就不去演示回調(diào)函數(shù)那種方式了泡嘴,而僅僅演示Promise這種方式甫恩。

這種方式是指,如果OC方法的最后兩個參數(shù)是RCTPromiseResolveBlockRCTPromiseRejectBlock(兩者必須同時存在)磕诊,那么橋接后的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)多線程的知識其實就這么點。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末黑竞,一起剝皮案震驚了整個濱河市捕发,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌很魂,老刑警劉巖扎酷,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異遏匆,居然都是意外死亡霞玄,警方通過查閱死者的電腦和手機骤铃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來坷剧,“玉大人,你說我怎么就攤上這事喊暖”蛊螅” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵陵叽,是天一觀的道長狞尔。 經(jīng)常有香客問我,道長巩掺,這世上最難降的妖魔是什么偏序? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮胖替,結(jié)果婚禮上研儒,老公的妹妹穿的比我還像新娘。我一直安慰自己独令,他們只是感情好端朵,可當(dāng)我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著燃箭,像睡著了一般冲呢。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上招狸,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天敬拓,我揣著相機與錄音,去河邊找鬼裙戏。 笑死乘凸,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的挽懦。 我是一名探鬼主播翰意,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼信柿!你這毒婦竟也來了冀偶?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤渔嚷,失蹤者是張志新(化名)和其女友劉穎进鸠,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體形病,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡客年,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年霞幅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片量瓜。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡司恳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出绍傲,到底是詐尸還是另有隱情扔傅,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布烫饼,位于F島的核電站猎塞,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏杠纵。R本人自食惡果不足惜荠耽,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望比藻。 院中可真熱鬧铝量,春花似錦、人聲如沸韩容。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽群凶。三九已至插爹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間请梢,已是汗流浹背赠尾。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留毅弧,地道東北人气嫁。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像够坐,于是被迫代替她去往敵國和親寸宵。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容