← 所有文章

watchOS运行时是一种契约,而非后台任务

Return的Watch应用运行着一个多周期的冥想计时器,必须在用户落腕时持续计数。1 在这一约束下能够存活的模式是WKExtendedRuntimeSession加上一个全局的、应用级作用域的delegate。其他一切方案都会在手表休眠的瞬间归于消亡。

watchOS不是屏幕更小的iOS。其运行时模型截然不同。iOS赋予应用充裕的前台运行预算,以及通过音频会话、位置更新、BGTaskScheduler等少数授权机制实现的虽然递减但确实存在的后台运行时。2 而watchOS给前台应用的运行预算,在落腕之后只能以秒计;此后,除非应用已与系统签订了运行时契约,否则会被挂起。这里没有所谓”我只是在后台做点事”的余地。只有”我在运行健身、正念、智能闹钟、导航路线或健康监测任务”,仅此而已。3

Return的Watch目标是一个正念计时器。会话契约为WKBackgroundModes: mindfulness。运行时API是WKExtendedRuntimeSession。让Watch应用从落腕即崩转变为能够撑过25分钟冥想的模式,正是本文所要讲述的。

TL;DR

  • watchOS没有iOS式的后台。前台运行时在落腕后不久即终止,只有已注册的会话类型能继续运行。
  • WKExtendedRuntimeSession是API接口。Apple支持四种会话类型:self-caremindfulnessphysical-therapyalarm。对于冥想计时器而言,会话类型为mindfulness,通过Info.plist中的WKBackgroundModes声明。
  • 会话管理器必须存在于应用作用域,而非视图作用域。SwiftUI的视图生命周期会在导航时释放视图所拥有的对象;一个被释放的会话delegate就是死会话,即便会话本身仍在运行也是如此。
  • WKExtendedRuntimeSessionDelegate回调即为契约:didStartwillExpiredidInvalidateWith。expire回调会在系统强制失效之前触发;Apple在”Using extended runtime sessions”中的示例代码将其定位为”会话结束前完成并清理任务”的位置。3
  • 没有活跃扩展会话时的落腕会暂停计时器。有活跃扩展会话时的落腕则计时器继续运行。会话就是”已上线产品”与”二次使用即崩”之间的分水岭。

watchOS无法以iOS方式解决的后台问题

iOS应用在需要让应用在熄屏状态下持续运行时,可以使用多种后台授权机制:2

  • 类别为.playbackAVAudioSession使音频应用在播放音乐时保持存活。
  • CLLocationManager后台更新通过蓝色顶栏使导航应用保持存活。
  • BGTaskScheduler将简短的维护工作排队,由系统按其自身节奏调度执行。
  • 前台UI扩展(灵动时刻、CallKit、PushKit)将应用进程桥接到系统控制的渲染界面。

这些机制在watchOS上都无法以你可能预期的方式发挥作用。Watch应用没有相同的后台任务调度器。它们没有能让计时器在静音状态下持续计数的后台AVAudioSession.playback模式。”我希望在用户落腕后继续运行”这一需求只对应一个结构性原语,即带有声明会话类型的WKExtendedRuntimeSession3

Apple为WKExtendedRuntimeSession支持的会话类型刻意收得很窄:3

  • self-care(简短的健康活动,前台运行时,10分钟限制)
  • mindfulness(静默冥想,前台运行时,1小时限制)
  • physical-therapy(拉伸和关节活动,后台运行时,1小时限制)
  • alarm(智能闹钟,后台运行时,30分钟限制,可通过start(at:)提前最多36小时调度)

健身应用使用HKWorkoutSession配合独立的workout-processing后台模式;该路径专为真正的健身活动而设,并非WKExtendedRuntimeSession类型。4 underwater-depth后台模式通过潜水会话API路径支持潜水和深度追踪应用,同样不通过WKExtendedRuntimeSession实现。一个应用可以将workout-processing与一种扩展运行时会话类型组合使用,但每个应用至多只能选择一种扩展运行时类型。4

