axiom-background-processing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Background 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
axiom-energy
skill. This skill focuses on task mechanics — making tasks run correctly and complete reliably.
Requirements: 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
    axiom-energy
    skill)
  • 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.backgroundtaskscheduler
Look 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

任务类型对比

TypeRuntimeWhen RunsUse Case
BGAppRefreshTask~30 secondsBased on user app usage patternsFetch latest content
BGProcessingTaskSeveral minutesDevice charging, idle (typically overnight)Maintenance, ML training
BGContinuedProcessingTaskExtendedSystem-managed with progress UIUser-initiated export/publish
beginBackgroundTask~30 secondsImmediately when backgroundingSave state, finish upload
Background URLSessionAs neededSystem-friendly time, even after terminationLarge 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
    setTaskCompleted
    in ALL code paths (success AND failure)
  • 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
    requiresExternalPower = true
    for CPU-intensive work (prevents battery drain)
  • Save progress incrementally — task may be interrupted
  • Work may never run if user doesn't charge device
  • Don't set
    earliestBeginDate
    more than a week ahead

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
    (防止电池消耗)
  • 增量保存进度——任务可能被中断
  • 如果用户不充电,任务可能永远不会运行
  • earliestBeginDate
    不要设置超过1周

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
    setTaskCompleted
    needed)
  • 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
    .fail
    strategy when work is only useful if it starts immediately

适用场景:用户明确触发的工作(如点击按钮),需要在应用退到后台后继续运行,并显示可见进度。
不适用场景:自动任务、维护工作、同步任务
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
    endBackgroundTask
    AS SOON as work completes (not just in expiration handler)
  • 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 (
    nsurlsessiond
    ) — continues after app termination
  • isDiscretionary = true
    for non-urgent (system waits for WiFi, charging)
  • Must handle
    handleEventsForBackgroundURLSession
    for app relaunch
  • 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
    )——应用终止后仍会继续
  • 非紧急任务设置
    isDiscretionary = true
    (系统会等待WiFi、充电时机)
  • 必须处理
    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
apns-priority: 5
(not 10) for energy efficiency.
json
{
    "aps": {
        "content-available": 1
    },
    "custom-data": "fetch-new-messages"
}
使用
apns-priority: 5
(而非10)以提升能效。

App 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:
  • withTaskCancellationHandler
    handles cancellation while task is suspended
  • Task.checkCancellation()
    throws
    CancellationError
    if cancelled
  • Task.isCancelled
    for non-throwing check
  • 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

测试流程

  1. Set breakpoint in task handler
  2. Run app, let it background
  3. Pause in debugger
  4. Run
    _simulateLaunchForTaskWithIdentifier
    command
  5. Resume — breakpoint should hit
  6. Test expiration with
    _simulateExpirationForTaskWithIdentifier
  1. 在任务处理程序中设置断点
  2. 运行应用,让它进入后台
  3. 在调试器中暂停
  4. 运行
    _simulateLaunchForTaskWithIdentifier
    命令
  5. 恢复执行——断点应被触发
  6. 使用
    _simulateExpirationForTaskWithIdentifier
    测试过期处理程序

Testing Checklist

7个调度因素

  • Task handler breakpoint hits when simulated?
  • Expiration handler called when simulated?
  • setTaskCompleted
    called in all code paths?
  • 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":
FactorDescriptionImpact
Critically Low Battery<20% batteryAll discretionary work paused
Low Power ModeUser-enabledBackground activity limited
App UsageHow often user launches appMore usage = higher priority
App SwitcherApp still visible?Swiped away = no background
Background App RefreshSystem settingOff = no BGAppRefresh tasks
System BudgetsEnergy/data budgetsDeplete with launches, refill over day
Rate LimitingSystem spacingPrevents 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
    didFinishLaunchingWithOptions
    BEFORE return?
  • Not registering same identifier multiple times?
  • Handler closure doesn't capture self strongly?
  • 在主队列或后台队列调度(如果对性能敏感)?
  • earliestBeginDate
    未设置得太远(最多约1周)?
  • 处理了
    submit()
    的错误?
  • 未调度重复任务(检查
    getPendingTaskRequests
    )?

Scheduling Checklist

