做者:镇雷html
苹果在WWDC2016推出了iOS10系统新功能CallKit framework,代替了原来的CoreTelephony.framework,能够调起系统的接听页进行音视频通话;iOS8中苹果新引入了PushKit的框架和一种新的push通知类型:VoIP push,提供区别于普通APNS push的能力,经过这种push方式收到消息时会直接将已经杀掉的APP激活,两个库配合使用造成了一套完整的VoIP解决方案。因为CallKit支持版本较高,并且限定了应用场景,目前集成的APP不是不少,官方文档和网上博客对相关功能介绍细节都颇有限,这篇文章主要为了记录一下项目过程当中遇到的问题。数组
==========bash
效果图以下,由于CallKit使用的是系统原生的控件, iOS10与iOS11的样式上有区别:session
==========app
闲鱼调用的逻辑图以下: 框架
==========ide
下面是CallKit和PushKit这两个库的简单介绍:测试
CallKit主要有:CXProvider、CXCallController、CXProviderConfiguration这三个类,使用时须要新建一个CallKit管理类并实现CXProviderDelegate协议。 实现步骤以下:fetch
1,设置CXProviderConfiguration优化
static CXProviderConfiguration* configInternal = nil;
configInternal = [[CXProviderConfiguration alloc] initWithLocalizedName:@"闲鱼"];
configInternal.supportsVideo = true;
configInternal.maximumCallsPerCallGroup = 1;
configInternal.maximumCallGroups = 1;
configInternal.supportedHandleTypes = [[NSSet alloc] initWithObjects:[NSNumber numberWithInt:CXHandleTypeGeneric],[NSNumber numberWithInt:CXHandleTypePhoneNumber], nil];
UIImage* iconMaskImage = [UIImage imageNamed:@"IconMask"];
configInternal.iconTemplateImageData = UIImagePNGRepresentation(iconMaskImage);
复制代码
2,初始化CXProvider与CXCallController
self.provider = [[CXProvider alloc] initWithConfiguration: configInternal];
[provider setDelegate:self queue:dispatch_get_main_queue()];
self.callController = [[CXCallController alloc] initWithQueue:dispatch_get_main_queue()];
复制代码
3,实现通话流程或按钮的回调方法(每一个回调结束的时候要执行[action fulfill];不然会提示通话失败)
- (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;
- (void)provider:(CXProvider *)provider performPlayDTMFCallAction:(CXPlayDTMFCallAction *)action;
……
复制代码
4,实现呼起电话和结束电话的方法
- (void)reportIncomingCallWithTitle:(NSString *)title Sid:(NSString *)sid{
CXCallUpdate* update = [[CXCallUpdate alloc] init];
update.supportsDTMF = false;
update.supportsHolding = false;
update.supportsGrouping = false;
update.supportsUngrouping = false;
update.hasVideo = false;
update.remoteHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:sid];
update.localizedCallerName = title;
NSUUID *uuid = [NSUUID UUID];
//弹出电话页面
[self.provider reportNewIncomingCallWithUUID:uuid update:update completion:^(NSError * _Nullable error) {
}];
}
复制代码
- (void)endCallAction {
CXEndCallAction* endCallAction = [[CXEndCallAction alloc] initWithCallUUID:self.currentCall];
CXTransaction* transaction = [[CXTransaction alloc] init];
[transaction addAction:endCallAction];
//关闭电话页面
[_callController requestTransaction:transaction completion:^(NSError * _Nullable error) {
}];
}
复制代码
PushKit主要有3步操做:
1,经过PKPushRegistry注册VoIP服务(通常在APP启动代码里添加)
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
PKPushRegistry *pushRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
pushRegistry.delegate = self;
pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
return YES;
}
复制代码
2,实现PKPushRegistryDelegate获取token方法
- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type {
NSString *str = [NSString stringWithFormat:@"%@",credentials.token];
NSString *tokenStr = [[[str stringByReplacingOccurrencesOfString:@"<" withString:@""]
stringByReplacingOccurrencesOfString:@">" withString:@""] stringByReplacingOccurrencesOfString:@" " withString:@""];
//上传token处理
}
复制代码
3,实现PKPushRegistryDelegate接收VoIP消息方法
- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type {
NSDictionary *alert = [payload.dictionaryPayload[@"aps"] objectForKey:@"alert"];
//调用CallKit处理
}
复制代码
==========
在作VoIP方案时可能会遇到的问题:
Q:锁屏时收不到VoIP消息的问题
A:开发时遇到一个非锁屏下能正常收到VoIP push,但锁屏时常常收不到的问题,经排查,是锁屏下收到VoIP时APP发生了crash,crash日志里显示的缘由是Termination Reason: Namespace SPRINGBOARD,Code 0x8badf00d,这个错误是由于watchdog超时引发,程序启动时,超过了5-6秒APP会被系统杀掉,而系统在锁屏的状态下启动要比激活状态慢不少,很容易触发watchdog的crash。解决的方法就是优化APP启动时的代码,把能够延后的操做尽可能延后执行,同时我对设备的cpu也作的了判断,armv7的低端设备启动慢容易超时不使用VoIP,保留APNS发送。
Q:APP启动时收不到VoIP token问题
A:要接收VoIP token 除了要引入PushKit库,注册并实现代理外,还要在工程的Capabilities中打开3个backmode:Background fetch、Remote nofications、Voice over IP,以及Push Notifications(在工程里打开设置,和手机里设置的接收通知权限没有关系,即便用户将设置里的APNS关闭也能收到VoIP消息)。
Q:获取点击通话记录事件问题
A:收到的VoIP电话,会出如今系统通话记录里,点击通话记录,会执行回调
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * __nullable))restorationHandler
复制代码
外部连接唤起都会执行这个方法,须要再根据userActivity.activityType的值(INStartAudioCallIntent或INStartVideoCallIntent,取决于你在唤起CallKit时CXCallUpdate设置的hasVideo值)来判断是点击通话记录行为。
在通话记录详情里,有我的社交资料,这里的值是经过CXCallUpdate的remoteHandle带过去的,这个值通常用一个惟一而又不敏感的值(避免使用电话号码)用于回拨,咱们使用的是IM会话的sessionId。
Q:无声问题
A:主要是在接通的时候在performAnswerCallAction方法里将AVAudioSession设置setCategory为PlayAndRecord。(双方都须要将AVAudioSession设置为PlayAndRecord)结束以后关闭音频,去初始化。
Q:facetime 按钮隐藏问题
A:由于对方极可能没有登陆或是安卓手机,facetime大部分状况下是没法接通的,但接听页中的这个按钮是没法隐藏的,不过能够替换为本身的视频按钮,经过将CXProviderConfiguration的supportsVideo设为true,facetime按钮位置就会显示为视频,点击后跳转进入APP,并会触发外部跳转连接方法
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * __nullable))restorationHandler
复制代码
userActivity.activityType的值是INStartVideoCallIntent(也就是说若是你在CXCallUpdate设置的hasVideo值为true的时候,将没法区分这个回调是点击接听页视频跳转进来触发的仍是点击通话记录跳转进来触发的,因此建议hasVideo设置为false),咱们再经过这个回调打开闲鱼音视频通话的视频开关。
Q:埋点问题
A:锁屏接听页上有6个按钮,分别为:静音、拨号键盘、免提、添加通话、视频、闲鱼(自定义按钮,点击跳转进入APP),再给各个按钮设置埋点的时候遇到这个问题:CallKit只提供了静音和添加通话的回调方法,点击视频按钮能够在外部跳转连接方法获取到,其余按钮都没有相应的回调,免提键只能经过监听AVAudioSessionPortOverride值的变化来获取,拨号键盘和跳转进入APP的自定义按钮没法获取点击事件。
Q:兼容老版本问题
A:由于PushKit是从iOS8开始支持,CallKit是从iOS10开始支持,这两个库的调用都须要作版本保护,咱们但愿的是iOS10之前的版本都保留APNS来通知,iOS8和9的设备即便收到VoIP消息也没法唤起CallKit功能,因而咱们和消息中心的同窗定的规则是:有要发送push的请求时先查询到用户表里有没有VoIP token,没有token时仍然发送APNS消息,客户端会判断系统版本,若是是iOS10以前的咱们客户端就不上传VoIP token。
Q:VoIP证书问题
A:申请的方法同APNS证书,在苹果开发中心申请,VoIP证书没有像APNS证书那样区分开发证书与发布证书,两种场景通用一个证书,生成消息服务端使用的p12证书的流程也和APNS同样,须要注意的申请VoIP证书的bundleID须要提早配置好APNS证书。
Q:免提键闪烁,失效问题
A:免提键默认关闭,会监听APP里AVSession的AVAudioSessionPortOverride值,咱们原来有一个逻辑是链接中是扬声器模式,链接成功后切换为听筒模式,会致使用户在接听过程当中接听页上的按钮闪烁,用户在链接中作的免提操做失效问题,因此要保持整个通话流程里APP里不要改变扬声器的设置。
Q:自定义按钮上的icon设置问题
A:自定义按钮用的iconMask是图片的剪影,原有的icon图片放上去显示是一个白色的方块,须要把图片背景抠除,保存为有alpha通道的png图片
Q:审核问题
A:最近App Store审核变的更加严格,提交审核时除了提供两个能够正常通话的测试帐号外最好再提供一个相关功能的演示视频,而且演示视频里要有APP被杀掉,而后再收到VoIP通知打开的操做。
========= 扩展
苹果在推出CallKit的时候就将这两个库绑定介绍,其实是两个能够独立调用的库,除了基本的视频通话功能,CallKit和PushKit分别有其余的扩展应用:
CallKit能够用做通信录扩展功能,用来屏蔽骚扰电话,好比在IM里拉黑了某个用户,能够同时将他的手机号码屏蔽,实现方法以下:
1,建立一个target,选择Call Directory Extension
2,主程序中获取受权状态和保存须要拦截的号码
CXCallDirectoryManager *manager = [CXCallDirectoryManager sharedInstance];
// 获取权限状态
[manager getEnabledStatusForExtensionWithIdentifier:@"XXX" completionHandler:^(CXCallDirectoryEnabledStatus enabledStatus, NSError * _Nullable error) {
if (!error) {
if (enabledStatus == CXCallDirectoryEnabledStatusDisabled ) {
}
}
}];
复制代码
NSUserDefaults * userDefaults = [[NSUserDefaults alloc]initWithSuiteName:@“XXX"]; // 黑名单号码要升序排列 NSArray *sortedArray = [phoneNumberList sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { return [obj1 compare:obj2]; }]; [userDefaults setObject:sortedArray forKey:@"blackPhoneNum"]; [userDefaults synchronize]; CXCallDirectoryManager *manager = [CXCallDirectoryManager sharedInstance]; [manager reloadExtensionWithIdentifier:@“XXX" completionHandler:^(NSError * _Nullable error) {
复制代码
3,Extension的代码CallDirectoryHandler.m的方法实现
- (BOOL)addBlockingPhoneNumbersToContext:(CXCallDirectoryExtensionContext *)context {
NSUserDefaults * userDefaults = [[NSUserDefaults alloc]initWithSuiteName:@“XXX"]; NSArray * array = [userDefaults objectForKey:@"blackPhoneNum"]; [array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSString * phoneStr = obj; int64_t phoneInt = [phoneStr integerValue]; CXCallDirectoryPhoneNumber number = phoneInt ; [context addBlockingEntryWithNextSequentialPhoneNumber:number]; }]; return YES; } - (BOOL)addIdentificationPhoneNumbersToContext:(CXCallDirectoryExtensionContext *)context { NSUserDefaults * userDefaults = [[NSUserDefaults alloc]initWithSuiteName:@“XXX"];
NSArray * array = [userDefaults objectForKey:@"blackPhoneNum"];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSString * phoneStr = obj;
int64_t phoneInt = [phoneStr integerValue];
CXCallDirectoryPhoneNumber number = phoneInt ;
NSString *label = @"黑名单";
[context addIdentificationEntryWithNextSequentialPhoneNumber:number label:label];
}];
return YES;
}
复制代码
须要注意两点:
不知足的话,在设置中开启 ‘来电阻止与身份识别’的时候会报应用程序扩展时出现错误。
而PushKit的由于权限很大,能够经过PushKit在后台打开应用作不少事,并且系统也没有给用户提供任何开关来关闭它(因此苹果对PushKit的审核是比较严格的,须要谨慎使用,保护用户数据),经过后台打开APP,能够实现后台提早加载某些比较大的资源或crash以后再后台将数据重置等功能,具体作法欢迎共同探讨。
=========
参考:
https://developer.apple.com/reference/callkit
https://developer.apple.com/documentation/pushkit?language=objc
https://developer.apple.com/library/prerelease/content/samplecode/Speakerbox/Introduction/Intro.html