不属于上述类别的应用无法使用WKExtendedRuntimeSession在落腕后继续运行。音频应用走的是另一条代码路径,需要使用AVAudioSession.Category.playback音频会话类别和Now Playing集成;导航应用使用CLLocationManager后台更新。Watch不是通用计算机;它是一台带有电池约束的设备,而运行时模型正是这些约束的强制执行者。

冥想计时器恰好契合mindfulness。契约如下:在Info.plist中声明后台模式,请求WKExtendedRuntimeSession,处理delegate回调,在计时器结束时结束会话。Apple文档说明mindfulness会话限制为一小时,系统在热压力或电量压力下有权酌情缩短。3

Return落地的模式

模式从Info.plist声明开始:4

<key>WKBackgroundModes</key>
<array>
    <string>mindfulness</string>
</array>

正是这一模式声明使会话类型生效。没有它,调用WKExtendedRuntimeSession().start()会静默失败,应用在落腕时会像没有任何后台模式的Watch应用一样直接挂起。

会话管理器本身必须存在于应用作用域。SwiftUI的视图生命周期对长生命周期的有状态对象并不友好:@StateObject@State的作用域限于拥有它们的视图,替换视图的导航推入会一并丢弃其状态。delegate在会话进行中被释放的WKExtendedRuntimeSession不会崩溃;会话仍在继续运行,但delegate回调(willExpiredidInvalidateWith)会触达一个已释放的对象,这意味着清理工作永远不会发生,也意味着下次startSession()调用会以为没有活跃会话从而启动一个重复会话。

已落地的模式是应用级作用域的单例。下方代码片段展示的是结构形态;生产代码会在每个方法内部加入日志以便观测:

import SwiftUI
import WatchKit

final class WatchSessionManager: NSObject, WKExtendedRuntimeSessionDelegate {
    static let shared = WatchSessionManager()

    private var session: WKExtendedRuntimeSession?

    private override init() {
        super.init()
    }

    var isSessionActive: Bool {
        session != nil
    }

    func startSession() {
        guard session == nil else { return }
        let newSession = WKExtendedRuntimeSession()
        newSession.delegate = self
        newSession.start()
        session = newSession
    }

    func endSession() {
        guard let existing = session else { return }
        existing.invalidate()
        session = nil
    }

    // MARK: - WKExtendedRuntimeSessionDelegate

    func extendedRuntimeSessionDidStart(_ session: WKExtendedRuntimeSession) {}

    func extendedRuntimeSessionWillExpire(_ session: WKExtendedRuntimeSession) {
        // Apple's "about to expire / finish and clean up" hook
    }

    func extendedRuntimeSession(
        _ session: WKExtendedRuntimeSession,
        didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason,
        error: Error?
    ) {
        self.session = nil
    }
}

除了协议遵循之外,还有三处结构性细节至关重要。

static let shared实例在Watch应用进程的整个生命周期内通过静态存储被持有;ARC不会释放它。 App级别的绑定带来的并非额外的引用持有,而是一个稳定的观察点。它所防范的bug模式是:会话管理器只被一个临时视图持有,该视图在会话进行中被弹出,视图随之消亡而static let shared仍存活,其副作用是任何被@StateObject包裹的管理器都会失去其观察循环并停止正确的重新渲染。请使用单例配合App级别的@Observable访问器,确保UI持续观察这个标准实例。

session属性是防止重复会话的保护机制。 带有”重新开始”按钮的计时器可能从多条路径调用startSession();guard session == nil检查就是那把锁。两个并发的扩展会话会引发不可预测的行为:有时第二个会话成功而第一个变为孤立状态,有时启动调用静默失败。单会话不变量从根本上预防了整类问题。

delegate回调主要用于记录,极少触发实际操作。 didStart回调每个会话触发一次,是用于观测的有用钩子;willExpire回调在系统强制失效之前触发,Apple的示例期望应用在此”会话结束前完成并清理任务”;didInvalidateWith回调则是清空会话引用以使下一次startSession()调用得以正常工作的地方。落地的模式是回调更新状态,状态机执行工作,而非回调直接执行工作

