You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PushKit VoIP推送未上报来电触发NSInternalInconsistencyException

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Killing app because it never posted an incoming call to the system after receiving a PushKit VoIP push.'

异常触发原因

这个崩溃是iOS系统对VoIP推送的强制约束导致的:从iOS 13版本开始,应用一旦收到PushKit的VoIP类型推送,必须在限定时间内通过CallKit框架向系统上报新来电,超时未上报、上报失败或者跳过上报逻辑,系统都会直接终止应用,多次违规还会被剥夺VoIP推送的接收权限。

现有代码存在4个直接或间接导致崩溃的问题:

  • 分支逻辑漏洞:completion()被放在了if let handleDict判断的外部,一旦推送payload中不存在handle字段,代码会直接执行completion()结束回调,完全不会走到来电上报逻辑,必然触发崩溃。
  • CXProvider使用错误:每次收到推送都新建CXProvider实例,该类设计为全局单例使用,重复创建会导致CallKit上报链路失效,即使调用了reportNewIncomingCall也无法成功将事件递交给系统。
  • 存在强制解包风险:UIImage(named: "user_circle")!.pngData()是强制解包逻辑,一旦图片资源缺失、加载失败,会直接触发崩溃中断来电上报流程。
  • 参数不规范:CXHandle使用了.phoneNumber类型,但传入的value是"SoCircle"字符串,不符合电话号码的格式要求,可能导致系统拒绝接收上报事件。
修复方案

首先调整CXProvider的初始化逻辑,全局只创建一次实例,不要在推送回调中重复创建:

// 全局持有CXProvider实例,仅初始化一次
private let callProvider: CXProvider = {
    let config = CXProviderConfiguration(localizedName: "App")
    // 安全处理图标资源,避免强制解包崩溃
    if let callIcon = UIImage(named: "user_circle") {
        config.iconTemplateImageData = callIcon.pngData()
    }
    config.ringtoneSound = "iphoneRingtone.mp3"
    config.includesCallsInRecents = true
    config.supportsVideo = true
    let provider = CXProvider(configuration: config)
    provider.setDelegate(self, queue: nil)
    return provider
}()
// 持有当前通话UUID,供后续接听、挂断、更新通话状态使用
private var currentCallUUID: UUID?

然后重写VoIP推送接收回调,保证所有分支都必须执行来电上报逻辑:

func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
    let payloadDict = payload.dictionaryPayload
    
    // 收到VoIP推送第一时间构造来电上报参数,不要先做耗时逻辑、分支判断
    let callUpdate = CXCallUpdate()
    // 修正CXHandle类型,非电话号码内容使用generic类型
    let callHandle = CXHandle(type: .generic, value: "SoCircle")
    callUpdate.remoteHandle = callHandle
    callUpdate.hasVideo = true
    let callUUID = UUID()
    currentCallUUID = callUUID

    // 自定义业务字段解析放在上报参数构造之后,不阻塞上报流程
    if let handleInfo = payloadDict["handle"] as? NSDictionary {
        self.callDict = handleInfo
        // 可在此处根据业务字段更新callUpdate内容,比如来电人昵称、头像、号码信息
    }

    // 必须调用reportNewIncomingCall上报来电,上报完成后再执行completion回调
    callProvider.reportNewIncomingCall(with: callUUID, update: callUpdate) { error in
        #if DEBUG
        if let err = error {
            print("CallKit来电上报失败: \(err.localizedDescription)")
        }
        #endif
        completion()
    }
}

额外注意事项:

  • 不存在“无效VoIP推送可以不上报”的例外,哪怕收到的是测试推送、参数错误的推送,也必须先上报来电,后续可以调用CXProvider的reportCall(with:endedAt:reason:)接口主动挂断无效来电,跳过上报必然触发崩溃。
  • 收到VoIP推送后不要先执行网络请求、数据库读写等耗时操作再上报,必须第一时间触发reportNewIncomingCall,超过系统超时阈值(约3-5秒)同样会被判定为未上报。
  • 如果业务场景不需要使用CallKit的系统通话界面,不要接入VoIP推送,改用普通APNs推送即可,iOS 13及以上版本VoIP推送与CallKit是强制绑定的。

内容的提问来源于stack exchange,提问作者Fuad Waheed

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.31 20:42:20