导读:本期聚焦于天马创作的《如何使用PushKit与CallKit开发iOS网络电话App?实现来电显示与通话记录全解析》,敬请观看详情。开发一款iOS网络电话App,绕不开CallKit与PushKit这两个框架的配合。CallKit让VoIP通话拥有和系统电话一致的外观体验,来电全屏界面、锁屏接听、通话记录写入通讯录全都不在话下;PushKit则负责接收VoIP推送,保证App在后台甚至被杀死后仍能及时唤醒并上报来电。本文详细讲解两个框架的底层协作机制,梳理证书配置、推送报文格式、CXProvider配置项,并通过完整代码演示如何注册PushKit令牌、处理来电上报、管理音频会话以及将通话记录写入系统最近通话列表,同时分析常见崩溃原因与审核被拒的典型场景,帮助你少踩坑、快速上线。

iOS上的VoIP应用如果只是自己做一套通话界面,用户体验会大打折扣:来电时App在后台根本没有机会弹界面,用户在锁屏状态下也看不到任何来电提醒。苹果提供的CallKit框架正是为了解决这个问题,它允许第三方通话应用接入系统电话级别的UI,包括全屏来电界面、锁屏接听滑块、系统通讯录匹配以及最近通话记录。配合PushKit的VoIP推送机制,App即使被用户完全杀死,也能在收到来电时被系统唤醒并弹出来电界面。这套方案是苹果官方钦定的VoIP标准做法,本文将从原理到实践完整梳理整个开发流程。

如何使用PushKit与CallKit开发iOS网络电话App?实现来电显示与通话记录全解析

一、PushKit与CallKit的协作机制

很多刚接触VoIP开发的同学容易把这两个框架的职责搞混。简单来说,PushKit负责“唤醒”,CallKit负责“展示”。PushKit提供了一个特殊的推送通道叫VoIP推送,它与普通APNs推送最大的区别在于:普通推送只是展示一条横幅,而VoIP推送会直接唤醒App进程执行代码,哪怕App已经被杀死。App被唤醒后会触发didReceiveIncomingPushWith回调,开发者必须在这个回调里同步调用CallKit的reportNewIncomingCall方法上报来电,系统才会弹出来电界面。

这里有一条苹果的硬性规定:从iOS 13开始,如果收到VoIP推送后没有调用reportNewIncomingCall,系统会直接杀掉App进程,并且这个行为会被记录,多次触发后VoIP推送权限可能被收回。这是苹果为了防止开发者滥用VoIP推送做消息通道而设的关卡,也是审核被拒的高频原因。所以一定要理解:VoIP推送只服务于真实来电,不能拿来当普通IM通知用。

另一个关键点是CallKit接管了音频会话的配置。一旦接入CallKit,你就不能再随意调用AVAudioSessionsetActive方法,音频会话的激活与失活由系统在通话生命周期内统一调度,开发者只需要在CXProviderDelegate的音频激活回调里开始播放或采集音频即可,否则会出现声音忽大忽小、路由异常等问题。

二、推送证书配置与令牌注册

VoIP推送使用独立的证书体系。你需要在开发者后台为App ID开启Push Notifications能力,然后单独创建一个VoIP Services Certificate,注意它和普通的APNs证书不是同一个东西,导出p8或者p12后配置到自己的推送服务端。服务端下发VoIP推送时,payload的组装也有讲究,下面是一个典型的报文结构:

{
  "aps": {
    "alert": {
      "title": "张三",
      "body": "来电"
    },
    "sound": "default"
  },
  "callId": "9f8e7d6c-1234-5678-abcd-ef0123456789",
  "callerName": "张三",
  "callerNumber": "+8613800138000"
}

客户端注册部分,PushKit的入口是PKPushRegistry,注册后系统会在回调里下发VoIP专用令牌,你需要把它上报到自己的信令服务器。代码如下:

// 在AppDelegate中初始化PushKit
PKPushRegistry *registry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
registry.delegate = self;
registry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];

#pragma mark - PKPushRegistryDelegate
- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(PKPushType)type {
    // 将VoIP令牌转成十六进制字符串,上报给信令服务器
    unsigned char token[credentials.token.length];
    [credentials.token getBytes:token length:credentials.token.length];
    NSMutableString *tokenString = [NSMutableString string];
    for (int i = 0; i < credentials.token.length; i++) {
        [tokenString appendFormat:@"%02x", token[i]];
    }
    NSLog(@"VoIP Token: %@", tokenString);
}

- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWith:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion {
    // 必须在此回调内同步上报来电,否则iOS 13+会终止App
    NSString *callId = payload.dictionaryPayload[@"callId"];
    NSString *callerName = payload.dictionaryPayload[@"callerName"];
    NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:callId];
    [self reportIncomingCallWithUUID:uuid callerName:callerName];
    completion();
}

需要注意令牌的有效性:卸载重装、系统升级都可能使令牌变化,所以每次启动App都要重新注册并上报最新令牌。另外调试阶段可以用一款叫VoipPush的测试工具手动下发推送,验证证书配置是否正确,避免服务端还没就绪时干等。