处理程序检查清单

  • Scheduling on main queue or background queue (if performance sensitive)?
  • earliestBeginDate
    not too far in future (max ~1 week)?
  • Handling
    submit()
    errors?
  • Not scheduling duplicate tasks (check
    getPendingTaskRequests
    )?
  • 在处理程序启动时立即设置了过期处理程序?
  • 所有代码路径中调用了
    setTaskCompleted(success:)
  • 调度了下一个任务(针对持续模式)?
  • 长时间操作时增量保存了进度?
  • 过期处理程序确实取消了进行中的工作?

Handler Checklist

生产环境就绪检查

  • Expiration handler set IMMEDIATELY at start of handler?
  • setTaskCompleted(success:)
    called in ALL code paths?
  • 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分钟的前台级运行时间
可行方案
  1. 拆分工作——分成30秒的片段,保存进度
  2. 使用BGProcessingTask并设置
    requiresExternalPower = true
    (夜间运行)
  3. 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:
  1. Chunk your work — Break into 30-second pieces, save progress
  2. Use BGProcessingTask with
    requiresExternalPower = true
    (runs overnight)
  3. 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附加的调试版本与野外的发布版本行为不同。
常见原因
  1. 启用了低电量模式——限制后台活动
  2. 后台应用刷新已禁用——用户或家长控制设置
  3. 应用被划走——终止所有后台任务
  4. 预算耗尽——近期启动次数过多
  5. 应用很少被使用——系统降低了优先级
调试步骤
  1. 在启动时检查
    backgroundRefreshStatus
    并记录日志
  2. 记录任务调度和完成的时间
  3. 使用MetricKit监控生产环境中的后台启动情况
  4. 询问用户:“你是否在应用切换器中强制退出过应用?”
应对话术模板:“后台执行取决于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:
  1. Low Power Mode enabled — limits background activity
  2. Background App Refresh disabled — user or parental controls
  3. App swiped away — kills all background tasks
  4. Budget exhausted — too many recent launches
  5. Rarely used app — system deprioritizes
Debugging steps:
  1. Check
    backgroundRefreshStatus
    at launch, log it
  2. Log when tasks are scheduled and completed
  3. Use MetricKit to monitor background launches in production
  4. 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:
submit()
succeeds but handler never called.
Diagnosis:
swift
// Code uses:
BGTaskScheduler.shared.register(
    forTaskWithIdentifier: "com.myapp.Refresh",  // Capital R
    ...
)

// Info.plist has:
// "com.myapp.refresh"  // lowercase r
Fix: 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")
        }
    }
}
修复:在所有代码路径(包括错误场景)中调用
setTaskCompleted
swift
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
setTaskCompleted
in ALL code paths including errors.
swift
case .failure:
    task.setTaskCompleted(success: false)  // ✅ Now called
Impact: Failing to call setTaskCompleted may cause system to penalize app's background budget.

症状:用户反馈后台同步不工作。开发者无法复现。
诊断
用户:“我每天晚上都会关闭应用以节省电池。”
开发者:“你是怎么关闭的?”
用户:“在应用切换器中向上划。”
现实:在应用切换器中划走应用=强制退出=直到用户再次启动应用前,不会有后台任务运行。
修复
  1. 教育用户(不理想)
  2. 接受这是iOS的行为
  3. 确保应用重新启动时有良好的首次启动体验

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:
  1. Educate users (not ideal)
  2. Accept this is iOS behavior
  3. 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
requiresExternalPower = true
and user uses wireless charger.
Wait, that's not the issue. Real issue:
swift
let request = BGProcessingTaskRequest(identifier: "com.app.maintenance")
// Missing: request.requiresExternalPower = true
Without
requiresExternalPower
, system STILL waits for charging but has less certainty. Setting it explicitly gives system clear signal.
Also: 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.backgroundtaskscheduler

Console Filter

任务类型汇总

subsystem:com.apple.backgroundtaskscheduler
需求使用方案运行时长
保持内容新鲜BGAppRefreshTask~30s
重型维护工作BGProcessingTask + requiresExternalPower数分钟
用户触发的任务延续BGContinuedProcessingTask(iOS 26)延长时长
后台时完成收尾beginBackgroundTask~30s
大型下载Background URLSession按需
服务器触发静默推送通知~30s

Task Type Summary

资源

NeedUseRuntime
Keep content freshBGAppRefreshTask~30s
Heavy maintenanceBGProcessingTask + requiresExternalPowerMinutes
User-initiated continuationBGContinuedProcessingTask (iOS 26)Extended
Finish on backgroundbeginBackgroundTask~30s
Large downloadsBackground URLSessionAs needed
Server-triggeredSilent 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