苹果平台 macOS/iOS/watchOS 后台任务的条件与执行

最近我一直在处理 SleepTap 的后台刷新问题。这个问题看起来只是一个 BGAppRefreshTask 没有执行,后来却牵扯出了 AlarmKit、Apple Watch Smart Session、WatchConnectivity、天气缓存和 WidgetKit。

处理完之后我发现,苹果平台上的“后台任务”其实是一个非常混乱的说法。

Task 可以叫后台任务,BGAppRefreshTask 也叫后台任务,静默推送唤醒应用还是后台任务,AlarmKit 关闭闹钟时启动应用进程,同样可以在后台执行代码。它们听起来像是一回事,实际上完全不是一回事。

判断它们最简单的方式,不是看 API 名字里有没有 Background,也不是看代码里有没有 Task,而是看一件事:

这段代码为什么在此刻有资格运行?

前台执行的“后台任务”

最常见的就是 GCD、OperationQueue 和 Swift Concurrency 的 Task

比如:

Task {
    await updateWeather()
}

这段代码看起来异步执行,不阻塞界面,所以很多人会顺口将它叫做后台任务。但它实际上只是前台应用中的并发任务。

Task.detached 也一样。它只是脱离当前 Actor,并没有顺便向系统申请后台运行资格。应用一旦被挂起,里面的 Task、GCD 队列、Timer 和异步网络请求也会一起停下来。

这不叫系统后台任务,这叫应用进程还活着时,在另外一条执行路径上工作。

iOS

iOS 应用进入后台时,系统会先给应用很短的时间完成收尾。Apple 的文档明确写了,applicationDidEnterBackground 默认大约只有 5 秒,之后应用通常就会进入 suspended 状态。

如果当前工作确实需要多一点时间完成,可以使用:

UIApplication.shared.beginBackgroundTask(...)

但这个 API 不是让应用从明天开始在后台工作,也不是让应用长期运行。它只是说:“用户现在把应用切到了后台,请再给我一点时间,把手头这件事做完。”

Apple 没有承诺固定时长,应用只能读取 backgroundTimeRemaining,并且必须处理 expiration。用完之后不主动结束,系统可以直接终止应用。Apple 关于延长后台执行时间的说明

iOS 26 又增加了 BGContinuedProcessingTask。它适合用户在前台主动开始了一项需要数分钟甚至更久的任务,例如本地模型处理、图像分析,然后用户中途把应用切到后台。这种任务必须由用户操作启动,不能拿来冒充定时后台刷新。Apple 关于 BGContinuedProcessingTask 的说明

watchOS

watchOS 比 iOS 更严格。用户放下手腕后,普通应用很快就会进入后台并被挂起。普通 Swift Task 不会因为它跑在 Apple Watch 上,就自动获得整夜执行资格。

这也是为什么睡眠检测、运动、音频和 Smart Alarm 都需要专门的系统会话。

macOS

macOS 不一样。Mac 应用退到后台之后,通常仍然是一个正常运行的进程。窗口不在最前面,不等于进程被冻结。因此 GCD、Task、Timer 和普通网络请求原则上都可以继续运行。

它没有 iOS 那种统一的“进入后台几秒后挂起”限制。真正的限制来自 App Nap、系统睡眠、内存压力、用户退出应用,以及系统对 CPU、磁盘和定时器的调度。

所以,同样一段 Swift Task,放在 iOS 上不能视为后台保证,放在 macOS 上却可能连续运行很多天。这也是跨平台代码最容易产生误判的地方。

向系统预约的后台任务

另一类后台任务,是应用提前向系统登记:“以后条件合适时,请启动我一次。”

注意,是条件合适时,不是我指定几点就几点。

BGAppRefreshTask

BGAppRefreshTask 适合 iOS 和 iPadOS 的短内容刷新。例如刷新新闻、同步少量数据、更新下一批本地通知。

应用需要:

  • 在应用启动时注册任务标识。
  • Info.plist 中声明允许的标识。
  • 提交 BGAppRefreshTaskRequest
  • 设置 expiration handler。
  • 执行完成后报告成功或失败。

earliestBeginDate 只是最早可以执行的时间,不是预约时间。设置成早上 7 点,真正执行时间可能是 7 点多,也可能是几个小时后,甚至根本不执行。

