
-
完成后台任务
探索后台任务执行方面的最新进展,并了解系统如何进行运行时调度。我们将讨论如何充分利用后台运行时,让你的 App 既能在后台提供功能,又能保持出色的前台体验。我们还将介绍各种 API 如何为你的 App 提供后台运行时环境,以及每个 API 是怎样针对不同用例量身定制的 — 其中包括 iOS 和 iPadOS 26 中的新 API,这些 API 让你的 App 能够在从前台过渡到后台时顺利完成任务。
章节
资源
-
搜索此视频…
大家好 我叫 Ryan 是 Apple 的软件工程师 今天 我想聊聊 App 如何利用后台运行时
App Store 汇聚了数以百万计的 App 每一款都有自己的独特之处 当你的 App 位于前台时 它往往能展现出最耀眼的一面 你可以完全掌控每一个像素 打造令用户难忘的体验
用户也期待开发者能带来这样的 App 快速、实用且精心设计 但是当 App 切换到后台时 会发生什么呢?
在这个讲座中 我将探讨后台运行时 以及如何为 App 构建一个 高效且表现良好的工具包 这不仅是为了保持 App 的正常运行 更重要的是发挥作用 比如进行数据预取、 同步和上传 为 App 的下次启动做好准备 合理运用后台运行时 能让你的 App 显得快速、 流畅 甚至带点魔力
我会先简要概述 前台和后台的实际定义 并分享一些关于系统如何决定 App 在后台运行的时机和频率的原则
然后 我将介绍多种现有的实现方法 包括一个用于延续 前台已启动任务的新 API
每一个处于前台的 App 都遵循相同的节奏
App 本身和它需要的一切 包括框架、资产等 都会载入到内存中 在这种状态下 App 的界面 是设备的焦点 这时 App 处于前台状态
当用户离开 App 但进程仍在运行 App 就进入了后台 默认情况下 后台 App 处于暂停状态 不会获得 CPU 时间 这既能维持电池续航、保护隐私 又能为前台 App 释放资源
在某些情况下 App 可以请求后台时间 以便在暂停前完成正在进行的工作 当用户通过 App 切换器 返回你的 App 时 系统会将它恢复到前台状态
在使用后台运行时之前 你应该了解系统如何优先分配 和管理资源 以及你可以在 App 中做些什么 来打造最佳体验
系统的主要目标很明确: 维持电池续航、优化性能 同时保持流畅且响应迅速的用户体验 这意味着后台执行不是必然保证的 而是伺机的 通常由系统酌情决定 并且受到严格管理 成功的工作负载需要理解这一背景 并与系统协同工作 而不是与系统对抗
最根本的限制是能耗
每一项操作 无论是 CPU 周期、 GPU 渲染、网络请求 甚至是神经网络引擎的使用 都会消耗电池电量 电池续航是一种有限的资源 为了维持电池续航 系统会在设备唤醒时 合并任务 从而减少 不必要的后台活动 这项工作全天持续进行 由于后台运行时间有限 请将 App 在后台执行的任务 视为独立的、 定制化的任务 每个任务高效地完成一件事 并始终关注系统的优先级和约束 这些后台工作 反映在“电池设置”中 用户可以从中了解哪些 App 对电池续航时间影响较大 iOS 26 为设备的电池性能提供了 更深入的洞察 会按 App 给出详细的耗电分析
你最明智的做法是提高效率 如果一个任务不需要立即运行 可以考虑推迟到设备充电时再执行 如果确实需要运行 也要保持轻量级且目标明确
除了电池 系统还负责 管理其他共享资源 比如内存、CPU 时间 和网络带宽 当用户使用设备时 前台 App 会被优先考虑 如果后台 App 消耗过多内存或 CPU 它不仅效率低下 还会与前台体验产生竞争 这时系统可能会介入 限制、暂停、 甚至终止资源消耗过大的进程
核心要点很简单 尽量减少后台工作量 避免臃肿的工作 优先使用批处理 以最大限度地减少内存占用
即使工作负载表现良好 也不能保证 它们会被允许长时间运行 后台工作队列永远不会为空 因此系统可能会选择 优先处理其他工作负载 话虽如此 我们都在协同工作 系统是在尽其所能地 优化前台体验
你的工作负载必须具有弹性 尽早且频繁地保存增量进度 及时响应到期信号 并相信系统很快就会 恢复你的工作负载 系统欣赏合作型的进程 并会利用这些行为来影响未来的调度 但归根结底 使用设备的人拥有最终决定权 用户通过切换设置来影响调度 例如切换到低功耗模式、 后台 App 刷新 和低数据模式等 系统提供了透明度 让用户能够自主做出这些决策 例如 如果你的 App 在后台 消耗过多电量 用户可能会采取行动 而这未必对你有利
因此 后台工作应该适度、 保持轻量级并尊重用户偏好 同时确保你的影响 与你提供的价值成正比 即使系统中的每个进程 都完全遵循这些原则 它们仍然运行在一个复杂 且高度动态的环境中
网络可用性、CPU 负载、设备活动、 散热状态和电池电量等因素 都会作为参考信息 影响调度决策
幸运的是 系统会考虑 这些具有挑战性的条件 始终打造最佳体验 这意味着 即使是精心设计的任务 如果条件不合适 也可能会被推迟 以实现全局优化 保持适应性至关重要 保持工作的原子化和轻量级 同时清晰地表明你的需求 将工作负载设计成能够 从中断处继续执行 以便在运行时机会出现时逐步推进 你对系统条件和优先级的 了解和适应程度越高 你的工作负载就会越成功
高效、精简、弹性、 适度和适应性强 这些是构建与平台无缝契合的 后台工作的关键要点 在开发过程中 你可以问自己几个关键问题
谁发起了这个任务? 这项工作是明确启动的 还是可酌情决定 在稍后运行的?
这项工作需要多长时间? 尝试将任务分为 短、中、长三种持续时间 这项工作对 App 状态和新鲜度 是否至关重要? 后台下载可以增强 App 的活跃度 但遥测上传 对设备用户没有直接好处 最后 这项工作 是否需要用户同意或输入?
后台运行时不适用于 这些类型的工作负载 因此如果是这种情况 我建议采取其他方法 有了这个基础 让我们来探索如何有效地 设计任务 iOS 提供了许多不同的 API 可让你请求后台运行时 并且每个 API 都具有 不同的类型或描述文件 具体取决于工作的支持 这使系统能够根据 我之前提到的约束和条件 调整 App 的运行时 例如 用户期望自己最常用的 App 始终拥有最新、最棒的内容 系统理应了解 App 使用模式 从而优化后台任务和支持
第一个 API BGAppRefreshTask 就是这种情况
利用这个 API App 可以在正准备使用内容前 从服务器静默获取内容 同时完全遵循适度原则 系统会将这些任务 与 App 使用历史记录相匹配 常用 App 被调度的几率更高 从而保证每次启动时都获得新鲜内容
要在 SwiftUI 中创建 App 刷新任务 请将 BackgroundTask 修饰符 添加到场景中 当系统在后台唤醒 App 时 它将调用这个闭包 并在闭包返回时 暂停 App
虽然刷新任务涵盖了 基于获取的用例 你可能还需要维护 不频繁或不定期更新的远程文档 后台推送通知 提供了一个巧妙的解决方案 当你的服务器发送 有关新内容的通知时 系统会在适当的时间唤醒你的 App 以获取新内容 请注意 这与 App 刷新的情况不同 在这里 更新被推送到设备 而不是设备适时主动获取数据
由于后台推送通知 用于指示新的远程内容 它们始终被视为是酌情进行的
它们还会以低优先级发送并合并 以最大限度地降低开销和功耗
当用户从 App 切换器中 移除你的 App 时 系统会尊重用户的意图 直到 App 再次启动 通知才会传递给 App
不过 有时你可能希望 App 执行其他类型的工作 也许你想在生成的数据上 运行机器学习模型 或者只是处理数据库维护
BGProcessingTask API 可以帮助你实现这些任务
注册这个任务非常简单 只需要任务标识符、回调队列 和一个在运行时调用的闭包
你必须在启动期间 立即注册 BackgroundTasks 这样 如果任务在后台启动 系统就会及时知晓 并立即调用它的处理程序 处理任务还支持其他配置 遵循我之前介绍的原则 例如 如果工作对延迟不是特别敏感 一项好的任务可能会选择仅在 设备充电并连接到网络时运行 这可以最大限度地减少对电池的影响 从而降低 App 在电池设置中显示的耗电量 到目前为止 我已经介绍了一些 API 它们可以让 App 为在后台启动的任务获取运行时 但有时你可能只是想 在切换到后台时多一点时间继续运行 beginBackgroundTask API 和 endBackgroundTask API 允许 App 完成那些 如果被中断而未完成 继而可能无法恢复的工作
以一个简单的状态保存为例 根据工作的不同 提前终止 可能会导致糟糕的体验
将代码包装在这些 API 调用中 会通知系统 App 正在处理 不应中断的关键工作 这个 API 非常适合清理文件句柄 或关闭数据库连接等场景
iOS 上的后台支持范围很广 旨在处理各种各样的任务 包括用户启动的任务 确保这些用户发起的操作可靠地完成 对于良好的体验至关重要 在 iPadOS 和 iOS 26 中 BGContinuedProcessingTask 为 App 提供了一种支持 这些功能的方法 这个任务允许你在 App 进入后台后 仍继续工作 系统会提供 UI 来传达进度 以“手记”App 为例 它使用持续处理任务 在后台驱动导出 并向发起者显示进度更新 任务完成后 系统会短暂更新 然后自动关闭 UI 用户始终具有控制权 他们可以查看任务进度 并随时取消工作 因此 这可以实现复杂的功能
持续处理任务总是始于 用户在 App 中执行的明确操作 例如轻点按钮或手势 每个任务都代表 用户希望 App 执行的 一个明确且即时的目标 例如导出文件、发布社交媒体内容 或完成已连接配件的更新 这些任务会取得可衡量的进展 并且用户很容易理解 任务完成的意义
用户不期望任务自动启动 即使他们之前在 App 中设置过偏好 避免自动执行工作负载 比如维护、备份 或照片同步 如果任务在没有明确操作 的情况下启动 用户可能无法理解任务的目标 或任务进度的真正含义 执行这种意外的工作可能会 导致 App 的任务被取消 对于这些需求 可以考虑其他更合适的 API
有了这些考虑 采用持续处理任务就变得轻而易举 首先 在 Info.plist 中添加 任务标识符 你将使用它来注册一个启动处理程序 用于管理状态和报告进度 然后 只需在出现提示时 提交任务请求即可 让我们从标识符开始 要定义标识符 在 Info.plist 中的 允许的后台任务计划程序标识符 数组中添加一个新值 并确保它的前缀是 App 的套装 ID
除了静态标识符 持续处理任务 还支持带有动态后缀的 新通配符表示法
通配符标识符始终以套装 ID 开头 后跟一些语义上下文 这里新的组成部分是点星号 表示在注册和提交时 将在标识符后 附加一个动态后缀
用于注册和提交的完整组合标识符 具有这样的形式
现在 让我们先使用更静态的标识符
选择标识符后 调度程序需要知道 当持续处理任务被要求运行时 要执行什么代码 和之前一样 提交任务请求后 会调用提供给调度程序的闭包 但这里有一个关键的转变 启动处理程序不需要 在 App 完成启动前注册 现在 你可以在表达使用意图时 动态注册这些处理程序
就像在“手记”App 中一样 必须及时更新工作负载的进度 如果进度比预期慢 系统将提示发起者 询问是否希望工作继续进行 系统依靠这些进度更新 来管理正在进行的工作 因此 不报告任何进度的任务 将会过期 以便系统回收 并重新分配资源 App 会使用进度报告协议 主动传达这些更新 与此同时 系统会持续监控更新 并在 UI 中显示进度 现在我们仍然需要注意 如果条件发生变化 系统可能需要 提前停止任务 为了处理这种情况 任务必须提供一个到期处理程序 当任务需要停止时 它将被调用 可以将这个处理程序视为 快速翻转变量的机会 以便任务能够优雅地停止 并避免做额外的工作
至关重要的是 当任务完成它的工作时 你必须调用 setTaskCompleted 这会告诉系统你已经完成 好的 我已经介绍了 如何管理进度更新、 处理潜在的中断 以及发出运行中任务的完成信号 现在 让我们通过构建和提交 一个有效的、用户发起的任务请求 来整合这些内容
首先初始化一个任务请求对象 这需要三部分信息 与 Info.plist 中的标识符 匹配的标识符、 本地化的标题 和本地化的副标题 这些是用户在系统 UI 中看到的内容
接下来 你需要提供提交策略 供系统遵守 默认情况下 如果系统 发现自己无法立即 运行持续处理任务 它会将这个任务添加到队列的末尾 你不需要指定任何额外内容 然而 有时排队并不是正确的方法 如果任务只有立即启动才有意义呢? 与其让它排队 你可以告诉系统 如果不能立即启动 就让提交失败 这会给 App 提供即时反馈 让你能够妥善处理这种情况
一旦你决定了策略并配置了请求 只需提交给调度程序 让系统管理工作负载 这就是使用持续处理任务的核心流程 通过提供标题和副标题 并考虑提交策略 你可以与系统顺畅集成 同时遵守关键的同意原则 这个 API 提供的功能 比表面上看到的更多 在 iPadOS 和 iOS 26 中 持续处理任务还可以受益于 受支持设备上的后台 GPU 访问
要利用这一点 请确保在 Xcode 项目设置中 添加后台 GPU 功能
添加后 你可以并且应该 动态查询调度程序的 supportedResources 属性 这样 App 就可以准确地了解 当前设备在运行时支持什么资源 以便你可以相应地调整任务需求 这项检查非常重要 系统会强制执行这些要求 任何对不可用资源的请求 在提交时都会被拒绝 以保持系统 和任务处于健康、已知的状态
最后 请记住系统优先级 的更广泛背景 iOS 优先考虑前台体验 这意味着后台任务获得的 服务质量可能会低于 App 处于活动状态时的服务质量 但是 系统会智能地处理这种情况 当 App 返回前台时 它会智能地提高任务优先级 以确保运行顺畅且响应迅速 这些新 API 实现了迄今为止 最强大、最流畅的后台 执行功能 将重要的 BGContinuedProcessingTask 添加到你的工具包中 在全面了解这些任务 以及它们如何融入更广泛的系统后 你就可以开始采用它们了 我们期待看到 大家使用这些工具 来打造更智能、更高效的后台体验 感谢观看!
-
-
8:27 - Register an app refresh task
import BackgroundTasks import SwiftUI @main struct ColorFeed: App { var body: some Scene { WindowGroup { // ... } .backgroundTask(.appRefresh("com.colorfeed.wwdc25.appRefresh")) { await self.handleAppRefreshTask() } } }
-
9:45 - Register a processing task
import BackgroundTasks import UIKit class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { BGTaskScheduler.shared.register( forTaskWithIdentifier: "com.example.apple-samplecode.ColorFeed.db_cleaning", using: nil ) { task in self.handleAppRefresh(task: task as! BGProcessingTask) } } func submitProcessingTaskRequest() { let request = BGProcessingTaskRequest( identifier: "com.example.apple-samplecode.ColorFeed.db_cleaning" ) request.requiresNetworkConnectivity = true request.requiresExternalPower = true BGTaskScheduler.shared.submit(request)! } }
-
10:51 - Begin and end background task
import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid func saveState() { /* ... */ } func handlePersistence() { let app = UIApplication.shared guard backgroundTaskID != .invalid else { return } backgroundTaskID = app.beginBackgroundTask(withName: "Finish Export") { app.endBackgroundTask(self.backgroundTaskID) self.backgroundTaskID = .invalid } self.saveState() app.endBackgroundTask(backgroundTaskID) backgroundTaskID = .invalid } }
-
14:00 - Continued processing task registration
import BackgroundTasks func handleDialogConfirmation() { BGTaskScheduler.shared.register("com.colorfeed.wwdc25.userTask") { task in let task = task as! BGContinuedProcessingTask var shouldContinue = true task.expirationHandler = { shouldContinue = false } task.progress.totalUnitCount = 100 task.progress.completedUnitCount = 0 while shouldContinue { // Do some work task.progress.completedUnitCount += 1 } task.setTaskCompleted(success: true) } }
-
15:47 - Continued processing task submission
import BackgroundTasks func submitContinuedProcessingTaskRequest() { let request = BGContinuedProcessingTaskRequest( identifier: "com.colorfeed.wwdc25.userTask", title: "A succinct title", subtitle: "A useful and informative subtitle" ) request.strategy = .fail BGTaskScheduler.shared.submit(request)! }
-
-
- 0:00 - 开场介绍
了解如何使用后台运行时在 App 中执行数据预取和同步等任务。
- 1:22 - 简介
App 会载入到内存中,然后移到前台,成为设备的主要焦点。当用户离开某个 App 时,这个 App 会进入后台,系统会将它暂停以节省电池电量和资源。App 可以请求一小段时间来完成任务。用户回到这个 App 后,它就会回到前台。
- 2:09 - 行为与限制
在 iOS 和 iPadOS 中,后台运行时受到严格管理,以优先延长电池续航并优化性能。后台执行会“伺机而动”,由系统酌情决定,其中耗电是最根本的制约因素。应将 App 的后台任务设计为高效、轻量级且目标明确。系统会合并任务,并可能限制、暂停甚至终止消耗过多资源的进程。 尽量减少后台工作,将非必要的任务推迟到充电时再处理,并确保任务精简、独立且具有弹性。App 占用的资源应适度,应尊重用户偏好和系统状况。系统会给用户提供透明度,使他们能够影响调度决策。后台工作负载成功与否,取决于这些工作负载的适应性如何,以及是否与系统优先级一致。
- 7:29 - 后台任务 API
iOS 和 iPadOS 提供了多个 API,用于设计在后台运行的任务。系统会根据 App 使用模式和设备限制来优化这些任务。 “BGAppRefreshTask”允许 App 在使用前静默获取内容,常用 App 的调度会更频繁。 当服务器发送通知时,“后台推送通知”会唤醒 App 以获取新内容,但这些任务由系统酌情执行,优先级低,以充分减少开销。 “BGProcessingTask”使 App 能够执行更复杂的工作,例如运行机器学习模型或数据库维护。这个任务需要在启动期间注册,并且可以将它配置为仅在设备充电并连接到网络时才运行。 beginBackgroundTask API 和 endBackgroundTask API 为 App 提供额外的时间来完成关键工作,例如在转换到后台时保存状态或断开连接。
- 11:23 - 持续处理任务
iPadOS 和 iOS 26 中新增了“BGContinuedProcessingTask”API,它让 App 即使在后台运行,也能完成用户从前台开始的任务。这个功能使 App 无需打开即可完成复杂的任务,从而增强了用户体验。 例如,“手记”App 就使用这个 API 在后台导出文件。用户轻点按钮即可操作导出,然后 App 会通过系统 UI 提供进度更新。用户可以随时监控进度并取消任务,保持完全的控制权。 这些任务总是从一项明确的操作开始,例如轻点按钮或手势。它们代表了明确而紧迫的目标,例如导出文件、发布社交媒体内容或更新连接的配件。系统期望这些任务取得可衡量的进展。 你需要注册启动处理程序来管理状态和进度报告。使用进度报告协议及时提供有关工作负载进度的更新。如果任务进度慢于预期,系统会提示用户决定是否继续。 任务完成后,需要使用“setTaskCompleted”方法通知系统。此外,App 必须提供到期处理程序来从容应对可能发生的中断。 此外,这个 API 还支持在受支持的设备上进行后台图形处理器访问,让后台体验更强大、更高效。但请记住,iOS 会优先保障前台体验,后台任务的服务质量可能较低。尽管如此,当 App 返回前台时,系统会智能地提高任务优先级。