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

为何检测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**导致任务组无法正常结束:

  1. 当注释掉代理中的resume代码后,doWork()里的withCheckedContinuation永远不会被恢复,该异步任务会一直处于挂起状态。
  2. 超时任务抛出TimedOutError后,代码调用group.cancelAll()试图取消doWork()任务,但CheckedContinuation默认不会响应任务取消——即使任务被标记为取消,挂起的continuation依然不会被恢复,doWork()任务永远无法结束。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 10:54:58