background-processing

background-processing

热门

使用 BGTaskScheduler 在 iOS 上调度和执行后台工作。适用于注册 BGAppRefreshTask 进行短时后台获取、BGProcessingTask 进行长时间维护、BGContinuedProcessingTask(iOS 26+)进行前台启动并在后台继续的工作、后台 URLSession 下载或后台推送通知。涵盖 Info.plist 配置、过期处理、任务完成以及使用模拟启动进行调试。

932Star
47Fork
更新于 2026/7/15
SKILL.md
readonly只读
name
background-processing
description

使用 BGTaskScheduler 在 iOS 上调度和执行后台工作。适用于注册 BGAppRefreshTask 进行短时后台获取、BGProcessingTask 进行长时间维护、BGContinuedProcessingTask(iOS 26+)进行前台启动并在后台继续的工作、后台 URLSession 下载或后台推送通知。涵盖 Info.plist 配置、过期处理、任务完成以及使用模拟启动进行调试。

后台处理

使用 BackgroundTasks 框架、后台 URLSession 和后台推送通知在 iOS 上注册、调度和执行后台工作。

目录

Info.plist 配置

每个任务标识符必须Info.plistBGTaskSchedulerPermittedIdentifiers 中声明,否则 submit(_:) 会抛出 BGTaskScheduler.Error.Code.notPermitted

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.example.app.refresh</string>
    <string>com.example.app.db-cleanup</string>
    <string>com.example.app.export.*</string>
</array>

同时启用所需的 UIBackgroundModes

<key>UIBackgroundModes</key>
<array>
    <string>fetch</string>       <!-- BGAppRefreshTask 需要 -->
    <string>processing</string>  <!-- BGProcessingTask 需要 -->
</array>

在 Xcode 中:目标 > Signing & Capabilities > Background Modes > 启用 "Background fetch" 和 "Background processing"。

BGTaskScheduler 注册

在应用启动完成之前注册处理程序。在 UIKit 中,在 application(_:didFinishLaunchingWithOptions:) 中注册;在 SwiftUI 中,在 App.init() 中注册。

UIKit 注册

import BackgroundTasks

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        BGTaskScheduler.shared.register(
            forTaskWithIdentifier: "com.example.app.refresh",
            using: nil  // nil = 默认后台队列
        ) { task in
            self.handleAppRefresh(task: task as! BGAppRefreshTask)
        }

        BGTaskScheduler.shared.register(
            forTaskWithIdentifier: "com.example.app.db-cleanup",
            using: nil
        ) { task in
            self.handleDatabaseCleanup(task: task as! BGProcessingTask)
        }

        return true
    }
}

SwiftUI 注册

import SwiftUI
import BackgroundTasks

@main
struct MyApp: App {
    init() {
        BGTaskScheduler.shared.register(
            forTaskWithIdentifier: "com.example.app.refresh",
            using: nil
        ) { task in
            BackgroundTaskManager.shared.handleAppRefresh(
                task: task as! BGAppRefreshTask
            )
        }
    }

    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

BGAppRefreshTask 模式

短时任务(约 30 秒),用于获取小数据更新。系统决定何时启动;earliestBeginDate 仅作为下限提示。

func scheduleAppRefresh() {
    let request = BGAppRefreshTaskRequest(
        identifier: "com.example.app.refresh"
    )
    request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
    do {
        try BGTaskScheduler.shared.submit(request)
    } catch {
        print("无法调度应用刷新:\(error)")
    }
}

func handleAppRefresh(task: BGAppRefreshTask) {
    // 在执行工作前调度下一次刷新
    scheduleAppRefresh()

    let fetchTask = Task {
        do {
            let data = try await APIClient.shared.fetchLatestFeed()
            await FeedStore.shared.update(with: data)
            task.setTaskCompleted(success: true)
        } catch {
            task.setTaskCompleted(success: false)
        }
    }

    // 关键:处理过期——系统随时可能收回时间
    task.expirationHandler = {
        fetchTask.cancel()
        task.setTaskCompleted(success: false)
    }
}

BGProcessingTask 模式

长时间任务(数分钟),用于维护、数据处理或清理。它们在设备空闲时运行,可能需要外部电源;同样适用 earliestBeginDate 下限规则。

func scheduleProcessingTask() {
    let request = BGProcessingTaskRequest(
        identifier: "com.example.app.db-cleanup"
    )
    request.requiresNetworkConnectivity = false
    request.requiresExternalPower = true
    request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 60)
    do {
        try BGTaskScheduler.shared.submit(request)
    } catch {
        print("无法调度处理任务:\(error)")
    }
}