三、CallKit来电上报与系统UI集成

CallKit的核心角色有三个:CXProvider负责与系统UI交互,CXCallController负责发起和管理通话动作,CXCallUpdate则描述来电的元信息。初始化时需要仔细配置CXProviderConfiguration,它决定了来电界面长什么样,比如铃声文件名、是否支持视频、号码显示格式等。

- (void)setupCallKit {
    CXProviderConfiguration *config = [[CXProviderConfiguration alloc] initWithLocalizedName:@"我的网络电话"];
    config.supportsVideo = NO;
    config.maximumCallGroups = 1;
    config.maximumCallsPerCallGroup = 1;
    config.supportedHandleTypes = [NSSet setWithObject:@(CXHandleTypePhoneNumber)];
    // 铃声文件需要放在bundle根目录,传文件名不带后缀
    config.ringtoneSound = @"voip_ringtone.caf";
    // iOS 11+ 支持每次通话单独设置铃声
    _provider = [[CXProvider alloc] initWithConfiguration:config];
    [_provider setDelegate:self queue:dispatch_get_main_queue()];
    _callController = [[CXCallController alloc] initWithQueue:dispatch_get_main_queue()];
}

- (void)reportIncomingCallWithUUID:(NSUUID *)uuid callerName:(NSString *)callerName {
    CXCallUpdate *update = [[CXCallUpdate alloc] init];
    update.remoteHandle = [[CXHandle alloc] initWithType:CXHandleTypePhoneNumber value:@"+8613800138000"];
    update.localizedCallerName = callerName;
    update.hasVideo = NO;
    update.supportsGrouping = NO;
    update.supportsUngrouping = NO;
    update.supportsHolding = NO;

    [_provider reportNewIncomingCallWithUUID:uuid update:update completion:^(NSError *error) {
        if (error) {
            NSLog(@"上报来电失败: %@", error);
        }
    }];
}

reportNewIncomingCall执行成功后,系统会立即弹出来电界面,不管App在前台还是后台,锁屏状态下同样有效。界面上显示的名称来自localizedCallerName,如果这个号码存在于系统通讯录,CallKit会自动匹配通讯录头像和姓名,这个体验和原生电话完全一致,是自绘界面无法做到的。

用户在系统界面上点接听或挂断,事件会通过CXProviderDelegate回调到App。接听时需要先等待音频会话激活,这是新手最容易忽略的时序问题:

#pragma mark - CXProviderDelegate
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action {
    // 不要在这里直接激活音频,先记录通话上下文
    [self.currentCall answerWithUuid:action.callUUID];
    [action fulfill];
}

- (void)providerDidActivateAudioSession:(CXProvider *)provider {
    // 系统激活音频会话后,才能安全地开始音频采集与播放
    AVAudioSession *session = [AVAudioSession sharedInstance];
    [session setCategory:AVAudioSessionCategoryPlayAndRecord
             mode:AVAudioSessionModeVoiceChat
          options:AVAudioSessionCategoryOptionAllowBluetooth
                error:nil];
    [self.audioEngine start];
}

- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action {
    [self.currentCall hangup];
    [action fulfill];
}

- (void)providerDidDeactivateAudioSession:(CXProvider *)provider {
    // 通话结束后系统会失活音频会话,可在这里做资源清理
    [self.audioEngine stop];
}

四、通话记录写入与常见坑

只要通过CallKit上报过来电,系统就会自动在“最近通话”里生成记录,用户甚至可以直接从通话记录回拨。回拨时系统会启动App并触发CXHandle指定的URL Scheme或者通过CXStartCallAction回调进入App,你需要在CXProviderDelegateperformStartCallAction里处理外呼逻辑。通话记录的时长、 missed标记都由系统根据你上报的通话状态自动维护,开发者不需要也无法手动写数据库。

实际开发中有几个高频坑值得单独说明。第一,来电铃声必须是bundle内的资源且格式为caf或aiff,如果配置了不存在的文件名,来电会静默使用默认铃声,不会报错,排查起来很费劲。第二,App被杀后来电时进程会被重新拉起,但留给你的启动时间有限,不要在didFinishLaunchingWithOptions里做耗时初始化,否则上报来电会超时失败。第三,completion()必须调用且必须在reportNewIncomingCall之后调用,顺序反了或者漏掉都会导致系统判定推送处理异常。第四,提审时苹果会重点核查是否真的实现了通话功能,如果你的App只是用VoIP推送做通知而没有真实通话,基本会被4.2或2.1条款拒绝,准备好演示账号和测试通话环境能大幅加快审核。

整体来看,PushKit加CallKit的组合并不复杂,难点在于理解系统对时序的严格要求以及音频会话的生命周期管理。建议先用最简单的demo跑通“推送到来电界面弹出到接听回调”这条主链路,再逐步叠加音频引擎、信令层,出现问题时按链路逐段排查,效率会高很多。

PushKitCallKitiOS VoIP开发修改时间:2026-09-13 22:33:18

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260913/56288.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。