计时器管理器在每个会改变计时器是否处于活跃计数状态的转换处都会调用会话管理器:

@Observable final class WatchTimerManager {
    func start() {
        startExtendedSession()        // -> WatchSessionManager.shared.startSession()
        // ... start the timer state machine ...
    }

    func pause() {
        timer?.invalidate()
        isRunning = false
        endExtendedSession()          // -> WatchSessionManager.shared.endSession()
    }

    func reset() {
        // ... clear timer state ...
        endExtendedSession()
    }

    private func completeCycle() {
        // ... last cycle handling ...
        endExtendedSession()          // ends on final completion
    }
}

会话在暂停时、重置时以及最后一个周期完成时结束。产品层面的考量:暂停的冥想无需继续占用系统在mindfulness下授予的运行时预算;从暂停状态恢复时会重新获取一个新会话。代价在于,落腕状态下的暂停无法仅靠抬腕恢复;用户必须将应用重新带回前台才能恢复。收益则是暂停状态下的计时器电量消耗归零,且系统不会看到一个陈旧的会话。

落腕场景才是真正的测试

模拟器中的watchOS测试只是一种礼貌性的虚构。模拟器并不像真实的Apple Watch那样强制执行落腕运行时模型。只要模拟器窗口处于焦点状态,模拟器就会让应用保持前台;模拟器中的扩展运行时会话与完全没有会话的情形看起来毫无差别,因为前台应用无论如何都会持续运行。

真正的测试必须在真实的Apple Watch上进行:5

  1. 启动计时器。
  2. 落腕(或按下侧边按钮锁屏)。
  3. 等待30秒。
  4. 抬腕。

没有活跃扩展运行时会话时,Watch应用会被挂起;计时器状态在落腕的那一刻被冻结,并从冻结状态恢复。对于一段闭眼5分钟的冥想而言,这个bug在计时器误差等于闭眼时长之前是不可见的。

有活跃扩展运行时会话时,计时器持续计数。抬腕会显示出计时器处于正确的已逝时间位置。音频提示(若计时器在结束时播放)会在正确的墙钟时间触发,而非抬腕时间。

落腕场景正是Return首版Watch构建上线时所带的bug,而单例重构修复了它。修复方案就是上文的单例模式;bug的根源在于一个被SwiftUI视图持有的WatchSessionManager实例,在导航推入时被释放。系统侧的会话技术上仍在运行,但delegate已被释放;下次会话启动调用静默地变成空操作,因为管理器的session属性是设置在一个已经死亡的对象上的。真机测试在数秒内就能暴露这一失败。模拟器测试则永远无法暴露。

delegate回调到底告诉了你什么

WKExtendedRuntimeSessionInvalidationReason枚举了会话结束的各种方式:6

原因 触发场景
none 应用调用invalidate()显式失效会话
sessionInProgress 同类型的会话已在运行
expired 已达到系统设定的时间限制
resignedFrontmost 会话运行期间另一个应用变为前台应用
suppressedBySystem 系统抑制了会话(低电量、热压力)
error 发生不可恢复的错误;请检查error参数

对产品设计而言至关重要的几个原因:

expired意味着已达到系统设定的时间限制。 Apple文档说明mindfulness会话限制为一小时;3 Return最长的冥想时长为60分钟,这正是文档中的上限。一段90分钟的冥想无法在单次mindfulness会话内完成:计时器会在到达一小时大关时中途死亡。产品上的决定是将可用时长上限设定为运行时模型文档化能够保证的范围,而非寄望于系统的容忍度。

resignedFrontmost意味着用户打开了另一个Watch应用,你的会话因此失败。 Watch用户经常滑到另一个应用后就忘了。产品上的决定要么是切走时暂停(状态保留,用户可以回来),要么是切走时结束(会话终止,用户得到”提前停止”的信号)。Return选择切走时暂停,这样用户可以在冥想中接电话后再回来。

