mcpField
A
axiom-background-processing

axiom-background-processing

github.com/charleswiltgen开发1.2k178 次安装今天更新
代码仓库

关于 axiom-background-processing

axiom-background-processing 是面向 iOS 开发者的后台任务实现与排查技能,介绍注册、调度约束、任务类型和过期处理,帮助查明任务为何未运行或未完成。

主要功能

  1. 01检查任务标识符和注册时机
  2. 02选择 iOS 后台任务类型
  3. 03处理过期并保存阶段性进度
  4. 04通过 Console 日志排查调度
  5. 05使用 LLDB 测试并了解模拟器限制

适用场景

  1. 01诊断任务始终未启动的问题
  2. 02为内容刷新或维护选择任务类型
  3. 03系统结束任务时保留未完成进度
  4. 04调查开发环境与生产环境的差异

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-background-processing

安装后在 Claude Code 中输入/axiom-background-processing确认已加载。

详细介绍

后台处理

概述

后台执行是一种特权,而非权利。iOS 会主动限制后台工作,以保护电池续航和用户体验。核心原则:将后台任务视为酌情执行的工作——你可以申请一个时间窗口,但系统决定何时(或是否)运行你的代码。

关键要点:大多数“我的任务从未运行”问题,都源于注册错误或对影响执行的 7 个调度因素理解有误。本技能提供系统化调试方法,而非凭猜测排查。

能耗优化:如需降低后台任务对电池的影响,请参阅 axiom-energy 技能。本技能专注于任务的机制——确保任务正确运行并可靠完成。

要求:iOS 13+(BGTaskScheduler)、iOS 26+(BGContinuedProcessingTask)、Xcode 15+

示例提示

开发者常问的、本技能可以解答的问题:

1. “我的后台任务从未运行。我注册了,也安排了任务,但什么都没发生。”

→ 本技能介绍了“任务从未运行”问题的注册检查清单和调试决策树

2. “如何测试后台任务?它们似乎不会在模拟器中触发。”

→ 本技能介绍了 LLDB 调试命令和模拟器限制

3. “我的任务在完成前就被终止了。如何延长运行时间?”

→ 本技能介绍了任务类型(BGAppRefresh 为 30 秒,BGProcessing 为数分钟)、过期处理程序和增量保存进度

4. “我应该使用 BGAppRefreshTask 还是 BGProcessingTask?有什么区别?”

→ 本技能提供了决策树,根据工作时长和系统要求选择正确的任务类型

5. “如何将 Swift 6 并发与后台任务过期机制集成?”

→ 本技能介绍了用于将 BGTask 过期机制衔接到结构化并发的 withTaskCancellationHandler 模式

6. “我的后台任务在开发环境中正常运行,但在生产环境中不行。”

→ 本技能介绍了 7 个调度因素、节流行为和生产环境调试


危险信号——任务无法运行或被终止

如果看到以下任意情况,应怀疑是注册或调度问题:

  • 任务从未运行:submit() 成功,但从未调用处理程序
  • 任务立即终止:已调用处理程序,但工作未能完成
  • 开发环境正常,生产环境不行:任务在调试器下运行,但在发行版构建中不运行
  • 控制台未显示启动记录:统一日志中没有“BackgroundTask”条目
  • 标识符不匹配错误:任务标识符与 Info.plist 不匹配
  • “未注册处理程序”:首次安排任务之前未注册处理程序
与能耗问题的区别
  • 能耗问题:任务会运行,但会耗尽电池电量(请参阅 axiom-energy 技能)
  • 本技能:任务无法运行,或在完成工作之前被终止

必须先执行的步骤

调试代码前,务必先验证以下事项:

步骤 1:验证 Info.plist 配置(2 分钟)

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.yourapp.refresh</string>
    <string>com.yourapp.processing</string>
</array>


<key>UIBackgroundModes</key>
<array>
    <string>fetch</string>
</array>