func handleDatabaseCleanup(task: BGProcessingTask) {
    scheduleProcessingTask()

    let cleanupTask = Task {
        do {
            try await DatabaseManager.shared.purgeExpiredRecords()
            try await DatabaseManager.shared.rebuildIndexes()
            task.setTaskCompleted(success: true)
        } catch {
            task.setTaskCompleted(success: false)
        }
    }

    task.expirationHandler = {
        cleanupTask.cancel()
        task.setTaskCompleted(success: false)
    }
}

BGContinuedProcessingTask(iOS 26+)

由用户操作在前台启动并在后台继续运行的任务。系统通过 Live Activity 显示进度。遵循 ProgressReporting

可用性: iOS 26.0+,iPadOS 26.0+

BGAppRefreshTaskBGProcessingTask 不同,此任务从前台立即启动。系统可以在资源压力下终止它,优先终止报告进度最少的任务。设置 expirationHandler 以处理用户或系统取消,取消进行中的工作,并在报告完成前清理部分输出。

import BackgroundTasks

func startExport() {
    // 在应用启动时注册任务处理程序,而不是在这里。
    // BGTaskScheduler 要求在应用启动完成前注册。
    let jobID = UUID().uuidString
    let request = BGContinuedProcessingTaskRequest(
        identifier: "com.example.app.export.\(jobID)",
        title: "导出照片",
        subtitle: "正在处理 247 个项目"
    )
    // 使用允许的基础通配符标识符:com.example.app.export.*
    // earliestBeginDate 对于继续处理请求被忽略。
    // .queue:如果无法立即运行,则尽快开始
    // .fail:如果无法立即运行,则提交失败
    request.strategy = .queue

    do {
        try BGTaskScheduler.shared.submit(request)
    } catch {
        print("无法提交继续处理任务:\(error)")
    }
}

func performExport(task: BGContinuedProcessingTask) async {
    let items = await PhotoLibrary.shared.itemsToExport()
    let progress = task.progress
    progress.totalUnitCount = Int64(items.count)

    for (index, item) in items.enumerated() {
        if Task.isCancelled { break }

        await PhotoExporter.shared.export(item)
        progress.completedUnitCount = Int64(index + 1)

        // 更新面向用户的标题/副标题
        task.updateTitle(
            "导出照片",
            subtitle: "已完成 \(index + 1) / \(items.count)"
        )
    }

    task.setTaskCompleted(success: !Task.isCancelled)
}

对于 GPU 工作,检查支持并启用后台 GPU 访问(com.apple.developer.background-tasks.continued-processing.gpu):

let supported = BGTaskScheduler.supportedResources
if supported.contains(.gpu) {
    request.requiredResources = .gpu
}

后台 URLSession 下载

使用 URLSessionConfiguration.background 进行即使在应用挂起或终止后仍继续的下载。系统在进程外处理传输。

class DownloadManager: NSObject, URLSessionDownloadDelegate {
    static let shared = DownloadManager()

    private lazy var session: URLSession = {
        let config = URLSessionConfiguration.background(
            withIdentifier: "com.example.app.background-download"
        )
        config.isDiscretionary = true
        config.sessionSendsLaunchEvents = true
        return URLSession(configuration: config, delegate: self, delegateQueue: nil)
    }()

    func startDownload(from url: URL) {
        let task = session.downloadTask(with: url)
        task.earliestBeginDate = Date(timeIntervalSinceNow: 60)
        task.resume()
    }

    func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didFinishDownloadingTo location: URL
    ) {
        // 在此方法返回前将文件从临时目录移出
        let dest = FileManager.default.urls(
            for: .documentDirectory, in: .userDomainMask
        )[0].appendingPathComponent("download.dat")
        try? FileManager.default.moveItem(at: location, to: dest)
    }

    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didCompleteWithError error: (any Error)?
    ) {
        if let error { print("下载失败:\(error)") }
    }
}