系统会综合用户打开应用的习惯、电量、网络、后台刷新开关以及应用过去的执行表现决定是否运行。Apple 给这类任务的执行时间最多约 30 秒。Apple 关于后台策略的说明

这次 SleepTap 的教训就是,不能把所有可靠性都押在它上面。它适合兜底,不适合闹钟。

BGProcessingTask

BGProcessingTask 也需要提前注册,但它适合数据库维护、文件处理和比较慢的数据更新。

它可以运行数分钟,不过通常要求设备处于空闲状态。用户重新开始使用设备时,系统可能终止正在执行的 Processing Task。应用仍然必须提供 expiration handler。Apple 关于 BGProcessingTask 的说明

它的时间比 App Refresh 长,但调度时间更不可控。所以让 BGAppRefreshTask 先启动,再提交一个 Processing Task 等第二次唤醒,通常并不会变得更可靠,只是增加了一次不确定调度。

watchOS 后台刷新

watchOS 有自己的 WKApplicationRefreshBackgroundTask。应用可以建议一个未来时间,系统再决定什么时候唤醒。

这类任务通常只有几秒钟。适合读取少量数据、更新 complication 缓存、重新安排下一次任务,不适合执行长时间计算。Apple 关于 watchOS 后台执行的说明

而且,通过 Xcode 连接调试时,应用可能不会正常进入挂起状态。后台任务在调试器下成功,不足以证明自然调度一定成功。这个我现在是深有体会。

macOS 的后台调度

macOS 可以使用 NSBackgroundActivityScheduler 安排低优先级维护工作,例如自动保存、备份、内容获取和更新检查。

它适合可以延后的任务,建议间隔通常在 10 分钟以上。开发者提供 interval 和 tolerance,系统根据能耗、温度和 CPU 状态选择合适时间。Apple 关于 NSBackgroundActivityScheduler 的说明

它和 iOS 的 BGTask 有点像,但 Mac 应用本身通常不会因为退到后台就被挂起。所以它解决的更多是节能调度,而不是争取应用存活。

如果 Mac 应用退出后仍然需要长期服务,就不该指望一个普通 Task 或 Timer。应该注册 Login Item、LaunchAgent 或 LaunchDaemon。macOS 13 之后可以通过 SMAppService 管理,用户也可以在系统设置中关闭这些后台项目。Apple Service Management 文档

Login Item 可以从用户登录后一直运行到退出;LaunchAgent 由 launchd 代表当前用户管理;LaunchDaemon 则是系统级服务。它们没有“30 秒”这种短任务上限,但这已经是后台进程,不是普通应用刷新。

被事件主动唤醒的后台

还有一类不是应用预约系统,而是外部事件发生以后,系统认为应用需要处理,于是临时唤醒应用。

这里的“主动”也不是应用自己偷偷启动自己,而是 APNs、AlarmKit、WatchConnectivity、HealthKit 或 Core Location 主动通知系统。

静默远程通知

服务器可以通过 APNs 发送 content-available 静默通知。系统收到后,可能在后台启动或者唤醒 iOS、watchOS 应用。

应用得到的处理时间是 30 秒。

但静默推送是低优先级通知。系统不保证送达,发送太多还会限流。Apple 建议不要尝试每小时发送超过两三次。如果应用被强制退出,等待中的通知也可能被系统丢弃。Apple 关于后台推送的说明

所以,远程通知适合“服务器的数据变了,通知应用尽快刷新”,不适合精确闹钟,也不适合当成每五分钟执行一次的 cron。

AlarmKit 和 LiveActivityIntent

AlarmKit 的 Stop、Snooze 等按钮可以绑定 App Intent。我们在 SleepTap 中使用的是 LiveActivityIntent

当用户点击停止闹钟时,系统会在不打开应用界面的情况下启动应用进程,然后执行 Intent。也就是说,在 perform() 返回之前,我们可以更新睡眠记录、重新安排通知、更新起床日期,也可以尝试一次天气刷新。

这不是 BGTaskScheduler 调度的任务。它是用户操作触发的系统 Intent 执行窗口。

Apple 没有公开承诺它可以运行多少秒,因此正确做法就是尽快完成。不能在 Intent 中启动一个无限期 Task,然后立刻返回,期待应用继续活着。Intent 返回之后,系统什么时候挂起进程,不归应用决定。Apple 关于 LiveActivityIntent 的说明

