axiom-background-processing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseBackground Processing
后台处理
Overview
概述
Background execution is a privilege, not a right. iOS actively limits background work to protect battery life and user experience. Core principle: Treat background tasks as discretionary jobs — you request a time window, the system decides when (or if) to run your code.
Key insight: Most "my task never runs" issues stem from registration mistakes or misunderstanding the 7 scheduling factors that govern execution. This skill provides systematic debugging, not guesswork.
Energy optimization: For reducing battery impact of background tasks, see skill. This skill focuses on task mechanics — making tasks run correctly and complete reliably.
axiom-energyRequirements: iOS 13+ (BGTaskScheduler), iOS 26+ (BGContinuedProcessingTask), Xcode 15+
后台执行是一项特权,而非固有权利。iOS会主动限制后台工作以保护电池寿命和用户体验。核心原则:将后台任务视为可自由调度的作业——你请求一个时间窗口,由系统决定何时(或是否)运行你的代码。
关键要点:大多数“我的任务从未运行”问题源于注册错误,或是对控制执行的7个调度因素理解不足。本指南提供系统化的调试方案,而非仅凭猜测排查。
能耗优化:若需减少后台任务的电池消耗,请参考技能。本技能聚焦任务的机制层面——确保任务能正确运行并可靠完成。
axiom-energy要求:iOS 13+(BGTaskScheduler)、iOS 26+(BGContinuedProcessingTask)、Xcode 15+
Example Prompts
示例提问
Real questions developers ask that this skill answers:
开发者常问的、本技能可解答的问题:
1. "My background task never runs. I register it, schedule it, but nothing happens."
1. "我的后台任务从未运行。我完成了注册和调度,但什么都没发生。"
→ The skill covers the registration checklist and debugging decision tree for "task never runs" issues
→ 本技能涵盖了“任务无法运行”问题的注册检查清单和调试决策树
2. "How do I test background tasks? They don't seem to trigger in the simulator."
2. "如何测试后台任务?它们在模拟器里似乎不会触发。"
→ The skill covers LLDB debugging commands and simulator limitations
→ 本技能涵盖了LLDB调试命令和模拟器的局限性
3. "My task gets terminated before it completes. How do I extend the time?"
3. "我的任务在完成前就被终止了。如何延长运行时间?"
→ The skill covers task types (BGAppRefresh 30s vs BGProcessing minutes), expiration handlers, and incremental progress saving
→ 本技能涵盖了任务类型(BGAppRefreshTask的30秒 vs BGProcessingTask的数分钟)、过期处理程序以及增量进度保存方案
4. "Should I use BGAppRefreshTask or BGProcessingTask? What's the difference?"
4. "我应该用BGAppRefreshTask还是BGProcessingTask?两者有什么区别?"
→ The skill provides decision tree for choosing the correct task type based on work duration and system requirements
→ 本技能提供了根据工作时长和系统要求选择正确任务类型的决策树
5. "How do I integrate Swift 6 concurrency with background task expiration?"
5. "如何将Swift 6并发与后台任务过期机制集成?"
→ The skill covers withTaskCancellationHandler patterns for bridging BGTask expiration to structured concurrency
→ 本技能涵盖了使用withTaskCancellationHandler模式将BGTask过期与结构化并发桥接的方法
6. "My background task works in development but not in production."
6. "我的后台任务在开发环境正常运行,但生产环境不行。"
→ The skill covers the 7 scheduling factors, throttling behavior, and production debugging
→ 本技能涵盖了7个调度因素、节流行为以及生产环境调试方法
Red Flags — Task Won't Run or Terminates
危险信号——任务无法运行或被终止
If you see ANY of these, suspect registration or scheduling issues:
- Task never runs: Handler never called despite successful
submit() - Task terminates immediately: Handler called but work doesn't complete
- Works in dev, not prod: Task runs with debugger but not in release builds
- Console shows no launch: No "BackgroundTask" entries in unified logging
- Identifier mismatch errors: Task identifier not matching Info.plist
- "No handler registered": Handler not registered before first scheduling
如果出现以下任意一种情况,请排查注册或调度问题:
- 任务从未运行:尽管成功,但处理程序从未被调用
submit() - 任务立即终止:处理程序被调用,但工作未完成
- 开发环境正常,生产环境异常:任务在调试模式下运行,但在发布版本中不运行
- 控制台无启动日志:统一日志中没有“BackgroundTask”相关条目
- 标识符不匹配错误:任务标识符与Info.plist中的不匹配
- “未注册处理程序”:首次调度前未注册处理程序
Difference from energy issues
与能耗问题的区别
- Energy issue: Task runs but drains battery (see skill)
axiom-energy - This skill: Task doesn't run, or terminates before completing work
- 能耗问题:任务运行但消耗大量电池(请参考技能)
axiom-energy - 本技能解决的问题:任务无法运行,或在完成工作前被终止
Mandatory First Steps
必须先执行的步骤
ALWAYS verify these before debugging code:
在调试代码前,请务必验证以下内容:
Step 1: Verify Info.plist Configuration (2 minutes)
步骤1:验证Info.plist配置(2分钟)
xml
<!-- Required in Info.plist -->
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.yourapp.refresh</string>
<string>com.yourapp.processing</string>
</array>
<!-- For BGAppRefreshTask -->
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
<!-- For BGProcessingTask (add to UIBackgroundModes) -->
<array>
<string>fetch</string>
<string>processing</string>
</array>Common mistake: Identifier in code doesn't EXACTLY match Info.plist. Check for typos, case sensitivity.
xml
<!-- Info.plist中必填 -->
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.yourapp.refresh</string>
<string>com.yourapp.processing</string>
</array>
<!-- 针对BGAppRefreshTask -->
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
<!-- 针对BGProcessingTask(添加到UIBackgroundModes) -->
<array>
<string>fetch</string>
<string>processing</string>
</array>常见错误:代码中的标识符与Info.plist中的不完全匹配。请检查拼写错误和大小写敏感性。
Step 2: Verify Registration Timing (2 minutes)
步骤2:验证注册时机(2分钟)
Registration MUST happen before app finishes launching:
swift
// ✅ 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(...)
}Exception: BGContinuedProcessingTask (iOS 26+) uses dynamic registration when user initiates the action.
注册必须在应用启动完成前进行:
swift
// ✅ 正确:在didFinishLaunchingWithOptions中注册
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.refresh",
using: nil
) { task in
// 安全强制转换:标识符确保是BGAppRefreshTask类型
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
return true // 返回前完成注册
}
// ❌ 错误:在启动后或按需注册
func someButtonTapped() {
// 为时已晚——注册不会生效
BGTaskScheduler.shared.register(...)
}例外情况:BGContinuedProcessingTask(iOS 26+)在用户触发操作时使用动态注册。
Step 3: Check Console Logs (5 minutes)
步骤3:检查控制台日志(5分钟)
Filter Console.app for background task events:
subsystem:com.apple.backgroundtaskschedulerLook for:
- "Registered handler for task with identifier"
- "Scheduling task with identifier"
- "Starting task with identifier"
- "Task completed with identifier"
- Error messages about missing handlers or identifiers
在Console.app中过滤后台任务事件:
subsystem:com.apple.backgroundtaskscheduler查找以下内容:
- "Registered handler for task with identifier"
- "Scheduling task with identifier"
- "Starting task with identifier"
- "Task completed with identifier"
- 关于缺少处理程序或标识符的错误消息
Step 4: Verify App Not Swiped Away (1 minute)
步骤4:验证应用未被划走(1分钟)
Critical: If user force-quits app from App Switcher, NO background tasks will run.
Check in App Switcher: Is your app still visible? Swiping away = no background execution until user launches again.
关键:如果用户从应用切换器中强制退出应用,不会有任何后台任务运行。
在应用切换器中检查:你的应用是否仍可见?划走应用后,直到用户再次启动应用前,不会有后台执行。
Background Task Decision Tree
后台任务决策树
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)需要在后台运行代码?
│
├─ 用户明确触发了操作(如点击按钮)?
│ ├─ iOS 26+? → BGContinuedProcessingTask(模式4)
│ └─ iOS 13-25? → beginBackgroundTask + 保存进度(模式5)
│
├─ 需要全天保持内容新鲜?
│ ├─ 所需运行时间 ≤ 30秒? → BGAppRefreshTask(模式1)
│ └─ 需要数分钟? → 带约束的BGProcessingTask(模式2)
│
├─ 可延迟的维护工作(数据库清理、ML训练)?
│ └─ 带requiresExternalPower的BGProcessingTask(模式2)
│
├─ 大型下载/上传?
│ └─ Background URLSession(模式6)
│
├─ 由服务器数据变更触发?
│ └─ 静默推送通知 → 获取数据 → 完成处理程序(模式7)
│
└─ 应用退到后台时的短时间关键工作?
└─ beginBackgroundTask(模式5)Task Type Comparison
任务类型对比
| Type | Runtime | When Runs | Use Case |
|---|---|---|---|
| BGAppRefreshTask | ~30 seconds | Based on user app usage patterns | Fetch latest content |
| BGProcessingTask | Several minutes | Device charging, idle (typically overnight) | Maintenance, ML training |
| BGContinuedProcessingTask | Extended | System-managed with progress UI | User-initiated export/publish |
| beginBackgroundTask | ~30 seconds | Immediately when backgrounding | Save state, finish upload |
| Background URLSession | As needed | System-friendly time, even after termination | Large transfers |
| 类型 | 运行时长 | 运行时机 | 使用场景 |
|---|---|---|---|
| BGAppRefreshTask | ~30秒 | 基于用户应用使用模式预测 | 获取最新内容 |
| BGProcessingTask | 数分钟 | 设备充电、闲置时(通常在夜间) | 维护工作、ML训练 |
| BGContinuedProcessingTask | 延长时长 | 系统管理并显示进度UI | 用户触发的导出/发布操作 |
| beginBackgroundTask | ~30秒 | 应用退到后台时立即运行 | 保存状态、完成上传 |
| Background URLSession | 按需 | 系统选择的合适时间,即使应用终止后也能运行 | 大型传输任务 |
Common Patterns
常见模式
Pattern 1: BGAppRefreshTask — Keep Content Fresh
模式1:BGAppRefreshTask — 保持内容新鲜
Use when: You need to fetch new content so app feels fresh when user opens it.
Runtime: ~30 seconds
When system runs it: Predicted based on user's app usage patterns. If user opens app every morning, system learns and refreshes before then.
适用场景:你需要获取新内容,让用户打开应用时感觉内容是最新的。
运行时长:~30秒
系统运行时机:基于用户的应用使用模式预测。如果用户每天早上打开应用,系统会学习并在那之前刷新内容。
Registration (at app launch)
注册(应用启动时)
swift
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
}swift
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
}Scheduling (when app backgrounds)
调度(应用退到后台时)
swift
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()
}
}swift
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.yourapp.refresh")
// earliestBeginDate = 最小延迟,而非精确时间
// 系统可能根据使用模式延迟数小时运行
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 至少15分钟
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Failed to schedule refresh: \(error)")
}
}
// 应用进入后台时调用
func applicationDidEnterBackground(_ application: UIApplication) {
scheduleAppRefresh()
}
// 或使用SceneDelegate / SwiftUI
.onChange(of: scenePhase) { newPhase in
if newPhase == .background {
scheduleAppRefresh()
}
}Handler
处理程序
swift
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)
}
}
}Key points:
- Set expiration handler FIRST
- Schedule next refresh inside handler (continuous pattern)
- Call in ALL code paths (success AND failure)
setTaskCompleted - Keep work under 30 seconds
swift
func handleAppRefresh(task: BGAppRefreshTask) {
// 1. 立即设置过期处理程序
task.expirationHandler = { [weak self] in
// 取消所有进行中的工作
self?.currentOperation?.cancel()
}
// 2. 调度下一次刷新(持续刷新模式)
scheduleAppRefresh()
// 3. 执行工作
fetchLatestContent { [weak self] result in
switch result {
case .success:
task.setTaskCompleted(success: true)
case .failure:
task.setTaskCompleted(success: false)
}
}
}关键点:
- 首先设置过期处理程序
- 在处理程序内部调度下一次刷新(持续模式)
- 在所有代码路径中调用(成功和失败场景)
setTaskCompleted - 工作时长控制在30秒内
Pattern 2: BGProcessingTask — Deferrable Maintenance
模式2:BGProcessingTask — 可延迟的维护工作
Use when: Maintenance work that can wait for optimal system conditions (charging, WiFi, idle).
Runtime: Several minutes
When system runs it: Typically overnight when device is charging. May not run daily.
适用场景:可等待系统最佳条件(充电、WiFi、闲置)的维护工作。
运行时长:数分钟
系统运行时机:通常在夜间设备充电时。可能不会每天运行。
Registration
注册
swift
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.maintenance",
using: nil
) { task in
self.handleMaintenance(task: task as! BGProcessingTask)
}swift
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.maintenance",
using: nil
) { task in
self.handleMaintenance(task: task as! BGProcessingTask)
}Scheduling with Constraints
带约束的调度
swift
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)")
}
}swift
func scheduleMaintenanceIfNeeded() {
// 谨慎操作——仅在确实需要工作时才调度
guard needsMaintenance() else { return }
let request = BGProcessingTaskRequest(identifier: "com.yourapp.maintenance")
// 关键:CPU密集型工作需设置requiresExternalPower
request.requiresExternalPower = true
// 可选:云同步时需网络
request.requiresNetworkConnectivity = true
// earliestBeginDate不要设置得太远——最多约1周
// 如果用户不返回应用,任务不会运行
do {
try BGTaskScheduler.shared.submit(request)
} catch BGTaskScheduler.Error.unavailable {
print("Background processing not available")
} catch {
print("Failed to schedule: \(error)")
}
}Handler with Progress Checkpointing
带进度检查点的处理程序
swift
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)
}
}
}Key points:
- Set for CPU-intensive work (prevents battery drain)
requiresExternalPower = true - Save progress incrementally — task may be interrupted
- Work may never run if user doesn't charge device
- Don't set more than a week ahead
earliestBeginDate
swift
func handleMaintenance(task: BGProcessingTask) {
var shouldContinue = true
task.expirationHandler = { [weak self] in
shouldContinue = false
self?.saveProgress() // 保存部分进度!
}
Task {
do {
// 分块处理,检查是否过期
for chunk in workChunks {
guard shouldContinue else {
// 触发过期——优雅停止
break
}
try await processChunk(chunk)
saveProgress() // 每块完成后保存进度
}
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
}关键点:
- CPU密集型工作需设置(防止电池消耗)
requiresExternalPower = true - 增量保存进度——任务可能被中断
- 如果用户不充电,任务可能永远不会运行
- 不要设置超过1周
earliestBeginDate
Pattern 3: SwiftUI backgroundTask Modifier
模式3:SwiftUI backgroundTask 修饰符
Use when: SwiftUI app using modern async/await patterns.
swift
@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 advantages:
- Implicit task completion when closure returns (no needed)
setTaskCompleted - Native Swift Concurrency support
- Task automatically cancelled on expiration
适用场景:使用现代async/await模式的SwiftUI应用。
swift
@main
struct MyApp: App {
@Environment(\.scenePhase) var scenePhase
var body: some Scene {
WindowGroup {
ContentView()
}
.onChange(of: scenePhase) { newPhase in
if newPhase == .background {
scheduleAppRefresh()
}
}
// 处理应用刷新
.backgroundTask(.appRefresh("com.yourapp.refresh")) {
// 调度下一次刷新
scheduleAppRefresh()
// 异步工作——闭包返回时任务完成
await fetchLatestContent()
}
// 处理Background URLSession事件
.backgroundTask(.urlSession("com.yourapp.downloads")) {
// Background URLSession完成时调用
await processDownloadedFiles()
}
}
}SwiftUI优势:
- 闭包返回时自动完成任务(无需)
setTaskCompleted - 原生支持Swift并发
- 任务过期时自动取消
Pattern 4: BGContinuedProcessingTask (iOS 26+)
模式4:BGContinuedProcessingTask(iOS 26+)
Use when: User explicitly initiates work (button tap) that should continue after backgrounding, with visible progress.
NOT for: Automatic tasks, maintenance, syncing
swift
// 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)
}
}Key points:
- Dynamic registration (when user acts, not at launch)
- Progress reporting is MANDATORY — tasks with no updates auto-expire
- User can monitor and cancel from system UI
- Use strategy when work is only useful if it starts immediately
.fail
适用场景:用户明确触发的工作(如点击按钮),需要在应用退到后台后继续运行,并显示可见进度。
不适用场景:自动任务、维护工作、同步任务
swift
// 1. Info.plist — 使用通配符作为动态后缀
// BGTaskSchedulerPermittedIdentifiers:
// "com.yourapp.export.*"
// 2. 用户触发操作时注册(而非启动时)
func userTappedExportButton() {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.export.photos"
) { task in
let continuedTask = task as! BGContinuedProcessingTask
self.handleExport(task: continuedTask)
}
// 立即提交
let request = BGContinuedProcessingTaskRequest(
identifier: "com.yourapp.export.photos",
title: "Exporting Photos",
subtitle: "0 of 100 photos"
)
// 可选:如果无法立即启动则失败
request.strategy = .fail // 或.enqueue(默认)
do {
try BGTaskScheduler.shared.submit(request)
} catch {
showError("Cannot export in background right now")
}
}
// 3. 带强制进度报告的处理程序
func handleExport(task: BGContinuedProcessingTask) {
var shouldContinue = true
task.expirationHandler = {
shouldContinue = false
}
// 强制要求:报告进度(无进度更新的任务会自动过期)
task.progress.totalUnitCount = 100
task.progress.completedUnitCount = 0
Task {
for (index, photo) in photos.enumerated() {
guard shouldContinue else { break }
await exportPhoto(photo)
// 更新进度——系统会向用户显示
task.progress.completedUnitCount = Int64(index + 1)
}
task.setTaskCompleted(success: shouldContinue)
}
}关键点:
- 动态注册(用户操作时,而非启动时)
- 进度报告是强制要求——无更新的任务会自动过期
- 用户可在系统UI中监控并取消任务
- 当工作仅在立即启动时有用,使用策略
.fail
Pattern 5: beginBackgroundTask — Short Critical Work
模式5:beginBackgroundTask — 短时间关键工作
Use when: App is backgrounding and you need ~30 seconds to finish critical work (save state, complete upload).
swift
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
}
}
}Key points:
- Call AS SOON as work completes (not just in expiration handler)
endBackgroundTask - Failing to end task may cause system to terminate your app and impact future launches
- ~30 seconds max, not guaranteed
- Use for state saving, not ongoing work
适用场景:应用退到后台时,你需要约30秒时间完成关键工作(保存状态、完成上传)。
swift
var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
func applicationDidEnterBackground(_ application: UIApplication) {
// 启动后台任务
backgroundTaskID = application.beginBackgroundTask(withName: "Save State") { [weak self] in
// 过期处理程序——清理并结束任务
self?.saveProgress()
if let taskID = self?.backgroundTaskID {
application.endBackgroundTask(taskID)
}
self?.backgroundTaskID = .invalid
}
// 执行关键工作
saveEssentialState { [weak self] in
// 工作完成后立即结束任务——不要等到过期
if let taskID = self?.backgroundTaskID, taskID != .invalid {
UIApplication.shared.endBackgroundTask(taskID)
self?.backgroundTaskID = .invalid
}
}
}关键点:
- 工作完成后立即调用(不仅在过期处理程序中)
endBackgroundTask - 未结束任务可能导致系统终止你的应用,并影响未来的启动
- 最多约30秒,且不保证一定能获得
- 用于状态保存,而非持续工作
Pattern 6: Background URLSession
模式6:Background URLSession
Use when: Large downloads/uploads that should continue even if app terminates.
swift
// 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)
}Key points:
- Work handed off to system daemon () — continues after app termination
nsurlsessiond - for non-urgent (system waits for WiFi, charging)
isDiscretionary = true - Must handle for app relaunch
handleEventsForBackgroundURLSession - Move downloaded files immediately — temp location deleted after delegate returns
适用场景:即使应用终止也需要继续的大型下载/上传任务。
swift
// 1. 创建后台配置
lazy var backgroundSession: URLSession = {
let config = URLSessionConfiguration.background(
withIdentifier: "com.yourapp.downloads"
)
config.sessionSendsLaunchEvents = true // 任务完成时重新启动应用
config.isDiscretionary = true // 系统选择最佳时间
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
// 2. 启动下载
func downloadFile(from url: URL) {
let task = backgroundSession.downloadTask(with: url)
task.resume()
}
// 3. 处理应用因会话事件重启(AppDelegate)
func application(_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void) {
// 保存完成处理程序——处理完事件后调用
backgroundSessionCompletionHandler = completionHandler
// 会话代理方法会被调用
}
// 4. URLSessionDelegate
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
// 所有事件处理完成——调用保存的完成处理程序
DispatchQueue.main.async {
self.backgroundSessionCompletionHandler?()
self.backgroundSessionCompletionHandler = nil
}
}
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
// 返回前将文件从临时位置移走
let destinationURL = getDestinationURL(for: downloadTask)
try? FileManager.default.moveItem(at: location, to: destinationURL)
}关键点:
- 工作移交到系统守护进程()——应用终止后仍会继续
nsurlsessiond - 非紧急任务设置(系统会等待WiFi、充电时机)
isDiscretionary = true - 必须处理以应对应用重启
handleEventsForBackgroundURLSession - 立即移动下载的文件——临时位置会在代理返回后被删除
Pattern 7: Silent Push Notification Trigger
模式7:静默推送通知触发
Use when: Server needs to wake app to fetch new data.
适用场景:服务器需要唤醒应用以获取新数据。
Server Payload
服务器负载
json
{
"aps": {
"content-available": 1
},
"custom-data": "fetch-new-messages"
}Use (not 10) for energy efficiency.
apns-priority: 5json
{
"aps": {
"content-available": 1
},
"custom-data": "fetch-new-messages"
}使用(而非10)以提升能效。
apns-priority: 5App Handler
应用处理程序
swift
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)
}
}
}Key points:
- Silent pushes are rate-limited — don't expect launch on every push
- System coalesces multiple pushes (14 pushes may result in 7 launches)
- Budget depletes with each launch and refills throughout day
- ~30 seconds runtime per launch
swift
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秒
Swift 6 Cancellation Integration
Swift 6 取消集成
When using structured concurrency, bridge BGTask expiration to task cancellation:
swift
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)
}
}Key points:
- handles cancellation while task is suspended
withTaskCancellationHandler - throws
Task.checkCancellation()if cancelledCancellationError - for non-throwing check
Task.isCancelled - Cancellation is cooperative — your code must check and respond
使用结构化并发时,将BGTask过期与任务取消桥接:
swift
func handleAppRefresh(task: BGAppRefreshTask) {
// 创建一个尊重过期机制的Task
let workTask = Task {
try await withTaskCancellationHandler {
// 你的异步工作
try await fetchAndProcessData()
task.setTaskCompleted(success: true)
} onCancel: {
// 调用task.cancel()时同步执行
// 注意:在任意线程运行,保持轻量
}
}
// 将过期事件桥接到取消操作
task.expirationHandler = {
workTask.cancel() // 触发onCancel块
}
}
// 在工作中检查取消状态
func fetchAndProcessData() async throws {
for item in items {
// 检查是否需要停止
try Task.checkCancellation()
// 或非抛出式检查
guard !Task.isCancelled else {
saveProgress()
return
}
try await process(item)
}
}关键点:
- 处理任务挂起时的取消
withTaskCancellationHandler - 在取消时抛出
Task.checkCancellation()CancellationError - 用于非抛出式检查
Task.isCancelled - 取消是协作式的——你的代码必须检查并响应
Testing Background Tasks
测试后台任务
Simulator Limitations
模拟器局限性
Background tasks do not run automatically in simulator. You must manually trigger them.
后台任务在模拟器中不会自动运行。你必须手动触发它们。
LLDB Debugging Commands
LLDB调试命令
While app is running with debugger attached, pause execution and run:
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"]应用在调试器附加的状态下运行时,暂停执行并运行以下命令:
lldb
// 触发任务启动
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.yourapp.refresh"]
// 触发任务过期(测试过期处理程序)
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.yourapp.refresh"]Testing Workflow
测试流程
- Set breakpoint in task handler
- Run app, let it background
- Pause in debugger
- Run command
_simulateLaunchForTaskWithIdentifier - Resume — breakpoint should hit
- Test expiration with
_simulateExpirationForTaskWithIdentifier
- 在任务处理程序中设置断点
- 运行应用,让它进入后台
- 在调试器中暂停
- 运行命令
_simulateLaunchForTaskWithIdentifier - 恢复执行——断点应被触发
- 使用测试过期处理程序
_simulateExpirationForTaskWithIdentifier
Testing Checklist
7个调度因素
- Task handler breakpoint hits when simulated?
- Expiration handler called when simulated?
- called in all code paths?
setTaskCompleted - Works on real device (not just simulator)?
- Works in release build (not just debug)?
- App not swiped away from App Switcher?
来自WWDC 2020-10063《Background execution demystified》:
| 因素 | 描述 | 影响 |
|---|---|---|
| 电量极低 | 电量<20% | 所有可自由调度的工作暂停 |
| 低电量模式 | 用户启用 | 后台活动受限 |
| 应用使用频率 | 用户启动应用的频率 | 使用越频繁,优先级越高 |
| 应用切换器状态 | 应用是否仍可见 | 被划走后无后台执行 |
| 后台应用刷新 | 系统设置 | 关闭后BGAppRefreshTask无法运行 |
| 系统预算 | 能耗/数据预算 | 每次启动消耗预算,预算随时间恢复 |
| 速率限制 | 系统间隔控制 | 防止过于频繁的启动 |
The 7 Scheduling Factors
响应系统约束
From WWDC 2020-10063 "Background execution demystified":
| Factor | Description | Impact |
|---|---|---|
| Critically Low Battery | <20% battery | All discretionary work paused |
| Low Power Mode | User-enabled | Background activity limited |
| App Usage | How often user launches app | More usage = higher priority |
| App Switcher | App still visible? | Swiped away = no background |
| Background App Refresh | System setting | Off = no BGAppRefresh tasks |
| System Budgets | Energy/data budgets | Deplete with launches, refill over day |
| Rate Limiting | System spacing | Prevents too-frequent launches |
swift
// 检查低电量模式
if ProcessInfo.processInfo.isLowPowerModeEnabled {
// 减少后台工作
}
// 监听变化
NotificationCenter.default.publisher(for: .NSProcessInfoPowerStateDidChange)
.sink { _ in
// 调整行为
}
// 检查后台应用刷新状态
let status = UIApplication.shared.backgroundRefreshStatus
switch status {
case .available:
break // 可以调度
case .denied:
// 用户已禁用——提示在设置中启用
case .restricted:
// 家长控制或MDM——无法启用
}Responding to System Constraints
审核检查清单
—
注册检查清单
swift
// 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?
Audit Checklists
调度检查清单
Registration Checklist
—
- Identifier in Info.plist exactly matches code (case-sensitive)?
- Correct background mode enabled (,
fetch)?processing - Registration happens in BEFORE return?
didFinishLaunchingWithOptions - Not registering same identifier multiple times?
- Handler closure doesn't capture self strongly?
- 在主队列或后台队列调度(如果对性能敏感)?
- 未设置得太远(最多约1周)?
earliestBeginDate - 处理了的错误?
submit() - 未调度重复任务(检查)?
getPendingTaskRequests
Scheduling Checklist
处理程序检查清单
- Scheduling on main queue or background queue (if performance sensitive)?
- not too far in future (max ~1 week)?
earliestBeginDate - Handling errors?
submit() - Not scheduling duplicate tasks (check )?
getPendingTaskRequests
- 在处理程序启动时立即设置了过期处理程序?
- 在所有代码路径中调用了?
setTaskCompleted(success:) - 调度了下一个任务(针对持续模式)?
- 长时间操作时增量保存了进度?
- 过期处理程序确实取消了进行中的工作?
Handler Checklist
生产环境就绪检查
- Expiration handler set IMMEDIATELY at start of handler?
- called in ALL code paths?
setTaskCompleted(success:) - Next task scheduled (for continuous patterns)?
- Progress saved incrementally for long operations?
- Expiration handler actually cancels ongoing work?
- 在真实设备上测试过,而非仅在模拟器?
- 在发布版本中测试过,而非仅在调试版本?
- 在低电量模式下测试过?
- 在应用切换器中强制退出后测试过(任务不应运行)?
- 控制台日志显示预期的“Task completed”消息?
Production Readiness
压力场景
—
场景1:“只需每30秒在后台轮询服务器一次”
- Tested on real device, not just simulator?
- Tested in release build, not just debug?
- Tested with Low Power Mode enabled?
- Tested after force-quit from App Switcher (should NOT run)?
- Console logs show expected "Task completed" messages?
诱惑:“轮询比推送通知简单。我们需要实时更新。”
现实:
- iOS不会给你30秒的后台间隔
- BGAppRefreshTask基于用户行为模式运行,而非你的固定调度
- 如果用户很少打开应用,任务可能每天仅运行一次或更少
- 轮询会快速消耗预算——导致总启动次数减少
时间成本对比:
- 实现轮询:30分钟(无法达到预期效果)
- 理解为何无效:2-4小时调试
- 实现正确的推送通知:3-4小时
可行方案:
- 静默推送通知(由服务器触发,而非轮询)
- BGAppRefreshTask用于预测用户行为(非实时)
- BGProcessingTask用于可延迟的工作(夜间)
应对话术模板:“iOS后台执行不支持固定间隔的轮询。BGAppRefreshTask基于iOS预测用户打开应用的时间运行,而非固定调度。如需实时更新,我们需要服务器端的推送通知。我可以给你看苹果官方的相关文档。”
Pressure Scenarios
场景2:“我的任务需要5分钟,不是30秒”
Scenario 1: "Just poll the server every 30 seconds in background"
—
The temptation: "Polling is simpler than push notifications. We need real-time updates."
The reality:
- iOS will NOT give you 30-second background intervals
- BGAppRefreshTask runs based on USER behavior patterns, not your schedule
- If user rarely opens app, task may run once per day or less
- Polling burns budget quickly — fewer total launches
Time cost comparison:
- Implement polling: 30 minutes (won't work as expected)
- Understand why it doesn't work: 2-4 hours debugging
- Implement proper push notifications: 3-4 hours
What actually works:
- Silent push notifications (server triggers, not polling)
- BGAppRefreshTask for predicted user behavior (not real-time)
- BGProcessingTask for deferrable work (overnight)
Pushback template: "iOS background execution doesn't support polling intervals. BGAppRefreshTask runs based on when iOS predicts the user will open our app, not on a fixed schedule. For real-time updates, we need server-side push notifications. Let me show you Apple's documentation on this."
诱惑:“我只要用beginBackgroundTask就能完成所有工作。”
现实:
- beginBackgroundTask:最多约30秒
- BGAppRefreshTask:约30秒
- BGProcessingTask:数分钟,但仅在充电时运行
- 没有API能保证给你5分钟的前台级运行时间
可行方案:
- 拆分工作——分成30秒的片段,保存进度
- 使用BGProcessingTask并设置(夜间运行)
requiresExternalPower = true - iOS 26+:对用户触发的工作使用BGContinuedProcessingTask
应对话术模板:“iOS限制后台运行时长以保护电池。对于需要数分钟的工作,我们有两个选项:(1) BGProcessingTask在夜间充电时运行——适合维护工作;(2) 将工作拆分为30秒的片段,在各片段间保存进度。哪个更符合我们的使用场景?”
Scenario 2: "My task needs 5 minutes, not 30 seconds"
场景3:“我的设备上正常,但用户的设备不行”
The temptation: "I'll just use beginBackgroundTask and do all my work."
The reality:
- beginBackgroundTask: ~30 seconds max
- BGAppRefreshTask: ~30 seconds
- BGProcessingTask: Several minutes, but only when charging
- No API gives you guaranteed 5-minute foreground-quality runtime
What actually works:
- Chunk your work — Break into 30-second pieces, save progress
- Use BGProcessingTask with (runs overnight)
requiresExternalPower = true - iOS 26+: Use BGContinuedProcessingTask for user-initiated work
Pushback template: "iOS limits background runtime to protect battery. For work that needs several minutes, we have two options: (1) BGProcessingTask runs overnight when charging — great for maintenance, (2) Break work into chunks that complete in 30 seconds, saving progress between runs. Which fits our use case better?"
诱惑:“代码是正确的——肯定是用户设备的问题。”
现实:Xcode附加的调试版本与野外的发布版本行为不同。
常见原因:
- 启用了低电量模式——限制后台活动
- 后台应用刷新已禁用——用户或家长控制设置
- 应用被划走——终止所有后台任务
- 预算耗尽——近期启动次数过多
- 应用很少被使用——系统降低了优先级
调试步骤:
- 在启动时检查并记录日志
backgroundRefreshStatus - 记录任务调度和完成的时间
- 使用MetricKit监控生产环境中的后台启动情况
- 询问用户:“你是否在应用切换器中强制退出过应用?”
应对话术模板:“后台执行取决于7个系统因素,包括电量水平、用户应用使用模式,以及是否强制退出过应用。我会添加日志来了解受影响用户的实际情况。”
Scenario 3: "It works on my device but not for users"
场景4:“现在先发布,之后再添加后台任务”
The temptation: "The code is correct — it must be a user device issue."
The reality: Debug builds with Xcode attached behave differently than release builds in the wild.
Common causes:
- Low Power Mode enabled — limits background activity
- Background App Refresh disabled — user or parental controls
- App swiped away — kills all background tasks
- Budget exhausted — too many recent launches
- Rarely used app — system deprioritizes
Debugging steps:
- Check at launch, log it
backgroundRefreshStatus - Log when tasks are scheduled and completed
- Use MetricKit to monitor background launches in production
- Ask users: "Did you force-quit the app from App Switcher?"
Pushback template: "Background execution depends on 7 system factors including battery level, user app usage patterns, and whether they force-quit the app. Let me add logging to understand what's happening for affected users."
诱惑:“后台工作是锦上添花的功能。”
现实:
- 用户打开应用时期望内容是新鲜的
- 支持后台刷新的竞品应用感觉响应更快
- 后续添加后台任务需要注意注册时机
- 内容过时的第一印象会影响留存率
时间成本对比:
- 现在添加BGAppRefreshTask:1-2小时
- 后续改造并完成测试:4-6小时
- 排查“为何不工作”的问题:额外数小时
最小可行的后台实现:
swift
// 在didFinishLaunchingWithOptions中
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.refresh",
using: nil
) { task in
task.setTaskCompleted(success: true) // 占位符
self.scheduleRefresh()
}应对话术模板:“后台刷新是[应用类型]的核心用户预期。最小实现仅需20行代码。如果现在发布时不添加,后续再补的话,可能会遇到注册时机的Bug。我现在就添加基础框架,这样我们可以在发布后再进行增强。”
Scenario 4: "Ship now, add background tasks later"
真实案例
—
案例1:任务从未运行——标识符不匹配
The temptation: "Background work is a nice-to-have feature."
The reality:
- Users expect content to be fresh when they open the app
- Competing apps that refresh in background feel more responsive
- Adding background tasks later requires careful registration timing
- First impression of stale content drives retention
Time cost comparison:
- Add BGAppRefreshTask now: 1-2 hours
- Retrofit later with proper testing: 4-6 hours
- Debug "why doesn't it work" issues: Additional hours
Minimum viable background:
swift
// In didFinishLaunchingWithOptions
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.refresh",
using: nil
) { task in
task.setTaskCompleted(success: true) // Placeholder
self.scheduleRefresh()
}Pushback template: "Background refresh is a core expectation for [type of app]. The minimum implementation is 20 lines of code. If we ship without it and add later, we risk registration timing bugs. Let me add the scaffolding now so we can enhance it post-launch."
症状:成功,但处理程序从未被调用。
submit()诊断:
swift
// 代码中使用:
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.myapp.Refresh", // 大写R
...
)
// Info.plist中的内容:
// "com.myapp.refresh" // 小写r修复:标识符必须完全匹配(区分大小写)。
浪费的时间:2小时调试代码逻辑,问题却只是拼写错误。
Real-World Examples
案例2:任务终止——缺少setTaskCompleted
Example 1: Task Never Runs — Identifier Mismatch
—
Symptom: succeeds but handler never called.
submit()Diagnosis:
swift
// Code uses:
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.myapp.Refresh", // Capital R
...
)
// Info.plist has:
// "com.myapp.refresh" // lowercase rFix: Identifiers must EXACTLY match (case-sensitive).
Time wasted: 2 hours debugging code logic when issue was typo.
症状:处理程序运行,工作看似完成,但下一个调度的任务从未运行。
诊断:
swift
func handleRefresh(task: BGAppRefreshTask) {
fetchData { result in
switch result {
case .success:
task.setTaskCompleted(success: true) // ✅ 已调用
case .failure:
// ❌ 缺少setTaskCompleted!
print("Failed")
}
}
}修复:在所有代码路径(包括错误场景)中调用。
setTaskCompletedswift
case .failure:
task.setTaskCompleted(success: false) // ✅ 现在已调用影响:未调用setTaskCompleted可能导致系统惩罚应用的后台预算。
Example 2: Task Terminates — Missing setTaskCompleted
案例3:开发环境正常,生产环境异常——强制退出
Symptom: Handler runs, work appears to complete, but next scheduled task never runs.
Diagnosis:
swift
func handleRefresh(task: BGAppRefreshTask) {
fetchData { result in
switch result {
case .success:
task.setTaskCompleted(success: true) // ✅ Called
case .failure:
// ❌ Missing setTaskCompleted!
print("Failed")
}
}
}Fix: Call in ALL code paths including errors.
setTaskCompletedswift
case .failure:
task.setTaskCompleted(success: false) // ✅ Now calledImpact: Failing to call setTaskCompleted may cause system to penalize app's background budget.
症状:用户反馈后台同步不工作。开发者无法复现。
诊断:
用户:“我每天晚上都会关闭应用以节省电池。”
开发者:“你是怎么关闭的?”
用户:“在应用切换器中向上划。”现实:在应用切换器中划走应用=强制退出=直到用户再次启动应用前,不会有后台任务运行。
修复:
- 教育用户(不理想)
- 接受这是iOS的行为
- 确保应用重新启动时有良好的首次启动体验
Example 3: Works in Dev, Not Production — Force Quit
案例4:BGProcessingTask从未运行——缺少电源要求
Symptom: Users report background sync doesn't work. Developer can't reproduce.
Diagnosis:
User: "I close my apps every night to save battery."
Developer: "How do you close them?"
User: "Swipe up in the app switcher."Reality: Swiping away from App Switcher = force quit = no background tasks until user opens app again.
Fix:
- Educate users (not ideal)
- Accept this is iOS behavior
- Ensure good first-launch experience when app reopens
症状:BGProcessingTask已调度,但从未执行。
诊断:用户夜间将手机插在充电器上,但任务设置了,而用户使用的是无线充电器。
requiresExternalPower = true不对,真实问题是:
swift
let request = BGProcessingTaskRequest(identifier: "com.app.maintenance")
// 缺少:request.requiresExternalPower = true如果不设置,系统仍会等待充电时机,但确定性更低。显式设置该参数能给系统明确的信号。
requiresExternalPower另外:用户必须在约2周内前台启动过应用,处理任务才会符合运行条件。
Example 4: BGProcessingTask Never Runs — Missing Power Requirement
快速参考
—
LLDB调试命令
Symptom: BGProcessingTask scheduled but never executes.
Diagnosis: User has phone plugged in at night, but task has and user uses wireless charger.
requiresExternalPower = trueWait, that's not the issue. Real issue:
swift
let request = BGProcessingTaskRequest(identifier: "com.app.maintenance")
// Missing: request.requiresExternalPower = trueWithout , system STILL waits for charging but has less certainty. Setting it explicitly gives system clear signal.
requiresExternalPowerAlso: User must have launched app in foreground within ~2 weeks for processing tasks to be eligible.
lldb
// 触发任务
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"IDENTIFIER"]
// 触发过期
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"IDENTIFIER"]Quick Reference
控制台过滤条件
LLDB Debugging Commands
—
lldb
// Trigger task
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"IDENTIFIER"]
// Trigger expiration
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"IDENTIFIER"]subsystem:com.apple.backgroundtaskschedulerConsole Filter
任务类型汇总
subsystem:com.apple.backgroundtaskscheduler| 需求 | 使用方案 | 运行时长 |
|---|---|---|
| 保持内容新鲜 | BGAppRefreshTask | ~30s |
| 重型维护工作 | BGProcessingTask + requiresExternalPower | 数分钟 |
| 用户触发的任务延续 | BGContinuedProcessingTask(iOS 26) | 延长时长 |
| 后台时完成收尾 | beginBackgroundTask | ~30s |
| 大型下载 | Background URLSession | 按需 |
| 服务器触发 | 静默推送通知 | ~30s |
Task Type Summary
资源
| Need | Use | Runtime |
|---|---|---|
| Keep content fresh | BGAppRefreshTask | ~30s |
| Heavy maintenance | BGProcessingTask + requiresExternalPower | Minutes |
| User-initiated continuation | BGContinuedProcessingTask (iOS 26) | Extended |
| Finish on background | beginBackgroundTask | ~30s |
| Large downloads | Background URLSession | As needed |
| Server-triggered | Silent push notification | ~30s |
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
最后更新:2025-12-31
平台:iOS 13+, iOS 26+(BGContinuedProcessingTask)
状态:可用于生产环境的后台任务模式
Resources
—
WWDC: 2019-707, 2020-10063, 2022-10142, 2023-10170, 2025-227
Docs: /backgroundtasks/bgtaskscheduler, /backgroundtasks/starting-and-terminating-tasks-during-development
Skills: axiom-background-processing-ref, axiom-background-processing-diag, axiom-energy
Last Updated: 2025-12-31
Platforms: iOS 13+, iOS 26+ (BGContinuedProcessingTask)
Status: Production-ready background task patterns
—