处理应用重新启动——存储并调用系统完成处理程序:

// 在 AppDelegate 中:
func application(
    _ application: UIApplication,
    handleEventsForBackgroundURLSession identifier: String,
    completionHandler: @escaping () -> Void
) {
    backgroundSessionCompletionHandler = completionHandler
}

// 在 URLSessionDelegate 中——事件完成时调用存储的处理程序:
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    Task { @MainActor in
        self.backgroundSessionCompletionHandler?()
        self.backgroundSessionCompletionHandler = nil
    }
}

后台推送触发

静默推送通知会短暂唤醒您的应用以获取新内容。在推送负载中设置 content-available: 1

{ "aps": { "content-available": 1 }, "custom-data": "new-messages" }

发送 APNs 请求时使用 apns-push-type: backgroundapns-priority: 5。后台推送投递优先级较低且不保证送达;保持发送频率较低,通常每小时不超过两到三次。

在 AppDelegate 中处理:

func application(
    _ application: UIApplication,
    didReceiveRemoteNotification userInfo: [AnyHashable: Any],
    fetchCompletionHandler completionHandler:
        @escaping (UIBackgroundFetchResult) -> Void
) {
    Task {
        do {
            let hasNew = try await MessageStore.shared.fetchNewMessages()
            completionHandler(hasNew ? .newData : .noData)
        } catch {
            completionHandler(.failed)
        }
    }
}

在 Background Modes 中启用 "Remote notifications" 并注册:

UIApplication.shared.registerForRemoteNotifications()

常见错误

1. 缺少 Info.plist 标识符

// 不要:提交标识符不在 BGTaskSchedulerPermittedIdentifiers 中的任务
let request = BGAppRefreshTaskRequest(identifier: "com.example.app.refresh")
try BGTaskScheduler.shared.submit(request)  // 抛出 .notPermitted

// 要:将每个标识符添加到 Info.plist 的 BGTaskSchedulerPermittedIdentifiers 中
// <string>com.example.app.refresh</string>

2. 未调用 setTaskCompleted(success:)

使用上述规范的应用刷新或处理处理程序:每个成功、失败和取消路径都恰好报告一次完成。

3. 忽略过期处理程序

使用相同的规范处理程序在 expirationHandler 中取消进行中的工作并报告失败。

4. 调度过于频繁

调度部分拥有下限规则。避免分钟级的刷新请求;系统仍然选择实际的启动时间。

5. 过度依赖后台时间

// 不要:假设 10 分钟的操作会完成
func handleRefresh(task: BGAppRefreshTask) {
    Task { await tenMinuteSync() }
}

// 要:将工作设计为增量且可取消
func handleRefresh(task: BGAppRefreshTask) {
    let work = Task {
        for batch in batches {
            try Task.checkCancellation()
            await processBatch(batch)
            await saveBatchProgress(batch)
        }
        task.setTaskCompleted(success: true)
    }
    task.expirationHandler = {
        work.cancel()
        task.setTaskCompleted(success: false)
    }
}

审查清单

  • [ ] 所有任务标识符已列在 BGTaskSchedulerPermittedIdentifiers
  • [ ] 已启用所需的 UIBackgroundModesfetchprocessing
  • [ ] 任务在应用启动完成前注册
  • [ ] 每个代码路径都调用了 setTaskCompleted(success:)
  • [ ] 设置了 expirationHandler 并取消进行中的工作
  • [ ] 在处理程序内部调度下一个任务(重新调度模式)
  • [ ] earliestBeginDate 使用合理的间隔并视为提示
  • [ ] 后台 URLSession 使用委托(而非 async/闭包)
  • [ ] 后台 URLSession 文件在 didFinishDownloadingTo 返回前移动
  • [ ] handleEventsForBackgroundURLSession 存储并调用完成处理程序
  • [ ] 后台推送负载包含 content-available: 1
  • [ ] 后台推送 APNs 请求使用 apns-push-type: backgroundapns-priority: 5
  • [ ] 及时调用 fetchCompletionHandler 并返回正确结果
  • [ ] BGContinuedProcessingTask 通过 ProgressReporting 报告进度
  • [ ] 工作是增量且可取消的(Task.checkCancellation()
  • [ ] 任务处理程序中无阻塞同步工作

参考