iOS 15应用进程被杀后PushKit无法接收VoIP推送问题
iOS 15 杀进程状态下无法接收PushKit VoIP通知问题修复
问题表现
- 应用在iOS 14全运行状态(前台、后台、进程被杀死)下均可正常接收PushKit VoIP通知
- 升级iOS 15后出现异常:仅前台、后台运行时可正常接收通知,进程被杀死后完全收不到PushKit通知
- 参考公开讨论帖中的方案操作后问题未解决
核心原因
iOS 15 升级了PushKit VoIP推送的强制合规校验规则:所有VoIP推送到达后,必须在限定时间内通过CallKit接口上报系统来电,否则系统会直接终止应用进程;累计3次违规后,系统会直接拦截应用的VoIP推送权限,冷启动(进程被杀死)场景下校验逻辑最严格。
从提供的代码看,存在4个直接触发系统拦截的问题:
didReceiveIncomingPushWith回调中completion()调用时机不统一,多个提前return的分支完全没有调用completion,系统直接判定推送处理超时- 收到推送后没有第一时间上报CallKit来电,反而先执行JSON解析、业务状态判断、Pusher初始化等耗时操作,冷启动时这些操作的耗时很容易超出系统允许的处理时长
- 部分分支收到推送后直接return,没有上报任何来电,违反VoIP推送必须绑定CallKit来电的强制要求
- 用自定义串行队列初始化
PKPushRegistry,冷启动时如果队列被其他任务阻塞,会直接导致推送回调延迟执行,触发超时
修复步骤
1. 修正PushKit初始化逻辑
移除自定义串行队列,改用主队列初始化PKPushRegistry,避免回调被阻塞:
// 删除原有自定义voipQueue初始化逻辑,直接使用主队列 voipRegistry = PKPushRegistry(queue: DispatchQueue.main) voipRegistry?.delegate = self voipRegistry?.desiredPushTypes = [.voIP]
2. 重构推送回调逻辑
严格遵守系统要求,推送到达后优先上报CallKit来电、优先调用completion回调,业务逻辑全部放到来电上报完成后再执行:
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) { // 第一步:立刻上报系统来电,先填默认信息,后续解析完业务数据再更新 let callUUID = UUID() let defaultUpdate = CXCallUpdate() defaultUpdate.localizedCallerName = "来电" defaultUpdate.hasVideo = true CallManager.shared.provider.reportNewIncomingCall(with: callUUID, update: defaultUpdate) { [weak self] error in guard error == nil else { return } // 来电上报成功后,再执行业务逻辑处理、数据解析 self?.processVoipPayload(payload: payload, currentCallUUID: callUUID) } // 第二步:立刻调用completion,不要放到任何分支逻辑里延后执行 completion() } // 抽离原有业务逻辑,在CallKit上报完成后执行 private func processVoipPayload(payload: PKPushPayload, currentCallUUID: UUID) { print("--> VOIP EVENT") do { let dict = payload.dictionaryPayload["aps"] as! NSDictionary let json: [String: Any] = [ "fromName" : (dict["data"] as! NSDictionary)["fromName"] as? String ?? "", "toEmail" : (dict["data"] as! NSDictionary)["toEmail"] as? String ?? "", "fromEmail" : (dict["data"] as! NSDictionary)["fromEmail"] as? String ?? "", "toName" : (dict["data"] as! NSDictionary)["toName"] as? String ?? "", "roomId" : (dict["data"] as! NSDictionary)["roomId"] as? String ?? "", "user" : (dict["data"] as! NSDictionary)["user"] as? String ?? "" ] print("VOIP -->", dict) switch dict["name"] as! String { case CLIENT_CALL: if UIApplication.shared.applicationState == .active { let topVC = UIApplication.shared.topViewController() if topVC is RemoteAssistanceCallViewController || CallManager.shared.isProviderActive { // 已在通话中,显示未接来电通知后主动挂断本次上报的来电 Utils.shared.showNotification( title: "Missed call", subtitle: "You have a missed call", body: "\(json["fromName"] ?? "Someone") tried to call you but you were busy on another call.", identifier: "\(Date())" ) CallManager.shared.endCall(uuid: currentCallUUID) return } } newCallEvent(data: CallRequestModel(JSON: json)!, callUUID: currentCallUUID) // 解析到真实来电信息后,更新系统来电UI显示 let update = CXCallUpdate() update.localizedCallerName = json["fromName"] as? String ?? "未知来电" CallManager.shared.provider.reportCall(with: currentCallUUID, updated: update) if !PusherHelper.shared.isConnected { PusherHelper.shared.setupPusher() } break default: if UIApplication.shared.applicationState == .active || CallManager.shared.isProviderActive { CallManager.shared.endCall(uuid: currentCallUUID) return } CallManager.shared.callInVein() print("Call Unknown! name. Alas!") } } catch { print(error) // 数据解析失败也要挂断已上报的来电,避免系统来电UI异常挂起 CallManager.shared.endCall(uuid: currentCallUUID) } } func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { print(pushCredentials.token) let deviceToken = pushCredentials.token.map { String(format: "%02x", $0)}.joined() print("push registry --> device token: \(deviceToken)") NotificationsHelper.shared.updateVoIPToken(voIPToken: deviceToken) }
3. 收尾校验
- 确认项目Capabilities中已开启
Voice over IP能力,且Background Modes下勾选了Voice over IP选项 - 禁止使用VoIP推送发送非来电类消息,非来电场景改用普通APNs推送
- 修复完成后先卸载应用再重装测试,清除系统之前记录的违规拦截标记(系统会持久化存储应用的VoIP违规记录,不重装的话即使修复了代码也可能继续被拦截)
补充说明:冷启动场景下收不到推送、前后台正常是这类违规的典型表现——前后台运行时应用本身已经在内存中,处理推送速度快,不容易触发超时;冷启动时应用需要从头加载进程,执行多余的耗时逻辑很容易超出系统给的推送处理时限,触发拦截。
内容的提问来源于stack exchange,提问作者Jaswant Singh
相关产品推荐
相关产品推荐

