最近一個(gè) RN 項(xiàng)目中需要接入 VoIP 語(yǔ)音通話功能遂蛀,雖然在學(xué)校的時(shí)候?qū)W過(guò)了 Java栈戳,做過(guò)一個(gè) Android 小項(xiàng)目,但是后面就完全沒(méi)有接觸過(guò) Android 開(kāi)發(fā)了扫沼,對(duì) Java 的了解也停留在基礎(chǔ)和一點(diǎn) Spring Boot 上崇呵,而且缤剧,iOS 開(kāi)發(fā)是完全沒(méi)有接觸過(guò)的,也就學(xué)校上課學(xué)了點(diǎn) C域慷、C++荒辕。一開(kāi)始是十分抗拒的汗销,不過(guò)想到編程總歸是相通的,邊做邊學(xué)也能搞定抵窒,于是就開(kāi)始嘗試去對(duì)接弛针,最后在經(jīng)過(guò)差不多 4 天的努力下總算的完成了。
由于對(duì)原生開(kāi)發(fā)的不熟悉估脆,項(xiàng)目中還是遇到了一些坑的钦奋,所以想把這些經(jīng)歷記錄下來(lái)。此外疙赠,完全不懂原生開(kāi)發(fā)實(shí)在是不利于做 RN 項(xiàng)目付材,所以最近打算稍微補(bǔ)一下 iOS,至少讓我能看懂別人的代碼(想干的事情又變多了圃阳,有點(diǎn)擔(dān)心貪多吃不下??)厌衔。
說(shuō)明
從功能和 UI 上來(lái)說(shuō),基本上做成和微信語(yǔ)音通話一樣的捍岳,支持主叫富寿、被叫、掛斷锣夹、靜音页徐、揚(yáng)聲器以及后臺(tái)通話。
因?yàn)椴欢_(kāi)發(fā)银萍,所以 UI 希望是能通過(guò) JS 代碼實(shí)現(xiàn)变勇,然后功能就調(diào)用封裝好的 SDK。在研究了 SDK 文檔以及 Demo 代碼之后贴唇,基本上我們要做的功能 SDK 都能提供搀绣,并且只是簡(jiǎn)單的函數(shù)調(diào)函即可,iOS 的后臺(tái)通話以及來(lái)電監(jiān)聽(tīng) SDK 也提供了戳气,那我要做的其實(shí)就只是寫好頁(yè)面链患、封裝好原生模塊然后調(diào)用就好了。
Android
Android 部分的接入相對(duì)來(lái)說(shuō)還是很容易的瓶您,畢竟 java 語(yǔ)言我是熟悉的并且了解 Android 開(kāi)發(fā)的一些基礎(chǔ)麻捻。
RN 中的 Android 原生模塊是一個(gè)繼承了 ReactContextBaseJavaModule
的類,覆蓋父類的 getName
方法返回模塊的名字呀袱,然后通過(guò) @ReactMethod
注解導(dǎo)出方法給 js 層使用芯肤,方法的返回類型必須為void
。如果需要返回結(jié)果給 js压鉴,可以通過(guò)傳入 Callback
的回調(diào)函數(shù)形式或者使用 Promise
對(duì)象。通過(guò)覆蓋 getConstants
方法锻拘,可以導(dǎo)出常量給 js 使用油吭。SDK 中通話狀態(tài)的變化击蹲,可以通過(guò) RCTDeviceEventEmitter
發(fā)送事件給 js 層,然后 js 層進(jìn)行處理婉宰。封裝好的原生模塊差不多長(zhǎng)下面這樣:
public class SipModule extends ReactContextBaseJavaModule {
private DeviceEventManagerModule.RCTDeviceEventEmitter getEventEmitter() {
return this.getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
}
public SipModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "SipModule";
}
@ReactMethod
public void registerSip(String account, String password, String addr, String port) {
YephoneDevice.registerSip(account, password, addr + ":" + port);
YephoneDevice.setInCallActivity(MainActivity.class);
final DeviceEventManagerModule.RCTDeviceEventEmitter emitter = this.getEventEmitter();
YephoneDevice.setAccountState(new YephoneManager.YephoneAccountStateChangedListener() {
@Override
public void state(int state, String account, String message) {
Log.i("sip", "state:" + message);
emitter.emit("sipStateChange", state);
}
});
}
...
}
原生模塊封裝好以后歌豺,添加一個(gè) 使用了 ReactPackage
接口的 Package
類注冊(cè)該模塊,然后在 MainApplication.java
文件的 getPackages
方法中添加該 Package
即可心包。
在 JS 中类咧,通過(guò) NativeModules.SipModule
即可訪問(wèn)到添加的原生模塊,通過(guò) DeviceEventEmitter
可以注冊(cè) Android 原生端發(fā)來(lái)的事件(iOS 中稍不一樣)蟹腾。
遇到的問(wèn)題
問(wèn)題一:SDK 中要求調(diào)用 setInCallActivity
函數(shù)指定當(dāng)有來(lái)電時(shí)應(yīng)該顯示的頁(yè)面痕惋,設(shè)置好后 SDK 會(huì)在有來(lái)電時(shí)激活 App 并跳轉(zhuǎn)到該頁(yè)面,但是 RN 中只有一個(gè) MainActivicy
娃殖,如果添加新的 Activity
值戳,通話頁(yè)面就無(wú)法使用 js 來(lái)寫了。
這里我們希望的是在收到來(lái)電后 SDK 發(fā)出通知而不是直接進(jìn)行頁(yè)面跳轉(zhuǎn)(這確實(shí)做得太多了)炉爆,但是在于開(kāi)發(fā)商溝通后得到的結(jié)果是暫不支持堕虹。后來(lái),在與做 Android 開(kāi)發(fā)的小伙伴溝通后得知Acvicity
支持多種啟動(dòng)模式芬首,其中如果設(shè)置為 singleTop
模式赴捞,在啟動(dòng)該 Acvicity
時(shí)不會(huì)創(chuàng)建新的。這樣一來(lái)郁稍,我們只需要調(diào)用 setInCallActivity
將來(lái)電的頁(yè)面設(shè)置為 MainAcvicity
即可赦政,并且當(dāng)發(fā)生調(diào)轉(zhuǎn)時(shí)會(huì)調(diào)用 onNewIntent
生命周期函數(shù),我們可以在這里發(fā)出事件通知 js 有新的來(lái)電艺晴。
問(wèn)題二:電話接通后沒(méi)有聲音昼钻。
在完成了主叫和被叫的邏輯之后發(fā)現(xiàn)通話時(shí)沒(méi)有聲音,起初以為是設(shè)置的編碼方式不對(duì)出了問(wèn)題封寞,但是我使用的是和 Demo 中一樣的編碼然评,并且后面測(cè)試發(fā)現(xiàn) Demo 通話時(shí)也沒(méi)有聲音。原本打算聯(lián)系 SDK 開(kāi)發(fā)商解決狈究,不過(guò)后面突然想到在使用的過(guò)程中沒(méi)有提示請(qǐng)求麥克風(fēng)權(quán)限碗淌,猜測(cè)是不是與這有關(guān)。于是抖锥,在核實(shí)了沒(méi)有麥克風(fēng)權(quán)限之后亿眠,手動(dòng)打開(kāi)權(quán)限再進(jìn)行測(cè)試便可以正常通話了。最終磅废,在 js 中添加了 Android 權(quán)限申請(qǐng)的代碼纳像,解決了該問(wèn)題。
iOS
iOS 部分的接入相較于 Android 中就稍微麻煩了一點(diǎn)拯勉,首先是 Objective-C 語(yǔ)法不熟悉竟趾,然后也不太會(huì) Xcode 的使用憔购,我甚至都不知道該如何導(dǎo)入 SDK。
由于不太懂岔帽,所以我只好照著 SDK 文檔中說(shuō)的進(jìn)行操作玫鸟。首先將 SDK 目錄復(fù)制到 iOS 項(xiàng)目目錄下,然后在 Xcode 中右鍵項(xiàng)目名選擇 Add File To...
犀勒,選擇剛剛復(fù)制過(guò)來(lái)的文件夾屎飘。然后在 Build Phases->Link Binary With Libraries
中添加 SDK 需要的依賴,接著在 Capabilities
中啟用 Background Modes
以支持后臺(tái)通話贾费,最后再 Build Settings
中關(guān)閉了 Bitcode
(因?yàn)?SDK 不支持)钦购。其中 Bitcode 我了解到是編輯器編譯過(guò)程中的一種中間碼,先將 C铸本、OC 等高級(jí)編程語(yǔ)言轉(zhuǎn)換成 Bitcode肮雨,然后在將 Bitcode 轉(zhuǎn)換成不同 CPU 架構(gòu)上的匯編或機(jī)器碼。
根據(jù)文檔添加好 SDK 之后箱玷,我先嘗試編譯了一下怨规,然后編譯時(shí)卻報(bào)錯(cuò)了,如下圖所示锡足。
從錯(cuò)誤日志上可以看出是因?yàn)橛兄貜?fù)的 Symbol 導(dǎo)致的波丰,參考這篇博客使用拆分庫(kù)然后刪除對(duì)應(yīng)的 .o
文件解決了該問(wèn)題。
原生模塊
編譯通過(guò)之后舶得,就需要添加 iOS 原生模塊了掰烟。一個(gè) iOS 模板就是一個(gè)使用了 RCTBridgeModule
的 Objective-C 類。為了實(shí)現(xiàn)RCTBridgeModule
沐批,類中需要包含 RCT_EXPORT_MODULE()
宏纫骑。這個(gè)宏也可以添加一個(gè)參數(shù)用來(lái)指定在 Javascript 中訪問(wèn)這個(gè)模塊的名字。如果不指定九孩,默認(rèn)就會(huì)使用這個(gè)類的名字先馆。通過(guò) RCT_EXPORT_METHOD()
宏可以聲明要給 Javascript 導(dǎo)出的方法。
// SipModule.h
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
#import "YeCallEventDelegate.h"
@interface SipModule : RCTEventEmitter <RCTBridgeModule, YeCallEventDelegate>
@property (nonatomic,strong) NSString* callID;
@end
// SipModule.m
#import "SipModule.h"
#import "YePhoneManager.h"
#import <Foundation/Foundation.h>
#import <React/RCTLog.h>
@implementation SipModule
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(registerSip:(NSString*)sipAccount sipAccPwd:(NSString*)sipAccPwd host:(NSString*)host port:(NSString*)port){
NSString * sipProxy = [[NSString alloc] initWithFormat:@"%@:%@", host, port];
[[YePhoneManager instance] registed:sipAccount sipAccPwd:sipAccPwd sipServerAddr:host sipProxy:sipProxy];
}
RCT_EXPORT_METHOD(answer){
[[YePhoneManager instance] acceptCall:_callID];
}
...
多線程
參考官網(wǎng)文檔躺彬,原生模塊可以指定自己想在哪個(gè)隊(duì)列中被執(zhí)行煤墙。如果模塊需要調(diào)用一些必須在主線程才能使用的API,那應(yīng)當(dāng)這樣指定:
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
類似的宪拥,如果一個(gè)操作需要花費(fèi)很長(zhǎng)時(shí)間仿野,原生模塊不應(yīng)該阻塞住,而是應(yīng)當(dāng)聲明一個(gè)用于執(zhí)行操作的獨(dú)立隊(duì)列:
- (dispatch_queue_t)methodQueue
{
return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
}
給 Javascript 發(fā)送事件
通過(guò)繼承 RCTEventEmitter
她君,實(shí)現(xiàn) suppportEvents
方法并調(diào)用 self sendEventWithName
:
- (NSArray<NSString *> *)supportedEvents{
return @[@"sipStateChange", @"sipOutRing", @"sipCallStart", @"sipCallFail", @"sipCallEnd", @"sipCallIn"];
}
- (NSString*)registrationOk:(NSString*)registrationOk{
[self sendEventWithName:@"sipStateChange" body:@1];
return registrationOk;
}
JavaScript代碼可以創(chuàng)建一個(gè)包含對(duì)應(yīng)模塊的 NativeEventEmitter
實(shí)例來(lái)訂閱這些事件:
import { NativeEventEmitter, DeviceEventEmitter, NativeModules } from 'react-native'
import { IS_ANDROID } from '../../utils/device';
const { SipModule } = NativeModules
export const sipEventEmitter = IS_ANDROID ? DeviceEventEmitter : new NativeEventEmitter(SipModule)
export default SipModule
總結(jié)
React Native 項(xiàng)目開(kāi)發(fā)過(guò)程中脚作,如果需要接入原生模塊或者原生 UI 組件,對(duì)于前端開(kāi)發(fā)者來(lái)說(shuō)確實(shí)不太友好缔刹,但是也并不是完全不能解決鳖枕。不過(guò)魄梯,如果能具備基本的原生開(kāi)發(fā)知識(shí),做起來(lái)也會(huì)更加事半功倍宾符,能想到更好的解決方案。