Devlog#21|夺命连环 Call
littlesheepラムです
7/10/2026, 3:25:42 PM253 次阅读

Devlog#21|夺命连环 Call

Flutter App 支持 CallKit 包教不会教程

#devlog

最近有很多针对 darwin 的针对性 PATCH,导致 iOS/macOS 的版本号和有记载的 Solian Releases 有很大的间隔,主要是为了修复这个畸形杂交出来的 CallKit 带来的问题。

但不管怎么说,我们目前有个阶段性成果了,所有支持 CallKit 的设备(iOS, iPadOS 现在都有 PushKit 的接听电话推送了)

服务端

服务端的实现相对简单,如果你不知道 APNs 怎么 Setup 的话可以回去参考我做 NSE 的那篇文章。这个 VoIP Push 就是换个 Topic 和 Payload 就好了。然后如果你像我一样喜欢用苹果的 iCloud Dashboard 来测试推送通知的话,千万不要被它页面骗了。在 iCloud Dashboard 中就算你把 Push Type 切换成了 voip 它的 Topic 也是不会改变的,还是你的 Bundle Identifier,千万不要误以为苹果改了这个不需要在 Bundle Identifier 后加 .voip 来推送 VoIP 的数据了。我因为这个浪费了自己的宝贵的两天人生来 Debug 为什么客户端没有收到这个 god damn 的推送。

客户端

因为 Solian 是个 flutter app,很多 UI 组件都是 flutter 的,甚至不像 React Native 可以有很大部份原生的组成。然后又因为 SwiftUI 很难写,或者说我和 Codex 的 SwiftUI 水平都太烂了,所以最开始想的比较优雅的结局方案让 Call 的部份在 iOS 上有原生实现不太现实。

所以目前来说的解决方案就是因为 iOS 在解锁状态下掉用 CallKit onAccept 函数会跳转到主 App,然后我们在 onAccpet method 里面通过 FlutterMethodChannel 掉用 Flutter 函数跳转到对应的 Call 的页面,接听 Call 就好了。非常简单……吗?

并不是,不然这个 Devlog 早出了。苹果会在 CallKit 中帮你配置 AudioSession,这在纯 Swift 的实现中自然是极好的,但是对于我们这个杂交 App(跨平台的框架确实叫 Hybird)来说就比较头疼了。Flutter WebRTC 会默认配置 AudioSession 在连接服务器的时候,这就会和 iOS 配置的起冲突,所以我们需要额外配置一下。

幸好幸好,我用的 LiveKit 依赖 flutter WebRTC 依赖 iOS WebRTC,所以到头来我们还是可以用他们配置好的解决方案:

override func application(
	_ application: UIApplication,
	didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
	RTCAudioSession.sharedInstance().useManualAudio = true
	RTCAudioSession.sharedInstance().isAudioEnabled = false
}

这样就可以告诉 iOS WebRTC 我们会自己手动处理 AudioSession,不用劳烦老人家帮我们处理了。这样子姑且就完成了吧……

还记得我之前说的是什么吗?解锁状态下。如果说用户在锁屏的时候接到了 Call 会怎么样呢?说得好,我也不知道。对于普通 pure-Swift app 来说可能比较好处理,但是我们这个杂交 App 在这种情况架又会遇到另一个大问题,还记得我们是怎么接到 Call 的吗?通过 Swift 告诉我们 Dart VM 中的 Flutter app 我们收到了个来自 xxx 房间的 Call,加入一下。

但问题是如果说 App 是被 PushKit 唤醒的呢?MethodChannel 要等到 Dart VM 完全启动才会初始化,Swift 肯定跑的比 Dart VM 快。这里我做了另一重处理,Swift 会把接到的 Call 的信息存储到 UserDefaults 中,然后当 Flutter 初始化完成的时候会向 Swift 要一次数据,看看有什么电话是刚刚接到的,如果有的话就想之前那样处理。

这样就能相对较好的处理从 PushKit 唤醒的场景,为什么说相对呢?因为不管是 NSE 还好,还是 PushKit 还好,通过推送触发的服务在 iOS 内部都有一个 DDL,如果你的 App 用的超过了多少资源,或者在多少秒之前还没有掉用一个函数(比如说 NSE 的 donation notification, CallKit 的 reportCall)它就认为你的代码坏掉了,而且对于用户来说是基本上是无感的;NSE 是静默失败,PushKit 还会弹一个弹窗告诉你 Solian 崩溃了。但是开发者看到这个 TestFlight / App Store 回报的错误日子也是束手无措的,因为啥信息都没有……

本文时常会提到 CallKit 和 PushKit 这两个名词。但是事实上他们负责的内容是不一样的,PushKit 处理 APNs 推送 Topic 为 VoIP 的消息的框架;而 CallKit 是处理 Call 的回报什么的,纯本地也能使用。

上文说到我们要解决的是锁定状态下接受 Call 的场景。但是怎么突然变成退出状态下的呢?不是我有 ADHD,是二者本质上是类似的。但是锁屏状态下的情况更苛刻,因为锁屏,所以 iOS 会一直使用系统原生的 UI(就是打手机电话的 UI)来处理。理论上来说,只是理论上,它的流程和一半的腿粗状态是一样的,只是系统把这个流程放到了后台隐藏来运行。但是具体状态就不知道了。

我调试这个 CallKit 的功能已经不知道发了多少个 darwin only patch,因为 PushKit 只有实体机才有的功能,模拟器模拟不出来这个,以至于我不想研究这个锁屏状态的是否预期工作了。换句话说,我力竭了。就这样吧。

测试

尽管如此,我还是想讲述一下为什么我力竭了,而且我在码这部份的文章的时候键盘敲的异常激烈的原因。避免如果你也要做 CallKit 的话浪费一样的宝贵生命。

上文说了这个功能需要实体机测试,如果你日常就有在用 iOS 的话应该会跟着系统的指引设置一个睡眠提醒,同时它也会自动帮你打开睡眠的专注模式。aka 免打扰。然后 CallKit 和 SIM Telephone Call 都会被这天杀的免打扰拦截。为什么我要单独说这一点呢?因为同样的代码,我下午跑的好好的,到了晚上就没有任何通知了?一看 Phone App 全部都被自动挂断了。我还以为我又碰到了什么 iOS 的风控,思考了一晚上,无意间瞥到了状态栏的床……

关于风控,我没有看到任何文档提及到这一点。但是我的事实体验就是如果你推的 VoIP 太多了的话并且用户的应答率又不高(全部被 Dismiss 或者超时自动挂断)的话,恭喜你,你就会浪费你的人生思考为什么我所有的配置都对但是我的 App 在后台的时候就是收不到推送呢?因为被 apnd 静默丢掉了哈哈(从 Console.app 读取 iPhone 的日志 Codex 发现的)。解决方法就是重启手机,重装 App。然后又会恢复正常了。


乂……尽管苹果挺折腾开发者的,但是在“营商环境“这一块和国内安卓相比还是不知道舒服到哪里去了。或者你说是果粉自适应发力了吧,我也是没招了。

开发 CallKit 的时候力竭一回,现在写博客又重新感到力竭了。事已至此,先睡觉吧。