suppressedBySystem是”手表过热”的礼貌说法。 处于热压力或低电量下的watchOS设备可能会撤销扩展运行时会话,即便应用并无误用。会话管理器必须优雅地处理这种情况:清除引用,呈现非阻塞的警告,且不进入试图重启系统刚刚拒绝的会话的状态。

willExpire回调在会话即将到期时触发;Apple的示例将其定位为”会话结束前完成并清理任务”的时刻。3 此回调是应用可以写入最终状态快照、播放收尾音频提示或呈现”会话即将结束”UI的位置。Return当前仅记录此回调日志;更丰富的清理(HealthKit日志条目、音频淡出)发生在计时器的重置和完成路径上,这部分内容列在如果重做我会怎么做的willExpire窗口待办清单中。

如果重做我会怎么做

如果Return从零开始,有两件事会做得不同。

对于任何因HealthKit集成而价值更高的会话,使用HKWorkoutSession 冥想计时器恰好处于mindfulnessworkout-processing的边界。Mindfulness是v1的正确选择,因为数据模型更简单,用户预期是”这是冥想,不是健身”。HKWorkoutSession携带更细粒度的HealthKit集成(会话开始、会话结束、片段、事件),并提供更丰富的LiveWorkoutBuilder接口用于累积数据。这是架构层面的判断,而非Apple的文档化保证:对于价值依赖于详细会话遥测的应用而言,workout-session路径处理的是WKExtendedRuntimeSession所不具备的结构。

从第一天起就加入会话状态可观测层。 Return的第一版将会话事件记录到控制台。第二版增加了用于调试的设备端会话状态可见性。第三版会暴露一个开发者模式开关,在出现异常时向用户呈现会话原因历史,而不是把会话失效当作黑箱处理。watchOS运行时是不透明的;调试层必须为此弥补。

何时WKExtendedRuntimeSession不是正确答案

会话类型不适配的三种情形:

需要片段标记、心率流或活跃卡路里追踪的健身活动。 直接使用HKWorkoutSession配合HKLiveWorkoutBuilder。Workout API是Apple为真正的健身活动(以及步行冥想或剧烈运动)记录的路径;WKExtendedRuntimeSession是为非健身会话(例如mindfulness或alarm)记录的路径。冥想应用无需健身;Couch-to-5K应用则需要。

需要Now Playing界面的音频播放。 使用配置为播放的AVAudioSession配合watchOS音频会话授权;Now Playing集成加上系统播放界面正是音频应用所需,而音频路径与WKExtendedRuntimeSession完全无关。WKExtendedRuntimeSession不会赋予你Now Playing或系统音频路由能力。

用户无感知下的长时间数据同步。 使用WKApplicationRefreshBackgroundTask处理系统调度的周期性刷新窗口。用户不在应用内;应用无需保持运行;它需要做的是短暂唤醒并刷新数据。两种后台任务模型与扩展运行时会话模型服务于截然不同的需求。

该模式对在watchOS 11+上线的应用意味着什么

三点要点。

  1. Watch运行时模型是opt-in的。选定一种会话类型,在其规则之内生存。 试图在watchOS上做”通用后台工作”的应用必将落败。请选择mindfulnessworkout-processingself-carephysical-therapyalarmunderwater-depth,并围绕所选会话类型附带的运行时预算设计用户体验。

  2. 会话delegate必须存在于应用作用域。SwiftUI的视图生命周期不保护长生命周期的有状态对象。@main App级别绑定的static let shared单例,是能够在导航推入、视图替换和SwiftUI正常释放行为下存活的最小模式。

  3. 在真机上测试。模拟器不会强制执行落腕运行时模型。 Watch应用在模拟器中无法测出的bug,正是会带给用户的bug。

可将本文与笔者关于同一系列应用的先前撰文一并阅读:跨平台SwiftUI上线(Return已上线iPhone、iPad、Watch、Mac和Apple TV);Live Activities状态机(同一计时器在iOS侧的呈现);HealthKit模式(Watch的mindfulness会话最终落地于用户Health数据中的位置)。完整内容收录于Apple生态系列中心。如需更广泛的iOS与AI agents结合的语境,请参阅iOS Agent Development指南

