关于 axiom-background-processing
axiom-background-processing 是面向 iOS 开发者的后台任务实现与排查技能,介绍注册、调度约束、任务类型和过期处理,帮助查明任务为何未运行或未完成。
主要功能
- 01检查任务标识符和注册时机
- 02选择 iOS 后台任务类型
- 03处理过期并保存阶段性进度
- 04通过 Console 日志排查调度
- 05使用 LLDB 测试并了解模拟器限制
适用场景
- 01诊断任务始终未启动的问题
- 02为内容刷新或维护选择任务类型
- 03系统结束任务时保留未完成进度
- 04调查开发环境与生产环境的差异
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-background-processing安装后在 Claude Code 中输入/axiom-background-processing确认已加载。
详细介绍
后台处理
概述
后台执行是一种特权,而非权利。iOS 会主动限制后台工作,以保护电池续航和用户体验。核心原则:将后台任务视为酌情执行的工作——你可以申请一个时间窗口,但系统决定何时(或是否)运行你的代码。
关键要点:大多数“我的任务从未运行”问题,都源于注册错误或对影响执行的 7 个调度因素理解有误。本技能提供系统化调试方法,而非凭猜测排查。
能耗优化:如需降低后台任务对电池的影响,请参阅 axiom-energy 技能。本技能专注于任务的机制——确保任务正确运行并可靠完成。
要求:iOS 13+(BGTaskScheduler)、iOS 26+(BGContinuedProcessingTask)、Xcode 15+
示例提示
开发者常问的、本技能可以解答的问题:
1. “我的后台任务从未运行。我注册了,也安排了任务,但什么都没发生。”
→ 本技能介绍了“任务从未运行”问题的注册检查清单和调试决策树
2. “如何测试后台任务?它们似乎不会在模拟器中触发。”
→ 本技能介绍了 LLDB 调试命令和模拟器限制
3. “我的任务在完成前就被终止了。如何延长运行时间?”
→ 本技能介绍了任务类型(BGAppRefresh 为 30 秒,BGProcessing 为数分钟)、过期处理程序和增量保存进度
4. “我应该使用 BGAppRefreshTask 还是 BGProcessingTask?有什么区别?”
→ 本技能提供了决策树,根据工作时长和系统要求选择正确的任务类型
5. “如何将 Swift 6 并发与后台任务过期机制集成?”
→ 本技能介绍了用于将 BGTask 过期机制衔接到结构化并发的 withTaskCancellationHandler 模式
6. “我的后台任务在开发环境中正常运行,但在生产环境中不行。”
→ 本技能介绍了 7 个调度因素、节流行为和生产环境调试
危险信号——任务无法运行或被终止
如果看到以下任意情况,应怀疑是注册或调度问题:
- 任务从未运行:
submit()成功,但从未调用处理程序 - 任务立即终止:已调用处理程序,但工作未能完成
- 开发环境正常,生产环境不行:任务在调试器下运行,但在发行版构建中不运行
- 控制台未显示启动记录:统一日志中没有“BackgroundTask”条目
- 标识符不匹配错误:任务标识符与 Info.plist 不匹配
- “未注册处理程序”:首次安排任务之前未注册处理程序
与能耗问题的区别
- 能耗问题:任务会运行,但会耗尽电池电量(请参阅
axiom-energy技能) - 本技能:任务无法运行,或在完成工作之前被终止
必须先执行的步骤
调试代码前,务必先验证以下事项:
步骤 1:验证 Info.plist 配置(2 分钟)
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.yourapp.refresh</string>
<string>com.yourapp.processing</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
<array>
<string>fetch</string>
<string>processing</string>
</array>
常见错误:代码中的标识符与 Info.plist 中的标识符不完全一致。检查是否有拼写错误,并注意大小写。
步骤 2:验证注册时机(2 分钟)
必须在应用启动完成之前完成注册:
// ✅ CORRECT: Register in didFinishLaunchingWithOptions
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.refresh",
using: nil
) { task in
// Safe force cast: identifier guarantees BGAppRefreshTask type
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
return true // Register BEFORE returning
}
// ❌ WRONG: Registering after launch or on-demand
func someButtonTapped() {
// TOO LATE - registration won't work
BGTaskScheduler.shared.register(...)
}
例外:BGContinuedProcessingTask(iOS 26+)会在用户发起操作时动态注册。
步骤 3:检查控制台日志(5 分钟)
在 Console.app 中筛选后台任务事件:
subsystem:com.apple.backgroundtaskscheduler
查找:
- “已注册任务标识符对应的处理程序”
- “正在安排任务标识符对应的任务”
- “正在启动任务标识符对应的任务”
- “任务已完成,标识符为”
- 有关缺少处理程序或标识符的错误消息
步骤 4:确认应用未被从多任务切换器中划掉(1 分钟)
重要:如果用户从多任务切换器中强制退出应用,后台任务将无法运行。
在多任务切换器中检查:应用仍然可见吗?将应用划掉后,后台将无法执行,直到用户再次启动应用。
后台任务决策树
Need to run code in the background?
│
├─ User initiated the action explicitly (button tap)?
│ ├─ iOS 26+? → BGContinuedProcessingTask (Pattern 4)
│ └─ iOS 13-25? → beginBackgroundTask + save progress (Pattern 5)
│
├─ Keep content fresh throughout the day?
│ ├─ Runtime needed ≤ 30 seconds? → BGAppRefreshTask (Pattern 1)
│ └─ Need several minutes? → BGProcessingTask with constraints (Pattern 2)
│
├─ Deferrable maintenance work (DB cleanup, ML training)?
│ └─ BGProcessingTask with requiresExternalPower (Pattern 2)
│
├─ Large downloads/uploads?
│ └─ Background URLSession (Pattern 6)
│
├─ Triggered by server data changes?
│ └─ Silent push notification → fetch data → complete handler (Pattern 7)
│
└─ Short critical work when app backgrounds?
└─ beginBackgroundTask (Pattern 5)
任务类型对比
| 类型 | 运行时长 | 运行时机 | 用例 |
|---|---|---|---|
| BGAppRefreshTask | ~30 秒 | 根据用户使用应用的模式 | 获取最新内容 |
| BGProcessingTask | 几分钟 | 设备正在充电且处于闲置状态(通常是夜间) | 维护、机器学习训练 |
| BGContinuedProcessingTask | 较长时间 | 由系统管理,并显示进度界面 | 用户发起的导出/发布 |
| beginBackgroundTask | ~30 秒 | 应用进入后台时立即运行 | 保存状态、完成上传 |
| Background URLSession | 视需要而定 | 系统认为合适的时间,即使应用已终止也会运行 | 大型传输 |
常见模式
模式 1:BGAppRefreshTask — 保持内容最新
适用场景:需要获取新内容,让用户打开应用时看到的内容保持最新。
运行时长:~30 秒
系统运行时机:根据用户使用应用的模式预测。如果用户每天早上打开应用,系统会逐渐掌握这一规律,并在此之前刷新内容。
注册(应用启动时)
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.refresh",
using: nil
) { task in
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
return true
}
计划任务(应用进入后台时)
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.yourapp.refresh")
// earliestBeginDate = MINIMUM delay, not exact time
// System may run hours later based on usage patterns
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // At least 15 min
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Failed to schedule refresh: \(error)")
}
}
// Call when app enters background
func applicationDidEnterBackground(_ application: UIApplication) {
scheduleAppRefresh()
}
// Or with SceneDelegate / SwiftUI
.onChange(of: scenePhase) { newPhase in
if newPhase == .background {
scheduleAppRefresh()
}
}
处理程序
func handleAppRefresh(task: BGAppRefreshTask) {
// 1. IMMEDIATELY set expiration handler
task.expirationHandler = { [weak self] in
// Cancel any in-progress work
self?.currentOperation?.cancel()
}
// 2. Schedule NEXT refresh (continuous refresh pattern)
scheduleAppRefresh()
// 3. Do the work
fetchLatestContent { [weak self] result in
switch result {
case .success:
task.setTaskCompleted(success: true)
case .failure:
task.setTaskCompleted(success: false)
}
}
}
要点:
- 首先设置过期处理程序
- 在处理程序中计划下一次刷新(持续执行模式)
- 在所有代码路径中调用
setTaskCompleted(成功和失败时都要调用) - 将工作时间控制在 30 秒以内
模式 2:BGProcessingTask — 可延迟的维护工作
适用场景:可以等到系统条件最理想时再执行的维护工作(充电、WiFi、闲置状态)。
运行时长:几分钟
系统运行时机:通常在设备充电的夜间。可能不会每天运行。
注册
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.maintenance",
using: nil
) { task in
self.handleMaintenance(task: task as! BGProcessingTask)
}
设置约束并计划任务
func scheduleMaintenanceIfNeeded() {
// Be conscientious — only schedule when work is actually needed
guard needsMaintenance() else { return }
let request = BGProcessingTaskRequest(identifier: "com.yourapp.maintenance")
// CRITICAL: Set requiresExternalPower for CPU-intensive work
request.requiresExternalPower = true
// Optional: Require network for cloud sync
request.requiresNetworkConnectivity = true
// Don't set earliestBeginDate too far — max ~1 week
// If user doesn't return to app, task won't run
do {
try BGTaskScheduler.shared.submit(request)
} catch BGTaskScheduler.Error.unavailable {
print("Background processing not available")
} catch {
print("Failed to schedule: \(error)")
}
}
带进度检查点的处理程序
func handleMaintenance(task: BGProcessingTask) {
var shouldContinue = true
task.expirationHandler = { [weak self] in
shouldContinue = false
self?.saveProgress() // Save partial progress!
}
Task {
do {
// Process in chunks, checking for expiration
for chunk in workChunks {
guard shouldContinue else {
// Expiration called — stop gracefully
break
}
try await processChunk(chunk)
saveProgress() // Checkpoint after each chunk
}
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
}
要点:
- 对 CPU 密集型工作设置
requiresExternalPower = true(防止电池电量耗尽) - 增量保存进度——任务可能会被中断
- 如果用户不给设备充电,工作可能永远不会运行
earliestBeginDate不要设置为超过一周后的时间
模式 3:SwiftUI backgroundTask 修饰符
适用场景:使用现代 async/await 模式的 SwiftUI 应用。
@main
struct MyApp: App {
@Environment(\.scenePhase) var scenePhase
var body: some Scene {
WindowGroup {
ContentView()
}
.onChange(of: scenePhase) { newPhase in
if newPhase == .background {
scheduleAppRefresh()
}
}
// Handle app refresh
.backgroundTask(.appRefresh("com.yourapp.refresh")) {
// Schedule next refresh
scheduleAppRefresh()
// Async work — task completes when closure returns
await fetchLatestContent()
}
// Handle background URLSession events
.backgroundTask(.urlSession("com.yourapp.downloads")) {
// Called when background URLSession completes
await processDownloadedFiles()
}
}
}
SwiftUI 的优势:
- 闭包返回时会隐式完成任务(无需调用
setTaskCompleted) - 原生支持 Swift Concurrency
- 到期时会自动取消任务
模式 4:BGContinuedProcessingTask(iOS 26+)
适用场景:用户明确发起的工作(点击按钮),需要在应用进入后台后继续运行并显示进度。
不适用于:自动任务、维护、同步
// 1. Info.plist — use wildcard for dynamic suffix
// BGTaskSchedulerPermittedIdentifiers:
// "com.yourapp.export.*"
// 2. Register WHEN user initiates action (not at launch)
func userTappedExportButton() {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.export.photos"
) { task in
let continuedTask = task as! BGContinuedProcessingTask
self.handleExport(task: continuedTask)
}
// Submit immediately
let request = BGContinuedProcessingTaskRequest(
identifier: "com.yourapp.export.photos",
title: "Exporting Photos",
subtitle: "0 of 100 photos"
)
// Optional: Fail if can't start immediately
request.strategy = .fail // or .enqueue (default)
do {
try BGTaskScheduler.shared.submit(request)
} catch {
showError("Cannot export in background right now")
}
}
// 3. Handler with mandatory progress reporting
func handleExport(task: BGContinuedProcessingTask) {
var shouldContinue = true
task.expirationHandler = {
shouldContinue = false
}
// MANDATORY: Report progress (tasks with no progress auto-expire)
task.progress.totalUnitCount = 100
task.progress.completedUnitCount = 0
Task {
for (index, photo) in photos.enumerated() {
guard shouldContinue else { break }
await exportPhoto(photo)
// Update progress — system shows this to user
task.progress.completedUnitCount = Int64(index + 1)
}
task.setTaskCompleted(success: shouldContinue)
}
}
要点:
- 动态注册(在用户操作时注册,而不是在应用启动时注册)
- 必须报告进度——没有进度更新的任务会自动过期
- 用户可以在系统界面中查看任务并取消
- 仅当工作能立即启动时才有用,应使用
.fail策略
模式 5:beginBackgroundTask — 短时关键工作
适用场景:应用即将进入后台,需要 ~30 秒完成关键工作(保存状态、完成上传)。
var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
func applicationDidEnterBackground(_ application: UIApplication) {
// Start background task
backgroundTaskID = application.beginBackgroundTask(withName: "Save State") { [weak self] in
// Expiration handler — clean up and end task
self?.saveProgress()
if let taskID = self?.backgroundTaskID {
application.endBackgroundTask(taskID)
}
self?.backgroundTaskID = .invalid
}
// Do critical work
saveEssentialState { [weak self] in
// End task as soon as done — DON'T wait for expiration
if let taskID = self?.backgroundTaskID, taskID != .invalid {
UIApplication.shared.endBackgroundTask(taskID)
self?.backgroundTaskID = .invalid
}
}
}
要点:
- 工作完成后应立即调用
endBackgroundTask(不要只在过期处理程序中调用) - 未能结束任务可能导致系统终止应用,并影响应用今后的启动
- 最长约 30 秒,且不保证一定能获得
- 用于保存状态,而非持续运行的工作
模式 6:Background URLSession
适用场景:大型下载/上传,即使应用终止后也应继续进行。
// 1. Create background configuration
lazy var backgroundSession: URLSession = {
let config = URLSessionConfiguration.background(
withIdentifier: "com.yourapp.downloads"
)
config.sessionSendsLaunchEvents = true // App relaunched when complete
config.isDiscretionary = true // System chooses optimal time
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
// 2. Start download
func downloadFile(from url: URL) {
let task = backgroundSession.downloadTask(with: url)
task.resume()
}
// 3. Handle app relaunch for session events (AppDelegate)
func application(_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void) {
// Store completion handler — call after processing events
backgroundSessionCompletionHandler = completionHandler
// Session delegate methods will be called
}
// 4. URLSessionDelegate
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
// All events processed — call stored completion handler
DispatchQueue.main.async {
self.backgroundSessionCompletionHandler?()
self.backgroundSessionCompletionHandler = nil
}
}
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
// Move file from temp location before returning
let destinationURL = getDestinationURL(for: downloadTask)
try? FileManager.default.moveItem(at: location, to: destinationURL)
}
要点:
- 工作会交给系统守护进程(
nsurlsessiond)——即使应用终止后也会继续 - 对非紧急任务设置
isDiscretionary = true(系统会等待 WiFi 或充电时再执行) - 应用重新启动时必须处理
handleEventsForBackgroundURLSession - 立即移动已下载的文件——代理方法返回后,临时位置中的文件会被删除
模式 7:静默推送通知触发
适用场景:服务器需要唤醒应用以获取新数据。
服务器载荷
{
"aps": {
"content-available": 1
},
"custom-data": "fetch-new-messages"
}
为提高能效,请使用 apns-priority: 5(而非 10)。
App 处理程序
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
Task {
do {
let hasNewData = try await fetchLatestData()
completionHandler(hasNewData ? .newData : .noData)
} catch {
completionHandler(.failed)
}
}
}
要点:
- 静默推送会受到频率限制 — 不要指望每次推送都会启动应用
- 系统会合并多个推送(14 次推送可能只触发 7 次启动)
- 每次启动都会消耗预算,预算会在一天内逐渐补充
- 每次启动的运行时间约为 30 秒
有关静默推送通知模式(content-available 负载、节流限制、APNs 设置),请参阅 axiom-push-notifications。
Swift 6 取消集成
使用结构化并发时,将 BGTask 过期桥接到任务取消:
func handleAppRefresh(task: BGAppRefreshTask) {
// Create a Task that respects expiration
let workTask = Task {
try await withTaskCancellationHandler {
// Your async work
try await fetchAndProcessData()
task.setTaskCompleted(success: true)
} onCancel: {
// Called synchronously when task.cancel() is invoked
// Note: Runs on arbitrary thread, keep lightweight
}
}
// Bridge expiration to cancellation
task.expirationHandler = {
workTask.cancel() // Triggers onCancel block
}
}
// Checking cancellation in your work
func fetchAndProcessData() async throws {
for item in items {
// Check if we should stop
try Task.checkCancellation()
// Or non-throwing check
guard !Task.isCancelled else {
saveProgress()
return
}
try await process(item)
}
}
要点:
withTaskCancellationHandler可在任务挂起时处理取消- 如果任务已取消,
Task.checkCancellation()会抛出CancellationError - 使用
Task.isCancelled进行不抛出错误的检查 - 取消采用协作式机制 — 你的代码必须进行检查并作出响应
测试后台任务
模拟器限制
后台任务不会在模拟器中自动运行。你必须手动触发它们。
LLDB 调试命令
应用在调试器连接的情况下运行时,暂停执行并运行:
// Trigger task launch
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.yourapp.refresh"]
// Trigger task expiration (test expiration handler)
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.yourapp.refresh"]
测试工作流
- 在任务处理程序中设置断点
- 运行应用,使其进入后台
- 在调试器中暂停
- 运行
_simulateLaunchForTaskWithIdentifier命令 - 恢复执行 — 应命中断点
- 使用
_simulateExpirationForTaskWithIdentifier测试过期
测试清单
- 模拟后是否命中任务处理程序断点?
- 模拟后是否调用过期处理程序?
- 是否在所有代码路径中都调用了
setTaskCompleted? - 是否能在真机上运行(而不仅仅是在模拟器中)?
- 是否能在发布版本中运行(而不仅仅是在调试版本中)?
- 应用是否未从应用切换器中划掉?
7 项调度因素
摘自 WWDC 2020-10063「揭秘后台执行」:
| 因素 | 描述 | 影响 |
|---|---|---|
| 电量极低 | 电量 <20% | 暂停所有非必要工作 |
| 低电量模式 | 由用户启用 | 限制后台活动 |
| 应用使用情况 | 用户启动应用的频率 | 使用越频繁,优先级越高 |
| 应用切换器 | 应用是否仍然可见? | 划掉应用 = 不会在后台运行 |
| 后台应用刷新 | 系统设置 | 关闭 = 不会运行 BGAppRefresh 任务 |
| 系统预算 | 能源/数据预算 | 随启动而消耗,并在一天内补充 |
| 频率限制 | 系统设定的间隔 | 防止启动过于频繁 |
应对系统限制
// Check Low Power Mode
if ProcessInfo.processInfo.isLowPowerModeEnabled {
// Reduce background work
}
// Listen for changes
NotificationCenter.default.publisher(for: .NSProcessInfoPowerStateDidChange)
.sink { _ in
// Adapt behavior
}
// Check Background App Refresh status
let status = UIApplication.shared.backgroundRefreshStatus
switch status {
case .available:
break // Good to schedule
case .denied:
// User disabled — prompt to enable in Settings
case .restricted:
// Parental controls or MDM — can't enable
}
审核清单
注册清单
- Info.plist 中的标识符是否与代码完全一致(区分大小写)?
- 是否启用了正确的后台模式(
fetch、processing)? - 是否在
didFinishLaunchingWithOptions中、返回之前完成注册? - 是否避免多次注册同一个标识符?
- 处理程序闭包是否避免强引用 self?
调度清单
- 是否在主队列或后台队列上调度(如果对性能敏感)?
-
earliestBeginDate是否没有设置得太晚(最多约 1 周)? - 是否处理了
submit()错误? - 是否避免调度重复任务(检查
getPendingTaskRequests)?
处理程序清单
- 是否在处理程序开始时立即设置过期处理程序?
- 是否在所有代码路径中都调用了
setTaskCompleted(success:)? - 是否安排了下一个任务(用于持续执行模式)?
- 长时间运行的操作是否逐步保存进度?
- 过期处理程序是否确实取消了正在进行的工作?
生产就绪情况
- 是否在真机上测试过,而不仅仅是在模拟器中?
- 是否在发布版本中测试过,而不仅仅是在调试版本中?
- 是否在启用低电量模式的情况下测试过?
- 是否在从应用切换器中强制退出后测试过(此时不应运行)?
- 控制台日志是否显示预期的“Task completed”消息?
压力场景
场景 1:“只需在后台每隔 30 秒轮询一次服务器”
容易想到的做法:“轮询比推送通知简单。我们需要实时更新。”
实际情况:
- iOS 不会允许你每隔 30 秒在后台运行一次
- BGAppRefreshTask 根据用户行为模式运行,而不是按你的计划运行
- 如果用户很少打开应用,任务可能每天运行一次,甚至更少
- 轮询会迅速耗尽配额,导致总启动次数减少
耗时对比:
- 实现轮询:30 分钟(效果不会符合预期)
- 弄清楚为什么行不通:花 2~4 小时调试
- 实现正确的推送通知:3~4 小时
实际可行的方案:
- 静默推送通知(由服务器触发,而非轮询)
- 使用 BGAppRefreshTask 预测用户行为(并非实时)
- 使用 BGProcessingTask 执行可延迟的工作(夜间)
回应模板:“iOS 后台执行不支持轮询间隔。BGAppRefreshTask 会根据 iOS 对用户何时打开我们应用的预测来运行,而不是按固定计划运行。要实现实时更新,我们需要服务器端推送通知。我来给你看看 Apple 的相关文档。”
场景 2:“我的任务需要 5 分钟,而不是 30 秒”
容易想到的做法:“我只要使用 beginBackgroundTask,然后完成所有工作就行。”
实际情况:
- beginBackgroundTask:最长约 30 秒
- BGAppRefreshTask:约 30 秒
- BGProcessingTask:可运行数分钟,但仅在设备充电时运行
- 没有 API 能保证你获得与前台相当的 5 分钟运行时间
实际可行的方案:
- 将工作拆分成小块——拆成 30 秒的部分,并保存进度
- 使用 BGProcessingTask,并设置
requiresExternalPower = true(夜间运行) - iOS 26+:使用 BGContinuedProcessingTask 执行用户发起的工作
回应模板:“iOS 会限制后台运行时间,以保护电池。对于需要数分钟的工作,我们有两个方案:(1)BGProcessingTask 在设备充电时于夜间运行,非常适合维护工作;(2)将工作拆分成可在 30 秒内完成的小块,并在每次运行之间保存进度。哪种方案更适合我们的使用场景?”
场景 3:“在我的设备上能运行,但用户那里不行”
容易想到的做法:“代码没问题——一定是用户设备的问题。”
实际情况:连接 Xcode 运行的调试版本,与实际环境中的发布版本表现不同。
常见原因:
- 启用了低电量模式——会限制后台活动
- 关闭了后台 App 刷新——可能是用户设置或家长控制所致
- 将应用从多任务界面划掉——会终止所有后台任务
- 配额已耗尽——近期启动次数过多
- 很少使用该应用——系统会降低其优先级
调试步骤:
- 在启动时检查
backgroundRefreshStatus并记录结果 - 记录任务的计划和完成时间
- 使用 MetricKit 监控生产环境中的后台启动情况
- 询问用户:“你是否在多任务界面中强制退出了应用?”
回应模板:“后台执行取决于 7 个系统因素,包括电池电量、用户使用应用的模式,以及用户是否强制退出了应用。我来添加日志,以了解受影响用户遇到的情况。”
场景 4:“先发布,以后再添加后台任务”
容易想到的做法:“后台工作只是锦上添花。”
实际情况:
- 用户期望打开应用时内容是最新的
- 能在后台刷新的竞品使用起来会显得响应更快
- 之后再添加后台任务,需要谨慎安排注册时机
- 内容过时会给用户留下糟糕的第一印象,影响留存率
耗时对比:
- 现在添加 BGAppRefreshTask:1~2 小时
- 之后再补上并进行适当测试:4~6 小时
- 调试“为什么不起作用”的问题:还需要额外时间
最低可行的后台功能:
// In didFinishLaunchingWithOptions
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.refresh",
using: nil
) { task in
task.setTaskCompleted(success: true) // Placeholder
self.scheduleRefresh()
}
回应模板:“对于[应用类型],后台刷新是用户的基本预期。最低限度的实现只需 20 行代码。如果我们不实现就发布,以后再添加,可能会遇到注册时机方面的 bug。我来先添加基础代码,这样我们就能在发布后继续完善。”
真实案例
示例 1:任务从未运行 — 标识符不匹配
症状:submit() 执行成功,但处理程序从未被调用。
诊断:
// Code uses:
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.myapp.Refresh", // Capital R
...
)
// Info.plist has:
// "com.myapp.refresh" // lowercase r
修复:标识符必须完全匹配(区分大小写)。
浪费的时间:花了 2 小时调试代码逻辑,最后发现问题只是拼写错误。
示例 2:任务终止 — 缺少 setTaskCompleted
症状:处理程序运行,工作看似已完成,但下一个计划任务从未运行。
诊断:
func handleRefresh(task: BGAppRefreshTask) {
fetchData { result in
switch result {
case .success:
task.setTaskCompleted(success: true) // ✅ Called
case .failure:
// ❌ Missing setTaskCompleted!
print("Failed")
}
}
}
修复:在所有代码路径中调用 setTaskCompleted,包括出错时。
case .failure:
task.setTaskCompleted(success: false) // ✅ Now called
影响:未调用 setTaskCompleted 可能导致系统减少应用的后台预算。
示例 3:开发环境正常,生产环境异常 — 强制退出
症状:用户反馈后台同步无法运行。开发者无法复现。
诊断:
User: "I close my apps every night to save battery."
Developer: "How do you close them?"
User: "Swipe up in the app switcher."
实际情况:从 App 切换器中向上轻扫 = 强制退出 = 在用户再次打开应用之前,不会运行后台任务。
修复:
- 向用户说明(并不理想)
- 接受这是 iOS 的行为
- 确保应用重新打开时能提供良好的首次启动体验
示例 4:BGProcessingTask 从未运行 — 缺少电源要求
症状:已计划 BGProcessingTask,但始终未执行。
诊断:用户夜间将手机接入电源,但任务设置了 requiresExternalPower = true,而用户使用的是无线充电器。
等等,这不是问题所在。真正的问题是:
let request = BGProcessingTaskRequest(identifier: "com.app.maintenance")
// Missing: request.requiresExternalPower = true
如果不设置 requiresExternalPower,系统仍会等待充电,但确定性较低。显式设置该属性可以向系统提供明确的信号。
此外:用户必须在最近约 2 周内曾在前台启动应用,处理任务才有资格运行。
快速参考
LLDB 调试命令
// Trigger task
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"IDENTIFIER"]
// Trigger expiration
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"IDENTIFIER"]
控制台筛选条件
subsystem:com.apple.backgroundtaskscheduler
任务类型摘要
| 需求 | 使用方式 | 运行时间 |
|---|---|---|
| 保持内容最新 | BGAppRefreshTask | 约 30 秒 |
| 执行繁重维护 | BGProcessingTask + requiresExternalPower | 数分钟 |
| 继续执行用户发起的任务 | BGContinuedProcessingTask(iOS 26) | 较长 |
| 在进入后台时完成任务 | beginBackgroundTask | 约 30 秒 |
| 大型下载 | 后台 URLSession | 按需 |
| 由服务器触发 | 静默推送通知 | 约 30 秒 |
资源
WWDC:2019-707、2020-10063、2022-10142、2023-10170、2025-227
文档:/backgroundtasks/bgtaskscheduler、/backgroundtasks/starting-and-terminating-tasks-during-development
技能:axiom-background-processing-ref、axiom-background-processing-diag、axiom-energy、axiom-push-notifications
最后更新:2025-12-31 平台:iOS 13+、iOS 26+(BGContinuedProcessingTask) 状态:可用于生产环境的后台任务模式