<array>
    <string>fetch</string>
    <string>processing</string>
</array>

常见错误:代码中的标识符与 Info.plist 中的标识符不完全一致。检查是否有拼写错误,并注意大小写。

步骤 2:验证注册时机(2 分钟)

必须在应用启动完成之前完成注册:

// ✅ CORRECT: Register in didFinishLaunchingWithOptions
func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    BGTaskScheduler.shared.register(
        forTaskWithIdentifier: "com.yourapp.refresh",
        using: nil
    ) { task in
        // Safe force cast: identifier guarantees BGAppRefreshTask type
        self.handleAppRefresh(task: task as! BGAppRefreshTask)
    }

    return true  // Register BEFORE returning
}

// ❌ WRONG: Registering after launch or on-demand
func someButtonTapped() {
    // TOO LATE - registration won't work
    BGTaskScheduler.shared.register(...)
}

例外:BGContinuedProcessingTask(iOS 26+)会在用户发起操作时动态注册。

步骤 3:检查控制台日志(5 分钟)

在 Console.app 中筛选后台任务事件:

subsystem:com.apple.backgroundtaskscheduler

查找:

  • “已注册任务标识符对应的处理程序”
  • “正在安排任务标识符对应的任务”
  • “正在启动任务标识符对应的任务”
  • “任务已完成,标识符为”
  • 有关缺少处理程序或标识符的错误消息
步骤 4:确认应用未被从多任务切换器中划掉(1 分钟)

重要:如果用户从多任务切换器中强制退出应用,后台任务将无法运行。

在多任务切换器中检查:应用仍然可见吗?将应用划掉后,后台将无法执行,直到用户再次启动应用。


后台任务决策树

Need to run code in the background?
│
├─ User initiated the action explicitly (button tap)?
│  ├─ iOS 26+? → BGContinuedProcessingTask (Pattern 4)
│  └─ iOS 13-25? → beginBackgroundTask + save progress (Pattern 5)
│
├─ Keep content fresh throughout the day?
│  ├─ Runtime needed ≤ 30 seconds? → BGAppRefreshTask (Pattern 1)
│  └─ Need several minutes? → BGProcessingTask with constraints (Pattern 2)
│
├─ Deferrable maintenance work (DB cleanup, ML training)?
│  └─ BGProcessingTask with requiresExternalPower (Pattern 2)
│
├─ Large downloads/uploads?
│  └─ Background URLSession (Pattern 6)
│
├─ Triggered by server data changes?
│  └─ Silent push notification → fetch data → complete handler (Pattern 7)
│
└─ Short critical work when app backgrounds?
   └─ beginBackgroundTask (Pattern 5)
任务类型对比
类型运行时长运行时机用例
BGAppRefreshTask~30 秒根据用户使用应用的模式获取最新内容
BGProcessingTask几分钟设备正在充电且处于闲置状态(通常是夜间)维护、机器学习训练
BGContinuedProcessingTask较长时间由系统管理,并显示进度界面用户发起的导出/发布
beginBackgroundTask~30 秒应用进入后台时立即运行保存状态、完成上传
Background URLSession视需要而定系统认为合适的时间,即使应用已终止也会运行大型传输

常见模式

模式 1:BGAppRefreshTask — 保持内容最新

适用场景:需要获取新内容,让用户打开应用时看到的内容保持最新。

运行时长:~30 秒

系统运行时机:根据用户使用应用的模式预测。如果用户每天早上打开应用,系统会逐渐掌握这一规律,并在此之前刷新内容。

注册(应用启动时)
func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    BGTaskScheduler.shared.register(
        forTaskWithIdentifier: "com.yourapp.refresh",
        using: nil
    ) { task in
        self.handleAppRefresh(task: task as! BGAppRefreshTask)
    }

    return true
}
计划任务(应用进入后台时)
func scheduleAppRefresh() {
    let request = BGAppRefreshTaskRequest(identifier: "com.yourapp.refresh")

    // earliestBeginDate = MINIMUM delay, not exact time
    // System may run hours later based on usage patterns
    request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)  // At least 15 min

    do {
        try BGTaskScheduler.shared.submit(request)
    } catch {
        print("Failed to schedule refresh: \(error)")
    }
}

