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
相关产品推荐
相关产品推荐

