watchOS运行时是一份契约,而非后台任务
类型: 已发布代码。本文记录了Return在生产环境中采用的watchOS运行时模式。Return是App Store上的一款SwiftUI冥想计时器;其Watch应用运行多周期计时器,必须在用户放下手腕时持续计时。1 能够在该约束下存活的模式是WKExtendedRuntimeSession加上一个全局的、应用级作用域的delegate。其他一切都会在手表休眠的瞬间死去。
watchOS不是屏幕更小的iOS。其运行时模型截然不同。iOS通过音频会话、位置更新、BGTaskScheduler以及其他若干机制,赋予应用充足的前台预算和虽递减但真实存在的后台运行时。2 而watchOS只在抬腕落下后给前台应用以秒计的预算,之后除非应用已与系统签订运行时契约,否则便会被挂起。这里没有”我只是在后台做点事”这样的便利。只有”我正在运行workout、mindfulness、smart alarm、navigation或health monitoring任务”,仅此而已。3
Return的Watch目标是一个mindfulness计时器。会话契约为WKBackgroundModes: mindfulness。运行时API为WKExtendedRuntimeSession。让Watch应用从抬腕落下即崩溃转变为能撑过25分钟冥想的模式,正是本文所要描述的。
TL;DR
- watchOS没有iOS式的后台。前台运行时在抬腕落下后不久结束,只有已注册的会话类型才能继续运行。
WKExtendedRuntimeSession是API接口。会话必须声明类型;对于冥想计时器而言,类型通过Info.plist中的WKBackgroundModes: mindfulness隐式声明。- 会话管理器必须存活于应用作用域,而非视图作用域。SwiftUI的视图生命周期会在导航时释放视图所拥有的对象;一个被释放的会话delegate就是一个死掉的会话,即便会话本身仍在运行也是如此。
WKExtendedRuntimeSessionDelegate回调即是契约:didStart、willExpire、didInvalidateWith。expire回调在系统强制失效之前触发;Apple将其描述为应用”完成并清理”的窗口。- 没有活动扩展会话时的抬腕落下会暂停计时器。有活动扩展会话时的抬腕落下会让计时器继续。会话就是”已发布产品”与”二次使用即崩溃”之间的分水岭。
watchOS未以iOS方式解决的后台问题
iOS应用在需要让应用在熄屏后保持运行时,可以借助多种后台机制:2
- 一个使用
.playback类别的AVAudioSession可让音频应用在播放音乐时保持存活。 CLLocationManager后台更新可让导航应用在蓝条下保持存活。BGTaskScheduler将简短的维护工作排入队列,由系统按其自身节奏调度。- 前台UI扩展(Live Activity、CallKit、PushKit)将应用进程桥接到系统控制的渲染表面。
这些在watchOS上都未必如您所想般奏效。Watch应用没有相同的后台任务调度器。没有可在静默中让计时器持续计数的后台AVAudioSession.playback模式。它们只有一个用于”用户放下手腕后我希望继续运行”的结构原语,那便是带有声明会话类型的WKExtendedRuntimeSession。3
Apple通过WKBackgroundModes支持的会话类型是有意收窄的:4
workout-processing(配合HKWorkoutSession用于实际锻炼)mindfulness(用于冥想计时器和呼吸练习)self-care(用于引导式日常活动)physical-therapy(用于理疗会话应用)alarm(用于基于时间的唤醒闹钟)underwater-depth(用于潜水和深度跟踪应用)
不属于上述任一类别的应用无法使用WKExtendedRuntimeSession在抬腕落下后继续运行。音频应用走的是mediaPlayback音频会话类别和Now Playing集成的另一条代码路径;导航应用使用CLLocationManager后台更新。Watch不是通用计算设备;它是一台受电池约束的设备,运行时模型对此严格执行。
冥想计时器恰好契合mindfulness。契约为:在Info.plist中声明后台模式,请求一个WKExtendedRuntimeSession,处理delegate回调,并在计时器结束时终止会话。系统会为每个会话授予约一小时的运行时,并保留在热压力或电池压力下自行缩短的权力。3
Return所采用的发布模式
模式始于Info.plist声明:4
<key>WKBackgroundModes</key>
<array>
<string>mindfulness</string>
</array>
模式声明使会话类型有效。若无此声明,调用WKExtendedRuntimeSession().start()会静默失败,应用在抬腕落下时会像未声明任何后台模式的Watch应用一样被挂起。
会话管理器本身必须存活于应用作用域。SwiftUI的视图生命周期对长生命周期有状态对象并不友好:@StateObject和@State的作用域限于拥有它们的视图,而替换视图的导航push会连同状态一并丢弃。一个delegate在会话中途被释放的WKExtendedRuntimeSession并不会崩溃;会话仍在继续运行,但delegate回调(willExpire、didInvalidateWith)会到达一个已释放的对象,这意味着清理永不发生,意味着下一次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加上@main App层级的@State private var sessionManager = WatchSessionManager.shared使管理器在Watch应用进程的整个生命周期中保持存活。 SwiftUI不会仅因视图持有单例而保留它;上述绑定才是告诉运行时保留引用的方式。若无App层级的绑定,当无视图持有时,ARC可能丢弃管理器。
session属性是防止重复会话的保护机制。 一个带有”重新开始”按钮的计时器可能从多个路径调用startSession();guard session == nil检查就是那把锁。两个并发的扩展会话会导致不可预测的行为:有时第二个成功而第一个变为孤儿,有时启动调用静默失败。单一会话不变量从根本上防止了此类问题。
delegate回调记录日志,但很少采取行动。 didStart回调每会话触发一次,是观察性的有用钩子;willExpire回调在系统强制失效之前触发,是Apple期望应用”完成并清理”之处;didInvalidateWith回调是会话引用清除的位置,以便下一次startSession()调用能正常工作。生产中的模式是回调更新状态,状态机执行工作,而非回调直接执行工作。
计时器管理器在每个改变计时器是否活跃计数的转换点都会调用会话管理器:
final class WatchTimerManager: ObservableObject {
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
- 启动计时器。
- 放下手腕(或按侧边按钮锁屏)。
- 等待30秒。
- 重新抬起手腕。
若无活动扩展运行时会话,Watch应用会被挂起;计时器状态在抬腕落下的瞬间被冻结,并从该冻结状态恢复。对于一段让用户闭眼的5分钟冥想而言,这个bug在闭眼期间是不可见的,直到计时器偏差正好等于闭眼时长。
若有活动扩展运行时会话,计时器会持续计数。抬腕时显示的是正确流逝位置上的计时器。音频提示(如果计时器在完成时播放)会在正确的挂钟时间触发,而非抬腕的时间。
抬腕落下场景正是Return在v1中存在并在v2中修复的bug。修复方案就是上述单例模式;bug在于WatchSessionManager实例被一个SwiftUI视图持有,而该视图在导航push时被释放。会话从系统侧来看仍在技术性地运行,但delegate已被释放;下一次会话启动调用静默地变成空操作,因为管理器的session属性已设置在一个现已死亡的对象上。真机测试在数秒内即可暴露此故障。模拟器测试则永远无法暴露。
delegate回调究竟告诉您什么
WKExtendedRuntimeSessionInvalidationReason枚举了会话结束的方式:6
| 原因 | 触发时机 |
|---|---|
none |
应用通过调用invalidate()显式使会话失效 |
sessionInProgress |
已有同类型会话正在运行 |
expired |
达到系统设定的时间限制 |
resignedFrontmost |
会话运行期间另一应用变为最前 |
suppressedBySystem |
系统抑制了会话(低电量、热压力) |
error |
发生不可恢复的错误;请检查error参数 |
对产品设计至关重要的几个原因:
expired意味着用户获得了您所申请的完整会话。 会话运行至其自然结束。Return最长的冥想时长为60分钟,恰处于mindfulness会话通常被授予的边缘。一段90分钟的冥想会例行触发expired,而计时器会在会话中途死亡。产品决策是将可用时长限制在运行时模型实际能交付的范围内。
resignedFrontmost意味着用户打开了另一个Watch应用,您的会话告负。 Watch用户很容易划到另一个应用然后忘了回来。产品决策可以是resign时暂停(保留状态,用户可以回来)或resign时结束(会话结束,用户得到”您提前停止了”的信号)。Return选择resign时暂停,让用户可以在冥想中途接电话后再回来。
suppressedBySystem是”手表过热”的礼貌说法。 一台处于热压力或低电量下的watchOS设备可能在没有应用滥用的情况下撤销扩展运行时会话。会话管理器必须优雅地处理此情形:清除引用、呈现非阻塞的警告、避免进入试图重启系统刚拒绝的会话的状态。
willExpire回调在会话即将到期时触发,文档将其描述为应用”完成并清理”的时刻。3 该回调是应用可以写入最终状态快照、播放收尾音频提示或呈现”会话即将结束”UI的地方。Return当前仅记录该回调;更丰富的清理(HealthKit日志条目、音频淡出)发生在计时器的reset和completion路径上,并被列入了若重新设计会有何不同中关于willExpire窗口的清单。
若重新设计会有何不同
如果Return从零开始,有两件事。
对于任何因HealthKit集成而价值提升的会话,使用HKWorkoutSession。 冥想计时器位于mindfulness和workout-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+上发布的应用意味着什么
三点要点。
-
Watch运行时模型是opt-in的。选择一种会话类型并在其规则内生存。 试图在watchOS上做”通用后台工作”的应用必然失败。请选择
mindfulness、workout-processing、self-care、physical-therapy、alarm或underwater-depth,并围绕所选会话类型附带的运行时预算来设计用户体验。 -
会话delegate必须存活于应用作用域。SwiftUI的视图生命周期不保护长生命周期有状态对象。 一个绑定在
@main App层级的static let shared单例,是能在导航push、视图替换以及SwiftUI正常释放行为下存活的最小模式。 -
在真机上测试。模拟器不强制执行抬腕落下的运行时模型。 Watch应用在模拟器中无法测试出来的bug,正是它发布给用户的bug。
请将本文与我对同一系列应用的此前文章配合阅读:跨平台SwiftUI发布(Return在iPhone、iPad、Watch、Mac和Apple TV上发布);Live Activities状态机(同一计时器的iOS侧界面);HealthKit模式(Watch的mindfulness会话在用户Health数据中的归宿)。完整集合位于Apple生态系统系列中心。如需更广泛的iOS与AI agent结合的背景,请参阅iOS Agent开发指南。
常见问题
什么是watchOS扩展运行时会话?
watchOS扩展运行时会话(WKExtendedRuntimeSession)是Watch应用用以在用户放下手腕后保持运行的API。会话必须通过Info.plist中的WKBackgroundModes声明类型(mindfulness、workout-processing、alarm等)。若无活动扩展会话,watchOS会在抬腕落下后不久挂起应用。
为何用户放下手腕时我的watchOS计时器会停止计数?
除非有受支持类型的活动WKExtendedRuntimeSession正在运行,否则Watch应用会在抬腕落下后不久被挂起。一个不启动此类会话的计时器管理器会看到其后台运行时被切断,计时器状态在抬腕落下的瞬间冻结,直到用户再次抬起手腕。
WKExtendedRuntimeSession和HKWorkoutSession有何区别?
WKExtendedRuntimeSession是用于非锻炼会话(如mindfulness、alarm或self-care)的通用扩展运行时API。HKWorkoutSession是用于实际锻炼的API;它与HealthKit集成,支持分段标记,是行走冥想或剧烈活动所记载的路径。无锻炼级遥测需求的mindfulness应用使用前者;锻炼应用使用后者。
系统能撤销我的扩展运行时会话吗?
可以。WKExtendedRuntimeSessionInvalidationReason包括expired(达到系统时间限制)、resignedFrontmost(另一Watch应用变为最前)和suppressedBySystem(低电量或热压力)。会话管理器必须干净地处理每种情况:引用清除、计时器状态做出适当反应、下一次会话启动调用正确工作。
在SwiftUI watchOS应用中,会话管理器应存活于何处?
应在应用作用域,作为从@main App结构体绑定的单例。SwiftUI的视图作用域状态(@State、@StateObject)会在导航push、视图替换或应用进入后台时被释放。一个在会话中途被释放的视图所拥有的会话delegate会导致会话引用泄漏,并阻止后续会话的干净启动。
参考文献
-
作者的Return,一款于2026年4月21日在App Store发布的SwiftUI冥想计时器,可用于iPhone、iPad、Mac、Apple Watch和Apple TV。Watch应用使用带
mindfulness后台模式的WKExtendedRuntimeSession实现周期计时器运行时。 ↩ -
Apple Developer,“About the background execution sequence”。iOS侧后台运行时机制(音频会话、位置、BGTaskScheduler)以及它们与watchOS的差异。 ↩↩
-
Apple Developer,“WKExtendedRuntimeSession”。会话类型、生命周期、delegate回调、运行时限制以及
WKBackgroundModesInfo.plist键。 ↩↩↩↩ -
Apple Developer,“Information Property List: WKBackgroundModes”。受支持的会话类型字符串:
workout-processing、mindfulness、self-care、physical-therapy、alarm、underwater-depth。 ↩↩ -
Apple Developer,“Building a watchOS app”以及WatchKit测试指南。真机运行时行为在watchOS模拟器中无法重现;模拟器不强制执行抬腕落下挂起。 ↩
-
Apple Developer,“WKExtendedRuntimeSessionInvalidationReason”。枚举值:
none、sessionInProgress、expired、resignedFrontmost、suppressedBySystem、error。 ↩