// Call when app enters background
func applicationDidEnterBackground(_ application: UIApplication) {
    scheduleAppRefresh()
}

// Or with SceneDelegate / SwiftUI
.onChange(of: scenePhase) { newPhase in
    if newPhase == .background {
        scheduleAppRefresh()
    }
}
处理程序
func handleAppRefresh(task: BGAppRefreshTask) {
    // 1. IMMEDIATELY set expiration handler
    task.expirationHandler = { [weak self] in
        // Cancel any in-progress work
        self?.currentOperation?.cancel()
    }

    // 2. Schedule NEXT refresh (continuous refresh pattern)
    scheduleAppRefresh()

    // 3. Do the work
    fetchLatestContent { [weak self] result in
        switch result {
        case .success:
            task.setTaskCompleted(success: true)
        case .failure:
            task.setTaskCompleted(success: false)
        }
    }
}

要点:

  • 首先设置过期处理程序
  • 在处理程序中计划下一次刷新(持续执行模式)
  • 在所有代码路径中调用 setTaskCompleted(成功和失败时都要调用)
  • 将工作时间控制在 30 秒以内

模式 2:BGProcessingTask — 可延迟的维护工作

适用场景:可以等到系统条件最理想时再执行的维护工作(充电、WiFi、闲置状态)。

运行时长:几分钟

系统运行时机:通常在设备充电的夜间。可能不会每天运行。

注册
BGTaskScheduler.shared.register(
    forTaskWithIdentifier: "com.yourapp.maintenance",
    using: nil
) { task in
    self.handleMaintenance(task: task as! BGProcessingTask)
}
设置约束并计划任务
func scheduleMaintenanceIfNeeded() {
    // Be conscientious — only schedule when work is actually needed
    guard needsMaintenance() else { return }

    let request = BGProcessingTaskRequest(identifier: "com.yourapp.maintenance")

    // CRITICAL: Set requiresExternalPower for CPU-intensive work
    request.requiresExternalPower = true

    // Optional: Require network for cloud sync
    request.requiresNetworkConnectivity = true

    // Don't set earliestBeginDate too far — max ~1 week
    // If user doesn't return to app, task won't run

    do {
        try BGTaskScheduler.shared.submit(request)
    } catch BGTaskScheduler.Error.unavailable {
        print("Background processing not available")
    } catch {
        print("Failed to schedule: \(error)")
    }
}
带进度检查点的处理程序
func handleMaintenance(task: BGProcessingTask) {
    var shouldContinue = true

    task.expirationHandler = { [weak self] in
        shouldContinue = false
        self?.saveProgress()  // Save partial progress!
    }

    Task {
        do {
            // Process in chunks, checking for expiration
            for chunk in workChunks {
                guard shouldContinue else {
                    // Expiration called — stop gracefully
                    break
                }

                try await processChunk(chunk)
                saveProgress()  // Checkpoint after each chunk
            }

            task.setTaskCompleted(success: true)
        } catch {
            task.setTaskCompleted(success: false)
        }
    }
}

要点:

  • 对 CPU 密集型工作设置 requiresExternalPower = true(防止电池电量耗尽)
  • 增量保存进度——任务可能会被中断
  • 如果用户不给设备充电,工作可能永远不会运行
  • earliestBeginDate 不要设置为超过一周后的时间

模式 3:SwiftUI backgroundTask 修饰符

适用场景:使用现代 async/await 模式的 SwiftUI 应用。

