为何检测Apple Watch配对的异步函数无法捕获超时错误?
问题:Apple Watch配对检测的超时功能异常分析
我尝试编写一个工具函数,通过如下简洁的方式检测iOS设备是否与Apple Watch配对:
func isPaired( timeoutAfter maxDuration: TimeInterval ) async throws -> Bool {
代码整体运行正常,但超时功能存在问题:当注释掉代理中的activationContinuation?.resume(with: .success(session.isPaired))代码时,始终无法捕获代码抛出的超时错误,这是为什么?
完整代码如下:
import Combine import os import SwiftUI import WatchConnectivity let logger = Logger(subsystem: "net.mickf.BlocksApp", category: "Watch") class WatchPairingTask: NSObject { struct TimedOutError: Error, Equatable {} static var shared = WatchPairingTask() override private init() { logger.debug("Creating an instance of WatchPairingTask.") super.init() } private var activationContinuation: CheckedContinuation<Bool, Never>? func isPaired( timeoutAfter maxDuration: TimeInterval ) async throws -> Bool { logger.debug("Just called isPaired") return try await withThrowingTaskGroup(of: Bool.self) { group in group.addTask { try await self.doWork() } group.addTask { try await Task.sleep(nanoseconds: UInt64(maxDuration * 1_000_000_000)) try Task.checkCancellation() // We’ve reached the timeout. logger.error("Throwing timeout") throw TimedOutError() } // First finished child task wins, cancel the other task. let result = try await group.next()! group.cancelAll() return result } } private func doWork() async throws -> Bool { let session = WCSession.default session.delegate = self return await withCheckedContinuation { continuation in activationContinuation = continuation session.activate() } } } extension WatchPairingTask: WCSessionDelegate { func session(_ session: WCSession, activationDidCompleteWith _: WCSessionActivationState, error: Error?) { logger.debug("Session activation did complete") if let error = error { logger.error("Session activation did complete with error: \(error.localizedDescription, privacy: .public)") } // Remove the comment to make things work // activationContinuation?.resume(with: .success(session.isPaired)) } func sessionDidBecomeInactive(_: WCSession) { // Do nothing } func sessionDidDeactivate(_: WCSession) { // Do nothing } } class WatchState: ObservableObject { @Published var isPaired: Bool? func start() { Task { do { let value = try await WatchPairingTask.shared.isPaired(timeoutAfter: 1) await MainActor.run { isPaired = value } } catch { print("WatchState caught an error.") } } } } struct ContentView: View { @ObservedObject var model: WatchState init(model: WatchState) { self.model = model } var body: some View { VStack { Text("Is a watch paired with this iPhone?") Text(model.isPaired?.description ?? "…") } .padding() .onAppear { model.start() } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView(model: WatchState()) } }
控制台输出:
[Watch] Creating an instance of WatchPairingTask. [Watch] Just called isPaired [Watch] Session activation did complete [WC] -[WCSession onqueue_handleUpdateSessionState:]_block_invoke dropping as pairingIDs no longer match. pairingID (null), client pairingID: (null) [WC] WCSession is not paired [Watch] Throwing timeout
补充说明:
- 超时任务代码参考自Swift论坛的异步任务超时实现方案;
- 代码托管在GitHub的dirtyhenry/blocks仓库对应路径下。
问题原因
核心问题在于**未处理任务取消的CheckedContinuation**导致任务组无法正常结束:
- 当注释掉代理中的
resume代码后,doWork()里的withCheckedContinuation永远不会被恢复,该异步任务会一直处于挂起状态。 - 超时任务抛出
TimedOutError后,代码调用group.cancelAll()试图取消doWork()任务,但CheckedContinuation默认不会响应任务取消——即使任务被标记为取消,挂起的continuation依然不会被恢复,doWork()任务永远无法结束。 withThrowingTaskGroup需要等待所有子任务完成(或被取消并处理完成)才会退出,因此isPaired函数永远不会返回,超时错误无法传递到上层的do-catch块中,自然看不到捕获错误的打印。
解决方案
给doWork()添加任务取消处理,确保任务被取消时能恢复continuation,让任务正常结束:
修改doWork()方法,使用withTaskCancellationHandler绑定取消回调:
private func doWork() async throws -> Bool { let session = WCSession.default session.delegate = self return await withTaskCancellationHandler { return await withCheckedContinuation { continuation in activationContinuation = continuation session.activate() } } onCancel: { // 任务被取消时恢复continuation,避免永久挂起 activationContinuation?.resume(with: .success(false)) activationContinuation = nil } }
另外,还需要注意activationContinuation作为实例变量的线程安全问题——如果多次调用isPaired,会覆盖之前的continuation导致内存泄漏。建议将其改为doWork()的局部变量,或者用线程安全的容器管理:
private func doWork() async throws -> Bool { let session = WCSession.default session.delegate = self var localContinuation: CheckedContinuation<Bool, Never>? return await withTaskCancellationHandler { return await withCheckedContinuation { continuation in localContinuation = continuation activationContinuation = continuation session.activate() } } onCancel: { localContinuation?.resume(with: .success(false)) if activationContinuation === localContinuation { activationContinuation = nil } } }
这样修改后,当超时任务触发取消时,doWork()任务会被正常结束,withThrowingTaskGroup能及时返回超时错误,上层的do-catch块就能捕获到TimedOutError。
内容的提问来源于stack exchange,提问作者Mick F
相关产品推荐
相关产品推荐

