iOS 13 正在终止应用程序,因为它在收到 PushKit VoIP 回调后从未向系统发布来电

iOS 13 Killing app because it never posted an incoming call to the system after receiving a PushKit VoIP callback

升级到 iOS beta 13 后,我注意到一件不愉快的事情:我的应用有时会在收到 VoIP 推送时崩溃。

在崩溃报告中我看到以下内容:

iOS 13 Killing app because it never posted an incoming call to the system after receiving a PushKit VoIP callback 

Fatal Exception: NSInternalInconsistencyException
0  CoreFoundation                 0x1af21b9f0 __exceptionPreprocess
1  libobjc.A.dylib                0x1af7284fc objc_exception_throw
2  CoreFoundation                 0x1af11efec + 
 [_CFXNotificationTokenRegistration keyCallbacks]
3  Foundation                     0x1aeda1330 -[NSAssertionHandler 
 handleFailureInMethod:object:file:lineNumber:description:]
4  PushKit                        0x19caa6b54 -[PKPushRegistry 
 _terminateAppIfThereAreUnhandledVoIPPushes]
5  libdispatch.dylib              0x1afa441ec _dispatch_client_callout
6  libdispatch.dylib              0x1af9f6c6c 
_dispatch_lane_barrier_sync_invoke_and_complete
7  PushKit                        0x19caa5b74 __73-[PKPushRegistry 
 voipPayloadReceived:mustPostCall:withCompletionHandler:]_block_invoke
8  libdispatch.dylib              0x1afa43678 
 _dispatch_call_block_and_release
9  libdispatch.dylib              0x1afa441ec 
  _dispatch_client_callout

10 libdispatch.dylib              0x1af9f61f8 
_dispatch_main_queue_callback_4CF$VARIANT$mp
11 CoreFoundation                 0x1af1992a0 
CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
12 CoreFoundation                 0x1af1942a8 __CFRunLoopRun
13 CoreFoundation                 0x1af1937ac CFRunLoopRunSpecific
14 GraphicsServices               0x1ae395180 GSEventRunModal
15 UIKitCore                      0x1b6e60244 UIApplicationMain
16 VOIPProject                    0x1009822d8 main + 25 
(AppDelegate.swift:25)
17 libdyld.dylib                  0x1af6e9e7c start

我不明白如何解决这个问题。每当我收到 VoIP 推送时,我是否必须 post CallKit 来电屏幕?这听起来很疯狂,因为我在显示来电屏幕之前检查通知是否有效。谁能解释一下我该怎么办?

在苹果论坛的这个thread上,苹果工作人员解释说:

On iOS 13.0 and later, incoming Voice over IP calls must be reported when they are received and before the didReceiceIncomingPush() method finishes execution, using the CallKit framework, or the system will terminate your app.

Repeatedly failing to report calls may prevent your app from receiving any more incoming call notifications.

Basically, you can no longer use VoIP pushes for non VoIP messaging, and will need to use regular push notifications for those.

This was announced during the WWDC session "Advances in App Background Execution" https://developer.apple.com/videos/play/wwdc2019/707/


我一直在寻找有关如何针对此更改调整应用程序的答案,我可以收集到以下信息:

Voip 推送

当您的应用收到此类推送时,需要使用 CallKit 报告新来电。因此,这种推送将专用于使用CallKit的调用。

建议您将通知的 apns-expiration 设置为 0,这样您就不会收到推送并被迫显示已过期的通话屏幕。

推送通知

定期推送通知是另一种选择。如果您的服务器拥有编写通知文本所需的所有信息,您可以发送甚至 运行 您的应用程序都不会在后台运行的通知。如果您需要在向用户显示之前修改通知的内容,您可以使用通知服务应用程序扩展,如果您需要唤醒您的应用程序并在后台执行某些操作,您可以发送静默推送通知。

Notification Service App Extension

要使用它,您必须将通知的 mutable-content 设置为 1。这样,您的扩展程序将在通知呈现给用户之前收到通知,允许您更改其内容,只需 30 秒时间限制。

缺点是您的应用程序将留在后台,只有您的扩展程序可以 运行。这可能意味着您需要在您的应用程序和扩展程序之间共享信息和代码,方法是使用用户默认值、钥匙串或共享您的整个数据库(如果您的应用程序没有为此做好准备,这可能不是一项简单的任务) .

静默推送通知

要发送静默推送通知,您必须将通知的 content-available 设置为 1 并删除它的 alertbadge声音。此通知将在后台唤醒您的应用,并调用您的应用委托的 didReceiveRemoteNotification.

这个选项的缺点很烦人:

  • 你只有 30 秒的时间 运行。
  • 这些通知的 apns-priority 必须为 5,这可能会导致它们被分组并突发传送,甚至会受到限制或无法传送。
  • 如果用户强制关闭应用程序,它将完全忽略所有静默通知,直到用户再次打开应用程序。

我在尝试使用 didReceiveIncomingPushWith 的异步版本时遇到过同样的崩溃。我使用的代码:

func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) async {
    guard type == .voIP else { return }

    let callUpdate = CXCallUpdate()
    let phoneNumber = CXHandle(type: .phoneNumber, value: "handle")
    callUpdate.remoteHandle = phoneNumber

    try? await callProvider.reportNewIncomingCall(with: UUID(), update: callUpdate)
}

应用程序在前台运行正常,但在后台崩溃。日志显示由于某种原因它根本没有被调用。

同时完成版本工作正常:

func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
    guard type == .voIP else { return }

    let callUpdate = CXCallUpdate()
    let phoneNumber = CXHandle(type: .phoneNumber, value: "handle")
    callUpdate.remoteHandle = phoneNumber

    callProvider.reportNewIncomingCall(with: UUID(), update: callUpdate) { error in
        completion()
    }
}