@main
struct MyApp: App {
    @Environment(\.scenePhase) var scenePhase

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: scenePhase) { newPhase in
            if newPhase == .background {
                scheduleAppRefresh()
            }
        }
        // Handle app refresh
        .backgroundTask(.appRefresh("com.yourapp.refresh")) {
            // Schedule next refresh
            scheduleAppRefresh()

            // Async work — task completes when closure returns
            await fetchLatestContent()
        }
        // Handle background URLSession events
        .backgroundTask(.urlSession("com.yourapp.downloads")) {
            // Called when background URLSession completes
            await processDownloadedFiles()
        }
    }
}

SwiftUI 的优势:

  • 闭包返回时会隐式完成任务(无需调用 setTaskCompleted)
  • 原生支持 Swift Concurrency
  • 到期时会自动取消任务

模式 4:BGContinuedProcessingTask(iOS 26+)

适用场景:用户明确发起的工作(点击按钮),需要在应用进入后台后继续运行并显示进度。

不适用于:自动任务、维护、同步

// 1. Info.plist — use wildcard for dynamic suffix
// BGTaskSchedulerPermittedIdentifiers:
// "com.yourapp.export.*"

// 2. Register WHEN user initiates action (not at launch)
func userTappedExportButton() {
    BGTaskScheduler.shared.register(
        forTaskWithIdentifier: "com.yourapp.export.photos"
    ) { task in
        let continuedTask = task as! BGContinuedProcessingTask
        self.handleExport(task: continuedTask)
    }

    // Submit immediately
    let request = BGContinuedProcessingTaskRequest(
        identifier: "com.yourapp.export.photos",
        title: "Exporting Photos",
        subtitle: "0 of 100 photos"
    )

    // Optional: Fail if can't start immediately
    request.strategy = .fail  // or .enqueue (default)

    do {
        try BGTaskScheduler.shared.submit(request)
    } catch {
        showError("Cannot export in background right now")
    }
}

// 3. Handler with mandatory progress reporting
func handleExport(task: BGContinuedProcessingTask) {
    var shouldContinue = true

    task.expirationHandler = {
        shouldContinue = false
    }

    // MANDATORY: Report progress (tasks with no progress auto-expire)
    task.progress.totalUnitCount = 100
    task.progress.completedUnitCount = 0

    Task {
        for (index, photo) in photos.enumerated() {
            guard shouldContinue else { break }

            await exportPhoto(photo)

            // Update progress — system shows this to user
            task.progress.completedUnitCount = Int64(index + 1)
        }

        task.setTaskCompleted(success: shouldContinue)
    }
}

要点:

  • 动态注册(在用户操作时注册,而不是在应用启动时注册)
  • 必须报告进度——没有进度更新的任务会自动过期
  • 用户可以在系统界面中查看任务并取消
  • 仅当工作能立即启动时才有用,应使用 .fail 策略

模式 5:beginBackgroundTask — 短时关键工作

适用场景:应用即将进入后台,需要 ~30 秒完成关键工作(保存状态、完成上传)。

var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid

func applicationDidEnterBackground(_ application: UIApplication) {
    // Start background task
    backgroundTaskID = application.beginBackgroundTask(withName: "Save State") { [weak self] in
        // Expiration handler — clean up and end task
        self?.saveProgress()
        if let taskID = self?.backgroundTaskID {
            application.endBackgroundTask(taskID)
        }
        self?.backgroundTaskID = .invalid
    }

    // Do critical work
    saveEssentialState { [weak self] in
        // End task as soon as done — DON'T wait for expiration
        if let taskID = self?.backgroundTaskID, taskID != .invalid {
            UIApplication.shared.endBackgroundTask(taskID)
            self?.backgroundTaskID = .invalid
        }
    }
}

要点:

  • 工作完成后应立即调用 endBackgroundTask(不要只在过期处理程序中调用)
  • 未能结束任务可能导致系统终止应用,并影响应用今后的启动
  • 最长约 30 秒,且不保证一定能获得
  • 用于保存状态,而非持续运行的工作

