mcpField
B
background-processing

background-processing

github.com/dpearson2699开发1.2k3.9k 次安装13 周前更新
代码仓库

关于 background-processing

面向 iOS 开发者的后台处理技能,讲解使用 BackgroundTasks、后台 URLSession 和后台推送安排与执行任务,并涵盖配置、取消处理及常见问题。

主要功能

  1. 01配置 BGTaskScheduler 与任务标识符
  2. 02安排刷新和处理任务
  3. 03使用持续处理任务执行前台启动工作
  4. 04通过后台 URLSession 下载
  5. 05用后台推送触发内容更新
  6. 06处理过期、取消与任务完成

适用场景

  1. 01为应用安排周期性数据刷新
  2. 02在设备空闲时执行维护或数据处理
  3. 03让下载在应用暂停或终止后继续
  4. 04收到静默推送后获取新内容

安装

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

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill background-processing

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

附带文件evals/evals.jsonreferences/background-task-patterns.md

详细介绍

后台处理

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

目录

Info.plist 配置

每个任务标识符都必须在 Info.plist 中的 BGTaskSchedulerPermittedIdentifiers 下声明,否则 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>       
    <string>processing</string>  
</array>

在 Xcode 中:target > 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 = default background queue
        ) { 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("Could not schedule app refresh: \(error)")
    }
}

func handleAppRefresh(task: BGAppRefreshTask) {
    // Schedule the next refresh before doing work
    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)
        }
    }

    // CRITICAL: Handle expiration -- system can revoke time at any moment
    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("Could not schedule processing task: \(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+

与 BGAppRefreshTask 和 BGProcessingTask 不同,此任务会立即从前台启动。系统可能会因资源压力而终止任务,并优先终止报告进度最少的任务。请设置 expirationHandler 以处理用户或系统取消,取消正在进行的工作,并在报告完成前清理部分输出。

import BackgroundTasks

func startExport() {
    // Register the task handler at app launch, not here.
    // BGTaskScheduler requires registration before app launch completes.
    let jobID = UUID().uuidString
    let request = BGContinuedProcessingTaskRequest(
        identifier: "com.example.app.export.\(jobID)",
        title: "Exporting Photos",
        subtitle: "Processing 247 items"
    )
    // Use a permitted base wildcard identifier: com.example.app.export.*
    // earliestBeginDate is ignored for continued processing requests.
    // .queue: begin as soon as possible if can't run immediately
    // .fail: fail submission if can't run immediately
    request.strategy = .queue

    do {
        try BGTaskScheduler.shared.submit(request)
    } catch {
        print("Could not submit continued processing task: \(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)

        // Update the user-facing title/subtitle
        task.updateTitle(
            "Exporting Photos",
            subtitle: "\(index + 1) of \(items.count) complete"
        )
    }

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

对于 GPU 工作,请检查是否受支持,并启用 Background GPU Access(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
    ) {
        // Move file from tmp before this method returns
        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("Download failed: \(error)") }
    }
}

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

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

// In URLSessionDelegate — call stored handler when events finish:
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: background 和 apns-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 标识符
// DON'T: Submit a task whose identifier isn't in BGTaskSchedulerPermittedIdentifiers
let request = BGAppRefreshTaskRequest(identifier: "com.example.app.refresh")
try BGTaskScheduler.shared.submit(request)  // Throws .notPermitted

// DO: Add every identifier to Info.plist BGTaskSchedulerPermittedIdentifiers
// <string>com.example.app.refresh</string>
2. 未调用 setTaskCompleted(success:)

使用上面的标准应用刷新或处理程序:每条成功、失败和取消路径都要恰好报告一次完成状态。

3. 忽略过期处理程序

使用同一标准处理程序取消正在进行的工作,并在 expirationHandler 中报告失败。

4. 安排任务过于频繁

安排任务部分规定了最早启动时间规则。避免以分钟为间隔请求刷新;实际启动时间仍由系统决定。

5. 过度依赖后台运行时间
// DON'T: Start a 10-minute operation assuming it will finish
func handleRefresh(task: BGAppRefreshTask) {
    Task { await tenMinuteSync() }
}

// DO: Design work to be incremental and cancellable
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 中列出所有任务标识符
  • 已启用必需的 UIBackgroundModes(fetch、processing)
  • 在应用启动完成前注册任务
  • 在每条代码路径上调用 setTaskCompleted(success:)
  • 设置 expirationHandler 并取消正在进行的工作
  • 在处理程序内安排下一个任务(重新安排模式)
  • earliestBeginDate 使用合理的时间间隔,并将其视为提示
  • 后台 URLSession 使用代理(而非 async/闭包)
  • 在 didFinishDownloadingTo 中于返回前移动后台 URLSession 文件
  • handleEventsForBackgroundURLSession 保存并调用完成处理程序
  • 后台推送载荷包含 content-available: 1
  • 后台推送 APNs 请求使用 apns-push-type: background 和 apns-priority: 5
  • 及时调用 fetchCompletionHandler 并传入正确的结果
  • BGContinuedProcessingTask 通过 ProgressReporting 报告进度
  • 工作采用增量方式且可安全取消(Task.checkCancellation())
  • 任务处理程序中不执行会阻塞的同步工作

参考资料

评论

支持 Markdown