还有一个条件很重要:必须真的执行 Intent。闹钟只是响了,但用户没有点击绑定的按钮,不等于我们的代码一定执行。

watchOS Smart Alarm Session

SleepTap 的 Smart Session 使用 WKExtendedRuntimeSession 的 Smart Alarm 类型。

这个机制是 watchOS 明确提供的。应用必须在前台时调用 start(at:) 安排会话,最远只能安排未来 36 小时内的时间。到了安排时间,即使应用已经被系统挂起或者终止,watchOS 也可以重新启动应用并开始这个会话。

Smart Alarm 的执行窗口是 30 分钟。它就是为起床前监测心率和动作、选择合适的唤醒时间设计的。每个应用一次只能安排一个这样的会话,而且必须声明对应的 Background Mode。Apple 关于 Extended Runtime Session 的说明

所以这也不是偷偷运行。它远比拿一个 Timer 在凌晨等着靠谱。

不过,30 分钟不是让应用满负荷计算 30 分钟。CPU 长时间过高,系统仍然可以因为资源超限取消会话。

WatchConnectivity

Apple Watch 把数据传给 iPhone 时,又分成两种情况。

如果 iPhone 可达,sendMessage 可以立即发送消息。Apple 明确说明,从 watchOS 发送消息时,如果配对的 iPhone 应用可达,系统会唤醒它。

如果不可达,可以使用 transferUserInfo。数据交给系统排队,即使发送端应用已经被挂起,传输仍可继续。系统会在条件合适时投递到另一端。

这就是 SleepTap 的 Smart Session 检测到真实起床后,可以让 iPhone 更新七天起床日期和天气的原因。不是手表直接在 iPhone 上运行代码,而是 WatchConnectivity 把事件交给系统,系统再唤醒 iPhone 应用处理。Apple WatchConnectivity 示例

这类回调没有公开的固定长时间保证。watchOS 收到 WatchConnectivity 后台任务时,Apple 的要求是尽快处理并调用完成方法。没有完成,反而会消耗系统分配给应用的后台预算。

HealthKit 与 Core Location

HealthKit 的 Observer Query 可以注册 Background Delivery。当匹配的健康数据发生变化时,HealthKit 会唤醒应用。它的频率是上限,不是保证。例如某些数据最多每小时通知一次,系统还会合并更新。Apple 关于 HealthKit 后台投递的说明

Core Location 也可以因为持续定位、显著位置变化、访问记录或区域监控唤醒应用。但需要正确的定位权限和 Background Mode。iOS 和 watchOS 会挂起普通后台应用,macOS 则不会仅仅因为应用不在前台就挂起它。Apple 关于后台定位的说明

定位后台模式只能用于真的需要定位的功能。为了让天气每天刷新一次而长期运行定位,这显然说不过去吧。

系统代替应用工作的后台

还有一些机制更特殊。真正耗时间的工作不是应用进程完成,而是系统代替应用完成。应用只在开始和结束时参与。

后台 URLSession

后台 URLSession 的上传和下载由另外一个系统进程执行。应用即使被挂起,传输仍然可以继续。下载完成后,系统再恢复或者重新启动应用,把结果交给 delegate。

所以一个 2GB 文件可以下载几个小时,并不意味着应用获得了几个小时 CPU 时间。真正有长时间资格的是系统下载进程,应用只得到一个较短的结果处理窗口。Apple 关于后台下载的说明

如果每次系统唤醒以后,应用又提交一个新的单独下载,系统还会逐步增加延迟。这也是为了防止开发者借后台 URLSession 不断唤醒应用。

WidgetKit

Widget 看起来一直显示在桌面上,很容易让人误以为 Widget Extension 一直在运行。其实没有。

WidgetKit 保存的是 Timeline。系统到了某个时间,直接显示开发者提前提供的 Timeline Entry。只有需要生成新 Timeline 时,系统才短暂启动扩展。

常用 Widget 每天通常有大约 40 到 70 次刷新预算,大致相当于每 15 到 60 分钟一次,但实际次数会根据用户查看频率和系统状态变化。Timeline Entry 最好至少间隔约 5 分钟。Apple 关于 Widget 刷新预算的说明

WidgetCenter.reloadTimelines 也只是告诉系统“数据变了,请考虑重新加载”,不是让 Widget 立即执行。

