iOS CallKit接收missingCall推送取消来电仅首次生效问题排查
VoIP通话CallKit取消逻辑首次生效后后续失效问题修复
故障根因
问题出在两个核心逻辑缺陷:
- 已结束通话残留未清理:调用
reportCall结束系统来电后,没有将对应的Call对象从callManager的通话数组中移除,导致已失效的通话记录长期驻留内存。 - 通话匹配逻辑命中旧记录:后续收到同用户的来电取消事件时,按userId查找通话的方法会优先命中数组中残留的、已结束的旧通话记录,拿到无效的UUID调用结束接口,无法作用于当前活跃的新通话,因此取消操作失效。
首次操作时数组无残留记录,可以正确匹配到当前活跃通话,因此首次取消成功;重启App后内存中残留记录被清空,又会复现首次成功、后续失效的现象,和描述的异常完全吻合。
其他潜在逻辑缺陷
- 收到
missingCall类型的取消推送时,依然触发了响铃接口ringingApi,会导致取消来电时仍短暂播放铃声,不符合交互预期。 - 全局
callOption的属性在多线程环境下读写无保护,异步调用ringingApi时可能被后续推送的内容覆盖,拿到错误的参数。 - 存在无效冗余代码:在push回调中提前初始化了
CXCallUpdate对象但从未使用。 - 强制解包过多,推送字段异常时会直接触发崩溃;PushKit回调的
completion没有在所有分支保证调用,存在被系统杀进程的风险。 - 极端网络乱序场景下(取消推送早于对应来电推送到达),会因为找不到对应通话记录导致后续展示的来电无法被取消。
修复方案
1. 结束通话后立即移除对应记录
修改reportTheCallerEndCall方法,结束系统来电后同步删除callManager中的对应记录,保证数组中只存活跃通话:
func reportTheCallerEndCall(userId: String) { guard let callIndex = self.callManager.findCallIndexInCallsArray(userId: userId) else { return } let targetCall = self.callManager.calls[callIndex] // 报告系统结束通话 provider.reportCall(with: targetCall.uuid, endedAt: Date(), reason: .unanswered) // 核心修复:从数组中移除已结束的通话,避免残留 self.callManager.remove(call: targetCall) }
注意:需要确认
callManager已实现remove(call:)方法,如果没有则补充实现,逻辑为从calls数组中移除UUID匹配的对象即可。
2. 优化PushKit回调逻辑,修正分支判断
调整push回调的执行顺序,取消推送分支不要触发响铃逻辑,移除无用的提前初始化对象,替换强制解包为安全解析,保证completion正常调用:
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) { print("didReceiveIncomingPushWith payload") let payloadDictionary = payload.dictionaryPayload as NSDictionary // 当前线程解析完所有参数,避免多线程读写全局变量导致数据错乱 guard let callData = payloadDictionary.value(forKey: "aps") as? [String: Any], let user = callData["user"] as? [String: Any], let callerID = user["name"] as? String, let sndId = callData["snd_id"] as? String, let typeCall = callData["typeCall"] as? String else { completion() return } let hasVideo = true let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: nil) if typeCall == "missingCall" { // 取消分支:直接结束对应通话,不触发响铃 self.endIncomingCall(userId: sndId) UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) } else { // 正常来电分支:生成新UUID,展示来电后再触发响铃 let uuid = UUID() self.displayIncomingCall(uuid: uuid, handle: callerID, hasVideo: hasVideo, userId: sndId) { _ in UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) } DispatchQueue.global().async { self.callOption.ringingApi(otherId: sndId) } } completion() }
3. 增强鲁棒性的可选优化
- 修改
findCallIndexInCallsArray逻辑,除了匹配userId,增加通话状态判断,只返回未结束的活跃通话记录,进一步避免匹配到残留数据。 - 如果业务规定同一个用户同时只能有一路来电,可以在
displayIncomingCall添加新通话前,先移除该userId下所有已存在的旧记录,从源头避免重复。
内容的提问来源于stack exchange,提问作者Mostafa ALZOUBI
相关产品推荐
相关产品推荐

