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

iOS 15应用进程被杀后PushKit无法接收VoIP推送问题

iOS 15 杀进程状态下无法接收PushKit VoIP通知问题修复

问题表现

  • 应用在iOS 14全运行状态(前台、后台、进程被杀死)下均可正常接收PushKit VoIP通知
  • 升级iOS 15后出现异常:仅前台、后台运行时可正常接收通知,进程被杀死后完全收不到PushKit通知
  • 参考公开讨论帖中的方案操作后问题未解决

核心原因

iOS 15 升级了PushKit VoIP推送的强制合规校验规则:所有VoIP推送到达后,必须在限定时间内通过CallKit接口上报系统来电,否则系统会直接终止应用进程;累计3次违规后,系统会直接拦截应用的VoIP推送权限,冷启动(进程被杀死)场景下校验逻辑最严格。
从提供的代码看,存在4个直接触发系统拦截的问题:

  1. didReceiveIncomingPushWith回调中completion()调用时机不统一,多个提前return的分支完全没有调用completion,系统直接判定推送处理超时
  2. 收到推送后没有第一时间上报CallKit来电,反而先执行JSON解析、业务状态判断、Pusher初始化等耗时操作,冷启动时这些操作的耗时很容易超出系统允许的处理时长
  3. 部分分支收到推送后直接return,没有上报任何来电,违反VoIP推送必须绑定CallKit来电的强制要求
  4. 用自定义串行队列初始化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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 12:51:24