模式 6:Background URLSession

适用场景:大型下载/上传,即使应用终止后也应继续进行。

// 1. Create background configuration
lazy var backgroundSession: URLSession = {
    let config = URLSessionConfiguration.background(
        withIdentifier: "com.yourapp.downloads"
    )
    config.sessionSendsLaunchEvents = true  // App relaunched when complete
    config.isDiscretionary = true  // System chooses optimal time

    return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()

// 2. Start download
func downloadFile(from url: URL) {
    let task = backgroundSession.downloadTask(with: url)
    task.resume()
}

// 3. Handle app relaunch for session events (AppDelegate)
func application(_ application: UIApplication,
                 handleEventsForBackgroundURLSession identifier: String,
                 completionHandler: @escaping () -> Void) {

    // Store completion handler — call after processing events
    backgroundSessionCompletionHandler = completionHandler

    // Session delegate methods will be called
}

// 4. URLSessionDelegate
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    // All events processed — call stored completion handler
    DispatchQueue.main.async {
        self.backgroundSessionCompletionHandler?()
        self.backgroundSessionCompletionHandler = nil
    }
}

func urlSession(_ session: URLSession,
                downloadTask: URLSessionDownloadTask,
                didFinishDownloadingTo location: URL) {
    // Move file from temp location before returning
    let destinationURL = getDestinationURL(for: downloadTask)
    try? FileManager.default.moveItem(at: location, to: destinationURL)
}

要点:

  • 工作会交给系统守护进程(nsurlsessiond)——即使应用终止后也会继续
  • 对非紧急任务设置 isDiscretionary = true(系统会等待 WiFi 或充电时再执行)
  • 应用重新启动时必须处理 handleEventsForBackgroundURLSession
  • 立即移动已下载的文件——代理方法返回后,临时位置中的文件会被删除

模式 7:静默推送通知触发

适用场景:服务器需要唤醒应用以获取新数据。

服务器载荷
{
    "aps": {
        "content-available": 1
    },
    "custom-data": "fetch-new-messages"
}

为提高能效,请使用 apns-priority: 5(而非 10)。