FAQ

什么是watchOS扩展运行时会话?

watchOS扩展运行时会话(WKExtendedRuntimeSession)是Watch应用用以在用户落腕后继续运行的API。会话必须通过Info.plist中的WKBackgroundModes声明类型(mindfulness、workout-processing、alarm等)。没有活跃的扩展会话,watchOS会在落腕后不久挂起应用。

为什么我的watchOS计时器在用户落腕时停止计数?

除非有受支持类型的活跃WKExtendedRuntimeSession正在运行,否则Watch应用会在落腕后不久挂起。未启动此类会话的计时器管理器会看到其后台运行时被切断,计时器状态在落腕的那一刻冻结,直到用户再次抬腕才恢复。

WKExtendedRuntimeSessionHKWorkoutSession有何区别?

WKExtendedRuntimeSession是面向非健身会话(如mindfulness、alarm或self-care)的通用扩展运行时API。HKWorkoutSession是面向真正健身活动的API;它与HealthKit集成,支持片段标记,是步行冥想或剧烈运动的文档化路径。不具备健身级遥测的正念应用使用前者;健身应用使用后者。

系统能否撤销我的扩展运行时会话?

可以。WKExtendedRuntimeSessionInvalidationReason包含expired(达到系统时间限制)、resignedFrontmost(另一个Watch应用变为前台应用)和suppressedBySystem(低电量或热压力)。会话管理器必须妥善处理每一种情形:清除引用,使计时器状态做出恰当反应,并确保下一次会话启动调用能够正确工作。

SwiftUI watchOS应用中,会话管理器应当存在于何处?

存在于应用作用域,作为从@main App结构体绑定的单例。SwiftUI视图作用域的状态(@State@StateObject)会在导航推入、视图替换或应用进入后台时被释放。被视图持有的会话delegate若在会话进行中被释放,会导致会话引用泄漏,并使后续会话无法干净地启动。

参考文献


  1. 作者的Return,一款SwiftUI冥想计时器,已于2026年4月21日在App Store发布,支持iPhone、iPad、Mac、Apple Watch和Apple TV。Watch应用使用WKExtendedRuntimeSession配合mindfulness后台模式实现周期计时器的运行时。 

  2. Apple Developer,“About the background execution sequence”。iOS侧的后台运行时授权机制(音频会话、位置、BGTaskScheduler)及其与watchOS的差异。 

  3. Apple Developer,“WKExtendedRuntimeSession”。会话类型、生命周期、delegate回调、运行时限制以及WKBackgroundModes Info.plist键。 

  4. Apple Developer,“Information Property List: WKBackgroundModes”。受支持的会话类型字符串:workout-processingmindfulnessself-carephysical-therapyalarmunderwater-depth。 

  5. Apple Developer,“Building a watchOS app” 及WatchKit测试指南。真机运行时行为在watchOS模拟器中无法复现;模拟器不会强制执行落腕挂起。 

  6. Apple Developer,“WKExtendedRuntimeSessionInvalidationReason”。枚举值:nonesessionInProgressexpiredresignedFrontmostsuppressedBySystemerror。 

相关文章

iOS 26 上的 HealthKit + SwiftUI:来自两款已上架应用的授权流程、样本类型与跨平台模式

来自 Water(饮水追踪,HKQuantitySample)和 Return(正念会话,HKCategorySample)的真实生产模式。权限 UX、async 封装、watchOS 变体,以及需要避开的陷阱。

5 分钟阅读

SwiftUI 是由什么构成的

SwiftUI 是建立在值类型 View 树之上的结果构建器 DSL。一旦底层基础变得清晰可见,AnyView、Group 和 ViewBuilder 就不再神秘。

4 分钟阅读

清理层才是真正的AI智能体市场

Charlie Labs从构建智能体转向清理智能体留下的烂摊子。AI智能体市场正从生成转向证明。清理才是持久的层级。

2 分钟阅读