iOS使用VOIP與CallKit實現體驗優質的網絡通信功能

iOS使用VOIP與CallKit實現體驗優質的網絡通信功能

    VOIP是Apple提供給開發者的網絡電話功能接口。簡單來講,其可讓你的應用程序在徹底殺死的狀況下被服務端喚醒。CallKit是iOS10引入的新框架,使用它可讓你的應用程序調用系統的通話和通話記錄界面。試想一下,用戶能夠在鎖屏,應用被殺死,應用在後臺等狀況下收到通信請求而且彈出系統的通話界面進行交互是多麼酷的一件事。ios

1、建立VOIP推送證書

    VOIP說是一種網絡電話服務,其實質是一種特殊的長鏈接,使用它每一個網絡電話類APP不須要本身單獨進行保活維護,在進行通話請求時,只須要發送一條VOIP推送,VOIP推送會將應用程序拉起,以後由應用程序處理通信邏輯。VOIP也是Push的一種,只是其是一種特殊的Push,普通的Push當應用被殺死後能夠收到,可是用戶點擊Push消息前應用程序是不會被激活的,VOIP則否則,能夠直接激活應用。數組

    VOIP推送證書的建立方式與普通推送證書的建立方式基本一致,首先須要生成certSigningRequest文件,打開鑰匙串應用:緩存

在證書助理欄選擇從證書頒發機構申請證書:網絡

填寫相關資料後,將生成的文件保存:app

在Apple開發者中心建立新的證書,證書類型選擇生產環境的VOIP服務證書:框架

須要注意,普通的推送分開發環境和生產環境,VOIP證書不進行區分,生產環境和開發環境是通用的。以後選擇一個AppID而且上傳前面生成的certSigningRequest文件來完成VOIP證書的建立。ide

    建立完成後,在證書列表能夠看到多了一個VOIP服務證書,能夠加載此證書進行VOIP推送。函數

2、PushKit詳析

    咱們知道,客戶端若想要接收普通的Push消息,是須要註冊Token,經過Token來進行個推的。VOIP推送也是同樣的,只是這類推送須要使用PushKit框架。ui

    首先須要用到PKPushRegistey類,這個類進行推送的相關配置和Token的申請:atom

@interface PKPushRegistry : NSObject
//代理對象
@property (readwrite,weak,nullable) id<PKPushRegistryDelegate> delegate;
//目標推送類型
/*
PK_EXPORT PKPushType const PKPushTypeVoIP NS_AVAILABLE_IOS(8_0);//VOIP推送
PK_EXPORT PKPushType const PKPushTypeComplication NS_AVAILABLE_IOS(9_0);//Watch更新
PK_EXPORT PKPushType const PKPushTypeFileProvider NS_AVAILABLE_IOS(11_0);//文件傳輸
*/
@property (readwrite,copy,nullable) NSSet<PKPushType> *desiredPushTypes;
//獲取本地緩存的Token  申請Token執行回調後 這個方法能夠直接獲取緩存
- (nullable NSData *)pushTokenForType:(PKPushType)type;
//初始化,並設置工做線程
- (instancetype)initWithQueue:(nullable dispatch_queue_t)queue NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
@end

PKPushRegistryDelegate相關函數意義以下:

//申請Token更新後回調
/*
PKPushCredentials是證書對象,其中屬性以下:
@property (readonly,copy) PKPushType type;//推送類型
@property (readonly,copy) NSData *token; //Token
*/
- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)pushCredentials forType:(PKPushType)type;
//收到推送後執行的回調
/*
PKPushPayload爲推送信息 其中屬性以下:
@property (readonly,copy) PKPushType type; //推送類型
@property (readonly,copy) NSDictionary *dictionaryPayload; //服務端發來的信息
*/
- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type NS_DEPRECATED_IOS(8_0, 11_0);
//做用同上,最後的block須要在邏輯處理完成後主動回調
- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void(^)(void))completion NS_AVAILABLE_IOS(11_0);
//Token失效的回調
- (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(PKPushType)type;

若是配置成功,在收到VOIP推送時,不管應用程序是否活躍,都會執行代理函數,咱們即可以在其中進行邏輯處理。

3、關於CallKit框架

    CallKit框架是iOS10後系統提供的一套網絡電話UI和交互相關接口,應用程序能夠調用系統的電話界面來進行邏輯傳遞。下圖比較形象的表達了應用程序與CallKit的關係:

以收到網絡電話爲例,若是應用程序在前臺,客戶端能夠直接處理通信邏輯,若是應用程序不在前臺,服務端能夠發送一條VOIP推送喚醒APP,以後APP通知CallKit框架來喚起系統的通信界面。CXProvider類主要負責系統服務於APP之間的交互。例如能夠經過它來更新通話界面,顯示通話的來自方,當用戶點擊通話界面的某些按鈕後,也經過它來通知APP作邏輯處理。

    須要注意,上圖在CallKit和System之間有兩個雙向的白色箭頭,這描述了CallKit和系統交互的四個方向。

    首先,App想要和系統交互,例如接收到VOIP通知後彈出通話界面,須要使用CXProvider經過CXCallUpdate來進行控制。以下圖:

     以後系統會將一些用戶操做經過CSAction傳遞會APP,以下:

    APP中進行的操做若是須要通知系統,須要使用CXCallController經過CXTransaction傳遞。例如App內的通信須要添加到系統的歷史通話列表。以下:

 

1.先來看CXProvider類

    CXProvider類用來對系統通話界面進行一些配置操做,並處理回調邏輯,解析以下:

//初始化方法 使用CXProviderConfiguration來進行配置 後面會介紹
- (instancetype)initWithConfiguration:(CXProviderConfiguration *)configuration NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
//設置代理與代理函數所工做的線程
- (void)setDelegate:(nullable id<CXProviderDelegate>)delegate queue:(nullable dispatch_queue_t)queue;
//向系統發起一個新的通話請求
/*
UUID爲此通話請求的標識 可使用它來關閉通話
update設置界面的更新參數
*/
- (void)reportNewIncomingCallWithUUID:(NSUUID *)UUID update:(CXCallUpdate *)update completion:(void (^)(NSError *_Nullable error))completion;
//結束某個通話 使用上面的UUID做爲標識
/*
//通話結束的緣由設置
typedef NS_ENUM(NSInteger, CXCallEndedReason) {
    CXCallEndedReasonFailed = 1, // 通話服務失敗
    CXCallEndedReasonRemoteEnded = 2, // 對方掛斷
    CXCallEndedReasonUnanswered = 3, // 超時 對方爲接聽
    CXCallEndedReasonAnsweredElsewhere = 4, // 通話在其餘設備接聽
    CXCallEndedReasonDeclinedElsewhere = 5, // 通話在其餘設備拒絕
} API_AVAILABLE(ios(10.0));
*/
- (void)reportCallWithUUID:(NSUUID *)UUID endedAtDate:(nullable NSDate *)dateEnded reason:(CXCallEndedReason)endedReason;
//更新通話對方的信息
- (void)reportCallWithUUID:(NSUUID *)UUID updated:(CXCallUpdate *)update;
//調用這個函數來進行通話呼出開始
- (void)reportOutgoingCallWithUUID:(NSUUID *)UUID startedConnectingAtDate:(nullable NSDate *)dateStartedConnecting;
//調用這個函數來進行通話呼出鏈接完成
- (void)reportOutgoingCallWithUUID:(NSUUID *)UUID connectedAtDate:(nullable NSDate *)dateConnected;
//配置對象
@property (nonatomic, readwrite, copy) CXProviderConfiguration *configuration;
//調用此函數來將通話失效
- (void)invalidate;
//全部未完成的事物
@property (nonatomic, readonly, copy) NSArray<CXTransaction *> *pendingTransactions;
- (NSArray<__kindof CXCallAction *> *)pendingCallActionsOfClass:(Class)callActionClass withCallUUID:(NSUUID *)callUUID;

2.在看CXProviderConfiguration類

    這個類用來進行Provider的配置,例如設置通信服務名稱,鈴聲,圖標,是否支持組等。解析以下:

//設置服務名稱
@property (nonatomic, readonly, copy) NSString *localizedName;
//設置鈴聲  資源必須在 app的 bundle裏
@property (nonatomic, strong, nullable) NSString *ringtoneSound;
//設置應用圖標
@property (nonatomic, copy, nullable) NSData *iconTemplateImageData;
//設置最大支持的組數 默認爲2
@property (nonatomic) NSUInteger maximumCallGroups;
//設置最大的每組人數 默認爲5
@property (nonatomic) NSUInteger maximumCallsPerCallGroup;
//設置是否將通話記錄保存進最近通話列表
@property (nonatomic) BOOL includesCallsInRecents;
//設置是否支持視頻通話
@property (nonatomic) BOOL supportsVideo;
//設置支持的操做類型
@property (nonatomic, copy) NSSet<NSNumber *> *supportedHandleTypes;

當App接收到來電VOIP通知時,可使用CXCallUpdate來更新狀態喚出通話界面。

3.CXCallUpdate類

//遠程操做對象 若是是接收方 則此爲呼叫方 若是是呼叫方 則此爲接收方
@property (nonatomic, copy, nullable) CXHandle *remoteHandle;
//名稱
@property (nonatomic, copy, nullable) NSString *localizedCallerName;
//是否支持暫時掛起
@property (nonatomic) BOOL supportsHolding;
//是否支持組
@property (nonatomic) BOOL supportsGrouping;
//是否支持非組通話
@property (nonatomic) BOOL supportsUngrouping;
//是否支持DTMF
@property (nonatomic) BOOL supportsDTMF;
//是否包含視頻
@property (nonatomic) BOOL hasVideo;

CXHandle中來定義操做的類型,解析以下:

//類型
/*
typedef NS_ENUM(NSInteger, CXHandleType) {
    CXHandleTypeGeneric = 1,//通用
    CXHandleTypePhoneNumber = 2,//電話
    CXHandleTypeEmailAddress = 3,//郵箱地址
} API_AVAILABLE(ios(10.0));
*/
@property (nonatomic, readonly) CXHandleType type;
//值
@property (nonatomic, readonly, copy) NSString *value;

- (instancetype)initWithType:(CXHandleType)type value:(NSString *)value NS_DESIGNATED_INITIALIZER;

下面給出了簡單的當被叫收到VOIP後調起通話界面的代碼:

CXCallUpdate * callUpdate = [[CXCallUpdate alloc]init];
callUpdate.supportsGrouping = YES;
callUpdate.supportsDTMF = YES;
callUpdate.hasVideo = YES;
callUpdate.supportsHolding = YES;
[callUpdate setLocalizedCallerName:nickName];
CXHandle * handle = [[CXHandle alloc]initWithType:CXHandleTypePhoneNumber value:from];
callUpdate.remoteHandle = handle;
 [[self shareInstance].callProvider reportNewIncomingCallWithUUID:[self shareInstance].uuid update:callUpdate completion:^(NSError * _Nullable error) {
     LOG(@"吊起界面");
}];

鎖屏和應用程序在後臺的效果分別以下所示:

               

4.CXProviderDelegate相關函數解析

    CXProviderDelegate中的相關函數用來處理系統通話界面的某些操做回調給應用程序。

//當接收到呼叫重置時 調用的函數,這個函數必須被實現,其不需作任何邏輯,只用來重置狀態
- (void)providerDidReset:(CXProvider *)provider;
//呼叫開始時回調 
- (void)providerDidBegin:(CXProvider *)provider;
//音頻會話激活狀態的回調
- (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession;
//音頻會話停用的回調
- (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession;
//行爲超時的回調 
- (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)action;
//有事務被提交時調用 
//若是返回YES 則表示事務被捕獲處理 後面的回調都不會調用 若是返回NO 則表示事務不被捕獲,會回調後面的函數
- (BOOL)provider:(CXProvider *)provider executeTransaction:(CXTransaction *)transaction;
//點擊開始按鈕的回調
- (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action;
//點擊接聽按鈕的回調
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action;
//點擊結束按鈕的回調
- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action;
//點擊保持通話按鈕的回調
- (void)provider:(CXProvider *)provider performSetHeldCallAction:(CXSetHeldCallAction *)action;
//點擊靜音按鈕的回調
- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action;
//點擊組按鈕的回調
- (void)provider:(CXProvider *)provider performSetGroupCallAction:(CXSetGroupCallAction *)action;
//DTMF功能回調
- (void)provider:(CXProvider *)provider performPlayDTMFCallAction:(CXPlayDTMFCallAction *)action;

須要注意,上面的最後幾個回調中CXStartCallAction都會提供一個fullfill的函數,當處理完成回調邏輯後,開發者須要手動調用此函數來通知系統。一樣,其中還有一個fail和timeout函數,調用它要通知系統此行爲執行失敗和超時。

5.CXCallController解析

    當用戶在應用程序內部進行的通信操做時,可使用這個類來通知系統。

//初始化方法
- (instancetype)init;
- (instancetype)initWithQueue:(dispatch_queue_t)queue;
//通信監聽
@property (nonatomic, readonly, strong) CXCallObserver *callObserver;
//發起一個事務請求 CXProvider以後會接收到請求 進行邏輯
- (void)requestTransaction:(CXTransaction *)transaction completion:(void (^)(NSError *_Nullable error))completion;
//經過行爲發起事務
- (void)requestTransactionWithActions:(NSArray<CXAction *> *)actions completion:(void (^)(NSError *_Nullable error))completion API_AVAILABLE(ios(11.0));
- (void)requestTransactionWithAction:(CXAction *)action completion:(void (^)(NSError *_Nullable error))completion API_AVAILABLE(ios(11.0));

6.CXTransaction類

    CXTransaction是封裝了行爲的事務。

//惟一 ID
@property (nonatomic, readonly, copy) NSUUID *UUID;
//行爲完成後的回調
@property (nonatomic, readonly, assign, getter=isComplete) BOOL complete;
//行爲數組
@property (nonatomic, readonly, copy) NSArray<__kindof CXAction *> *actions;
//初始化函數
- (instancetype)initWithActions:(NSArray<CXAction *> *)actions;
- (instancetype)initWithAction:(CXAction *)action;
//添加行爲
- (void)addAction:(CXAction *)action;

4、進行來電攔截與號碼識別

    上面咱們介紹了使用CallKit框架來實現的通信功能,有通信功能就不免須要進行聯繫人識別與黑名單。CallKit框架中還有一部份內容能夠結合Call Directory Extension來實現號碼攔截與識別。

    首先建立一個擴展Target,選擇Call Directory Extension:

建立好Target工程後,其實須要的核心代碼Xcode已經幫咱們都生成。

    第一步,須要在主APP中進行號碼服務的驗證和更新,

_manager = [[CXCallDirectoryManager alloc]init];
    [_manager getEnabledStatusForExtensionWithIdentifier:@"jaki.CallKitTest.Ex" completionHandler:^(CXCallDirectoryEnabledStatus enabledStatus, NSError * _Nullable error) {
        if (enabledStatus==CXCallDirectoryEnabledStatusEnabled) {
            NSLog(@"容許");
        }else{
            NSLog(@"請開啓");
        }
    }];
    [_manager reloadExtensionWithIdentifier:@"jaki.CallKitTest.Ex" completionHandler:^(NSError * _Nullable error) {
        NSLog(@"刷新配置");
    }];

一般狀況下,當用戶在主APP中進行添加聯繫人,登陸,切換帳戶等操做後,須要通知擴展程序進行號碼庫的更新,固然,通常在號碼庫更新時須要從主APP傳遞數據給擴展,咱們能夠經過Group來實現,這裏再也不展開。

    工程運行後,會在用戶的「設置->電話->來電組織與身份識別」項目中看到擴展程序:

當用戶打開此服務或者調用上面的reloadExtension時,會從執行擴展程序的相關方法來從新加載號碼庫。須要注意,reloadExtension函數中的id參數爲擴展項目的bundleID,不是主項目的。

    在擴展工程的info.plist文件中,默認配置好了處理來電的操做類,若是要自定義,須要開發者手動修改:

默認的CallDirectoryHandler類爲來電攔截與身份識別的操做類,其集成自CXCallDirectoryProvider類,當收到加載號碼庫的請求時,會執行下面的函數:

- (void)beginRequestWithExtensionContext:(CXCallDirectoryExtensionContext *)context {
    context.delegate = self;
    //是否支持增量更新
    if (context.isIncremental) {
        [self addOrRemoveIncrementalBlockingPhoneNumbersToContext:context];

        [self addOrRemoveIncrementalIdentificationPhoneNumbersToContext:context];
    } else {
        [self addAllBlockingPhoneNumbersToContext:context];

        [self addAllIdentificationPhoneNumbersToContext:context];
    }
    //完成更新操做
    [context completeRequestWithCompletionHandler:nil];
}

上面是Xcode默認提供的實現,十分優雅,在iOS11後,號碼庫的更新支持增量,因此這裏進行的區分。

    CXCallDirectoryExtensionContext是一個操做上下文,經過它能夠像號碼庫中添加刪除數據。解析以下:

//是否支持增量更新
@property (nonatomic, readonly, getter=isIncremental) BOOL incremental API_AVAILABLE(ios(11.0));
//添加一個黑名單號碼
- (void)addBlockingEntryWithNextSequentialPhoneNumber:(CXCallDirectoryPhoneNumber)phoneNumber;
//移除一個黑名單號碼
- (void)removeBlockingEntryWithPhoneNumber:(CXCallDirectoryPhoneNumber)phoneNumber API_AVAILABLE(ios(11.0));
//移除全部的黑名單號碼
- (void)removeAllBlockingEntries API_AVAILABLE(ios(11.0));
//添加一個身份識別
- (void)addIdentificationEntryWithNextSequentialPhoneNumber:(CXCallDirectoryPhoneNumber)phoneNumber label:(NSString *)label;
//移除一個身份識別
- (void)removeIdentificationEntryWithPhoneNumber:(CXCallDirectoryPhoneNumber)phoneNumber API_AVAILABLE(ios(11.0));
//移除全部身份識別
- (void)removeAllIdentificationEntries API_AVAILABLE(ios(11.0));
//完成操做後 須要手動調用此函數
- (void)completeRequestWithCompletionHandler:(nullable void (^)(BOOL expired))completion;

添加了黑名單後,用戶將收不到此號碼的電話,一樣,設置了身份識別後,當用戶播出前,會顯示設置的身份信息(須要注意,大陸號碼須要前面帶86),以下:

相關文章
相關標籤/搜索