App 处理程序
func application(_ application: UIApplication,
                 didReceiveRemoteNotification userInfo: [AnyHashable: Any],
                 fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {

    Task {
        do {
            let hasNewData = try await fetchLatestData()
            completionHandler(hasNewData ? .newData : .noData)
        } catch {
            completionHandler(.failed)
        }
    }
}

要点:

  • 静默推送会受到频率限制 — 不要指望每次推送都会启动应用
  • 系统会合并多个推送(14 次推送可能只触发 7 次启动)
  • 每次启动都会消耗预算,预算会在一天内逐渐补充
  • 每次启动的运行时间约为 30 秒

有关静默推送通知模式(content-available 负载、节流限制、APNs 设置),请参阅 axiom-push-notifications。


Swift 6 取消集成

使用结构化并发时,将 BGTask 过期桥接到任务取消:

func handleAppRefresh(task: BGAppRefreshTask) {
    // Create a Task that respects expiration
    let workTask = Task {
        try await withTaskCancellationHandler {
            // Your async work
            try await fetchAndProcessData()
            task.setTaskCompleted(success: true)
        } onCancel: {
            // Called synchronously when task.cancel() is invoked
            // Note: Runs on arbitrary thread, keep lightweight
        }
    }

    // Bridge expiration to cancellation
    task.expirationHandler = {
        workTask.cancel()  // Triggers onCancel block
    }
}

// Checking cancellation in your work
func fetchAndProcessData() async throws {
    for item in items {
        // Check if we should stop
        try Task.checkCancellation()

        // Or non-throwing check
        guard !Task.isCancelled else {
            saveProgress()
            return
        }

        try await process(item)
    }
}

要点:

  • withTaskCancellationHandler 可在任务挂起时处理取消
  • 如果任务已取消,Task.checkCancellation() 会抛出 CancellationError
  • 使用 Task.isCancelled 进行不抛出错误的检查
  • 取消采用协作式机制 — 你的代码必须进行检查并作出响应

测试后台任务

模拟器限制

后台任务不会在模拟器中自动运行。你必须手动触发它们。

LLDB 调试命令

应用在调试器连接的情况下运行时,暂停执行并运行:

// Trigger task launch
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.yourapp.refresh"]

// Trigger task expiration (test expiration handler)
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.yourapp.refresh"]
测试工作流
  1. 在任务处理程序中设置断点
  2. 运行应用,使其进入后台
  3. 在调试器中暂停
  4. 运行 _simulateLaunchForTaskWithIdentifier 命令
  5. 恢复执行 — 应命中断点
  6. 使用 _simulateExpirationForTaskWithIdentifier 测试过期
测试清单
  • 模拟后是否命中任务处理程序断点?
  • 模拟后是否调用过期处理程序?
  • 是否在所有代码路径中都调用了 setTaskCompleted?
  • 是否能在真机上运行(而不仅仅是在模拟器中)?
  • 是否能在发布版本中运行(而不仅仅是在调试版本中)?
  • 应用是否未从应用切换器中划掉?

7 项调度因素

摘自 WWDC 2020-10063「揭秘后台执行」:

因素描述影响
电量极低电量 <20%暂停所有非必要工作
低电量模式由用户启用限制后台活动
应用使用情况用户启动应用的频率使用越频繁,优先级越高
应用切换器应用是否仍然可见?划掉应用 = 不会在后台运行
后台应用刷新系统设置关闭 = 不会运行 BGAppRefresh 任务
系统预算能源/数据预算随启动而消耗,并在一天内补充
频率限制系统设定的间隔防止启动过于频繁
应对系统限制
// Check Low Power Mode
if ProcessInfo.processInfo.isLowPowerModeEnabled {
    // Reduce background work
}

// Listen for changes
NotificationCenter.default.publisher(for: .NSProcessInfoPowerStateDidChange)
    .sink { _ in
        // Adapt behavior
    }

// Check Background App Refresh status
let status = UIApplication.shared.backgroundRefreshStatus
switch status {
case .available:
    break  // Good to schedule
case .denied:
    // User disabled — prompt to enable in Settings
case .restricted:
    // Parental controls or MDM — can't enable
}

审核清单

注册清单
  • Info.plist 中的标识符是否与代码完全一致(区分大小写)?
  • 是否启用了正确的后台模式(fetch、processing)?
  • 是否在 didFinishLaunchingWithOptions 中、返回之前完成注册?
  • 是否避免多次注册同一个标识符?
  • 处理程序闭包是否避免强引用 self?
调度清单
  • 是否在主队列或后台队列上调度(如果对性能敏感)?
  • earliestBeginDate 是否没有设置得太晚(最多约 1 周)?
  • 是否处理了 submit() 错误?
  • 是否避免调度重复任务(检查 getPendingTaskRequests)?
处理程序清单
  • 是否在处理程序开始时立即设置过期处理程序?
  • 是否在所有代码路径中都调用了 setTaskCompleted(success:)?
  • 是否安排了下一个任务(用于持续执行模式)?
  • 长时间运行的操作是否逐步保存进度?
  • 过期处理程序是否确实取消了正在进行的工作?
生产就绪情况
  • 是否在真机上测试过,而不仅仅是在模拟器中?
  • 是否在发布版本中测试过,而不仅仅是在调试版本中?
  • 是否在启用低电量模式的情况下测试过?
  • 是否在从应用切换器中强制退出后测试过(此时不应运行)?
  • 控制台日志是否显示预期的“Task completed”消息?

压力场景

场景 1:“只需在后台每隔 30 秒轮询一次服务器”

容易想到的做法:“轮询比推送通知简单。我们需要实时更新。”

实际情况:

  • iOS 不会允许你每隔 30 秒在后台运行一次
  • BGAppRefreshTask 根据用户行为模式运行,而不是按你的计划运行
  • 如果用户很少打开应用,任务可能每天运行一次,甚至更少
  • 轮询会迅速耗尽配额,导致总启动次数减少

耗时对比:

  • 实现轮询:30 分钟(效果不会符合预期)
  • 弄清楚为什么行不通:花 2~4 小时调试
  • 实现正确的推送通知:3~4 小时

实际可行的方案:

  • 静默推送通知(由服务器触发,而非轮询)
  • 使用 BGAppRefreshTask 预测用户行为(并非实时)
  • 使用 BGProcessingTask 执行可延迟的工作(夜间)

回应模板:“iOS 后台执行不支持轮询间隔。BGAppRefreshTask 会根据 iOS 对用户何时打开我们应用的预测来运行,而不是按固定计划运行。要实现实时更新,我们需要服务器端推送通知。我来给你看看 Apple 的相关文档。”


场景 2:“我的任务需要 5 分钟,而不是 30 秒”

容易想到的做法:“我只要使用 beginBackgroundTask,然后完成所有工作就行。”

实际情况:

  • beginBackgroundTask:最长约 30 秒
  • BGAppRefreshTask:约 30 秒
  • BGProcessingTask:可运行数分钟,但仅在设备充电时运行
  • 没有 API 能保证你获得与前台相当的 5 分钟运行时间

实际可行的方案:

  1. 将工作拆分成小块——拆成 30 秒的部分,并保存进度
  2. 使用 BGProcessingTask,并设置 requiresExternalPower = true(夜间运行)
  3. iOS 26+:使用 BGContinuedProcessingTask 执行用户发起的工作

回应模板:“iOS 会限制后台运行时间,以保护电池。对于需要数分钟的工作,我们有两个方案:(1)BGProcessingTask 在设备充电时于夜间运行,非常适合维护工作;(2)将工作拆分成可在 30 秒内完成的小块,并在每次运行之间保存进度。哪种方案更适合我们的使用场景?”


场景 3:“在我的设备上能运行,但用户那里不行”

容易想到的做法:“代码没问题——一定是用户设备的问题。”

实际情况:连接 Xcode 运行的调试版本,与实际环境中的发布版本表现不同。

常见原因:

  1. 启用了低电量模式——会限制后台活动
  2. 关闭了后台 App 刷新——可能是用户设置或家长控制所致
  3. 将应用从多任务界面划掉——会终止所有后台任务
  4. 配额已耗尽——近期启动次数过多
  5. 很少使用该应用——系统会降低其优先级

调试步骤:

  1. 在启动时检查 backgroundRefreshStatus 并记录结果
  2. 记录任务的计划和完成时间
  3. 使用 MetricKit 监控生产环境中的后台启动情况
  4. 询问用户:“你是否在多任务界面中强制退出了应用?”

回应模板:“后台执行取决于 7 个系统因素,包括电池电量、用户使用应用的模式,以及用户是否强制退出了应用。我来添加日志,以了解受影响用户遇到的情况。”


场景 4:“先发布,以后再添加后台任务”

容易想到的做法:“后台工作只是锦上添花。”

实际情况:

  • 用户期望打开应用时内容是最新的
  • 能在后台刷新的竞品使用起来会显得响应更快
  • 之后再添加后台任务,需要谨慎安排注册时机
  • 内容过时会给用户留下糟糕的第一印象,影响留存率

耗时对比:

  • 现在添加 BGAppRefreshTask:1~2 小时
  • 之后再补上并进行适当测试:4~6 小时
  • 调试“为什么不起作用”的问题:还需要额外时间

最低可行的后台功能:

// In didFinishLaunchingWithOptions
BGTaskScheduler.shared.register(
    forTaskWithIdentifier: "com.yourapp.refresh",
    using: nil
) { task in
    task.setTaskCompleted(success: true)  // Placeholder
    self.scheduleRefresh()
}

回应模板:“对于[应用类型],后台刷新是用户的基本预期。最低限度的实现只需 20 行代码。如果我们不实现就发布,以后再添加,可能会遇到注册时机方面的 bug。我来先添加基础代码,这样我们就能在发布后继续完善。”


真实案例

示例 1:任务从未运行 — 标识符不匹配

症状:submit() 执行成功,但处理程序从未被调用。

诊断:

// Code uses:
BGTaskScheduler.shared.register(
    forTaskWithIdentifier: "com.myapp.Refresh",  // Capital R
    ...
)

// Info.plist has:
// "com.myapp.refresh"  // lowercase r

修复:标识符必须完全匹配(区分大小写)。

浪费的时间:花了 2 小时调试代码逻辑,最后发现问题只是拼写错误。


示例 2:任务终止 — 缺少 setTaskCompleted

症状:处理程序运行,工作看似已完成,但下一个计划任务从未运行。

诊断:

func handleRefresh(task: BGAppRefreshTask) {
    fetchData { result in
        switch result {
        case .success:
            task.setTaskCompleted(success: true)  // ✅ Called
        case .failure:
            // ❌ Missing setTaskCompleted!
            print("Failed")
        }
    }
}

修复:在所有代码路径中调用 setTaskCompleted,包括出错时。

case .failure:
    task.setTaskCompleted(success: false)  // ✅ Now called

影响:未调用 setTaskCompleted 可能导致系统减少应用的后台预算。


示例 3:开发环境正常,生产环境异常 — 强制退出

症状:用户反馈后台同步无法运行。开发者无法复现。

诊断:

User: "I close my apps every night to save battery."
Developer: "How do you close them?"
User: "Swipe up in the app switcher."

实际情况:从 App 切换器中向上轻扫 = 强制退出 = 在用户再次打开应用之前,不会运行后台任务。

修复:

  1. 向用户说明(并不理想)
  2. 接受这是 iOS 的行为
  3. 确保应用重新打开时能提供良好的首次启动体验

示例 4:BGProcessingTask 从未运行 — 缺少电源要求

症状:已计划 BGProcessingTask,但始终未执行。

诊断:用户夜间将手机接入电源,但任务设置了 requiresExternalPower = true,而用户使用的是无线充电器。

等等,这不是问题所在。真正的问题是:

let request = BGProcessingTaskRequest(identifier: "com.app.maintenance")
// Missing: request.requiresExternalPower = true

如果不设置 requiresExternalPower,系统仍会等待充电,但确定性较低。显式设置该属性可以向系统提供明确的信号。

此外:用户必须在最近约 2 周内曾在前台启动应用,处理任务才有资格运行。


快速参考

LLDB 调试命令
// Trigger task
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"IDENTIFIER"]

// Trigger expiration
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"IDENTIFIER"]
控制台筛选条件
subsystem:com.apple.backgroundtaskscheduler
任务类型摘要
需求使用方式运行时间
保持内容最新BGAppRefreshTask约 30 秒
执行繁重维护BGProcessingTask + requiresExternalPower数分钟
继续执行用户发起的任务BGContinuedProcessingTask(iOS 26)较长
在进入后台时完成任务beginBackgroundTask约 30 秒
大型下载后台 URLSession按需
由服务器触发静默推送通知约 30 秒

资源

WWDC:2019-707、2020-10063、2022-10142、2023-10170、2025-227

文档:/backgroundtasks/bgtaskscheduler、/backgroundtasks/starting-and-terminating-tasks-during-development

技能:axiom-background-processing-ref、axiom-background-processing-diag、axiom-energy、axiom-push-notifications


最后更新:2025-12-31 平台:iOS 13+、iOS 26+(BGContinuedProcessingTask) 状态:可用于生产环境的后台任务模式

评论

支持 Markdown