如果只是倒计时,更不应该每秒刷新 Timeline。WidgetKit 的动态日期文本可以由系统持续绘制,扩展本身根本不需要运行。

WidgetKit 适用于 iOS、iPadOS、macOS 和 watchOS。Apple Watch 的 complication 现在也是同一套思路。

本地通知和 Live Activity

本地通知由系统保存并展示。通知到时间出现,不代表应用被启动了。

Live Activity 也是系统负责显示。通过 ActivityKit 或推送更新 Live Activity,主要是在更新系统维护的界面状态,不等于主应用获得了一次任意后台执行机会。

只有用户点击绑定的 Intent、通知 Action,或者系统明确把事件交给应用处理时,应用代码才有执行资格。

持续型后台模式

音频播放、导航定位、蓝牙通信和 Workout Session 又是另外一种后台。

它们不是“每隔一小时启动一次”,而是功能已经在前台开始,进入后台后仍然需要持续运行。

执行时间通常跟真实会话绑定:

  • 音频播放期间可以继续处理音频。
  • 导航和符合条件的定位会话可以继续接收位置。
  • Workout Session 可以在运动期间运行。
  • 蓝牙外设通信可以在声明对应模式后继续。
  • watchOS Extended Runtime Session 按类型获得 10 分钟、30 分钟或者 1 小时。

这里没有一个统一的 30 秒限制,但也不能拿来执行无关工作。播放一段静音音频,只为了让天气刷新保持活跃,这不叫利用系统能力,这叫滥用后台模式。

系统审核和运行时限制看的都是功能是否匹配,而不是 Info.plist 里有没有成功勾选 Background Mode。

执行时间到底应该怎么看?

整理下来,大致是这样:

机制 主要系统 条件 可用时间
GCD、Swift Task macOS、iOS、watchOS 应用进程仍可运行 没有额外后台资格
iOS 普通退后台 iOS 场景进入后台 默认约 5 秒收尾
beginBackgroundTask iOS 前台工作尚未完成 系统动态决定,无固定保证
BGAppRefreshTask iOS、iPadOS 注册、提交、系统选择执行 最多约 30 秒
BGProcessingTask iOS、iPadOS 注册、设备空闲及相关条件 可运行数分钟,随时可能过期
BGContinuedProcessingTask iOS、iPadOS 26 以后 用户主动在前台开始工作 数分钟或更久,无固定保证
WKApplicationRefreshBackgroundTask watchOS 系统接受预约 通常几秒
Smart Alarm Session watchOS 前台安排,未来 36 小时内 30 分钟
静默推送 iOS、watchOS APNs 投递且系统未限流 30 秒
AlarmKit Intent iOS 用户执行绑定的闹钟操作 未公开固定值,应尽快完成
WatchConnectivity iOS、watchOS 消息到达或系统投递队列 未公开固定值,应尽快完成
HealthKit 后台投递 iOS、watchOS 数据变化、权限和 entitlement 有效 未公开固定值,事件处理应简短
后台 URLSession 苹果平台 系统接管上传或下载 传输可很长,应用回调时间很短
WidgetKit Timeline iOS、macOS、watchOS 系统刷新预算允许 扩展短暂运行,常用 Widget 每天约 40–70 次刷新
NSBackgroundActivityScheduler macOS 应用运行并注册可延后活动 无统一短时上限,受系统节能调度
Login Item、LaunchAgent、Daemon macOS 注册并获得用户或管理员允许 可长期运行,直到退出、注销或被禁用

总结

我现在判断一个后台功能,不再问“这个 API 能不能在后台运行”,而是问下面几件事:

  • 是应用还没有被挂起,还是系统明确重新启动了应用?
  • 是应用进程执行工作,还是系统代替应用完成工作?
  • 这是一次短事件,还是一个持续会话?
  • 系统给的是最早执行时间,还是承诺的执行时间?
  • 任务没有执行时,有没有另一个合法的兜底入口?

SleepTap 的起床日期更新,最可靠的入口不是 BGTask,而是每天必然发生的 AlarmKit Stop 或 Smart Alarm 的真实起床事件。BGAppRefreshTask 和 BGProcessingTask只负责兜底。天气刷新成功一天一次就够了,失败才安排更早重试。

这才是苹果后台任务真正的用法:不要寻找一个可以偷偷常驻的 API,而要把工作放进系统明确提供的执行机会里。