Shaula Blog

美丽的东西都是肤浅的

iOS 后台任务革新: BGTaskScheduler

在 iOS 13 之前,后台任务执行一直是个难题——开发者需要小心翼翼地在系统限制下工作,而用户则常常为电池续航担忧。苹果推出的 BGTaskScheduler 框架彻底改变了这一局面,它提供了一套系统友好、高度节能的后台任务调度机制,让应用能在后台智能执行关键任务,同时最大限度减少对设备性能和电池的影响。

一、背景:为何需要 BGTaskScheduler?

  • 电池续航至上: iOS 始终将用户体验和电池寿命放在核心位置,对后台活动限制严格。
  • 老方案的局限: 之前的 Background FetchBackground Processing (有限后台模式,例如:Silent Push Notifications) 不够灵活、可控性差,开发者难以精准调度。
  • 统一与优化: BGTaskScheduler 旨在提供一个标准化、系统管理的后台任务入口,让系统能根据设备状态(是否充电、网络状况、用户活跃度等)智能调度任务,实现全局最优。

二、核心机制:BGTaskScheduler 如何工作?

  1. 任务声明: 应用在 Info.plist 中声明它需要使用的后台任务类型 (BGAppRefreshTask, BGProcessingTask) 及其唯一标识符。确保应用启用了相应的后台模式(在 XcodeCapabilities中开启Background Modes并选择需要的选项),尤其是对于 BGProcessingTask,需要启用Background processing
  2. 任务注册: 应用启动时,向 BGTaskScheduler.shared 注册每个任务标识符对应的处理程序(一个闭包或方法)。
  3. 系统智能调度:
    • 系统根据全局因素(设备是否闲置、是否在充电、电量水平、网络状况、用户使用模式等)决定何时以及是否触发任务。
    • 系统为任务分配一个有限的时间窗口执行。
  4. 任务执行: 当系统决定运行任务时,它会调用应用预先注册的处理程序,并传入一个 BGTask 对象(具体是 BGAppRefreshTaskBGProcessingTask 实例)。
  5. 任务管理:
    • 应用在 expirationHandler 中处理系统发出的即将超时信号,进行必要的清理。
    • 任务完成后,必须调用 setTaskCompleted(success:) 告知系统。

三、两种核心任务类型详解

特性 BGAppRefreshTask BGProcessingTask
主要目的 定期获取少量最新内容 执行需要较长时间或资源的维护/处理任务
典型场景 刷新新闻源、更新天气、拉取即时消息预览 大文件上传/下载、数据库清理/重组、机器学习模型本地训练、内容索引重建
执行频率 较低(系统严格控制,通常每天几次) 很低(系统严格控制,通常仅在设备闲置充电时)
预期执行时间 非常短(通常几十秒) 较长(具体由系统决定
资源优先级 较高
触发条件 系统预测用户可能需要最新内容时 设备闲置、连接电源、良好网络时更可能触发
关键要求 快速完成,只做必要的最小数据获取/更新 需处理超时(expirationHandler),任务可中断

四、实现 BGTaskScheduler 的关键步骤(Swift 示例)

  1. Info.plist 声明:

    <key>BGTaskSchedulerPermittedIdentifiers</key>
    <array>
        <string>com.yourcompany.app.refresh</string> <!-- BGAppRefreshTask ID -->
        <string>com.yourcompany.app.cleanup</string> <!-- BGProcessingTask ID -->
    </array>
    
  2. 注册任务处理程序 (通常在 AppDelegate didFinishLaunching):

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: ...) -> Bool {
    
        // 注册 App Refresh 任务
        BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.yourcompany.app.refresh", using: nil) { task in
            guard let appRefreshTask = task as? BGAppRefreshTask else {
                task.setTaskCompleted(success: false)
                return
            }
            self.handleAppRefresh(task: appRefreshTask)
        }
    
    
        // 注册 Processing 任务
        BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.yourcompany.app.cleanup", using: nil) { task in
            guard let appProcessingTask = task as? BGProcessingTask else {
                task.setTaskCompleted(success: false)
                return
            }
            self.handleDatabaseCleanup(task: appProcessingTask)
        }
    
        // 可以在这里提交首次调度请求(可选)
        scheduleAppRefresh()
        scheduleDatabaseCleanup()
    
        return true
    }
    
  3. 提交任务请求 (在需要时,如应用进入后台或特定事件后):

    func scheduleAppRefresh() {
        let request = BGAppRefreshTaskRequest(identifier: "com.yourcompany.app.refresh")
        request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 60) // 最早1小时后执行
    
        do {
            try BGTaskScheduler.shared.submit(request)
            print("成功提交 App Refresh 任务请求")
        } catch {
            print("提交 App Refresh 请求失败: \(error)")
        }
    }
    
    func scheduleDatabaseCleanup() {
        let request = BGProcessingTaskRequest(identifier: "com.yourcompany.app.cleanup")
        request.requiresNetworkConnectivity = true // 需要网络
        request.requiresExternalPower = true      // 最好在充电时执行(非必须)
        request.earliestBeginDate = nil           // 由系统决定何时执行
    
        do {
            try BGTaskScheduler.shared.submit(request)
            print("成功提交 Database Cleanup 任务请求")
        } catch {
            print("提交 Database Cleanup 请求失败: \(error)")
        }
    }
    
  4. 实现任务处理逻辑 (核心):

    func handleAppRefresh(task: BGAppRefreshTask) {
        // 1. 设置超时处理程序(必须!清理未完成工作)
        task.expirationHandler = {
            // 立即停止所有正在进行的网络请求或操作
            NetworkManager.cancelAllRefreshRequests()
            task.setTaskCompleted(success: false) // 标记失败
        }
    
        // 2. 执行实际刷新逻辑(快速!)
        fetchLatestNews { result in
            // 3. 更新本地数据/UI (通过主线程)
            DispatchQueue.main.async {
                updateUIWithNewData()
            }
            // 4. 无论成功与否,告知系统任务完成
            task.setTaskCompleted(success: result == .success)
        }
    }
    
    func handleDatabaseCleanup(task: BGProcessingTask) {
        task.expirationHandler = {
            // 关键:保存清理进度/状态,以便下次继续或回滚
            DatabaseManager.saveCleanupState()
            task.setTaskCompleted(success: false)
        }
    
        // 执行耗时的数据库维护操作
        DatabaseManager.performMaintenance { progress, shouldStop in
            // 定期检查是否快超时
            if task.state == .cancelled || shouldStop.pointee {
                shouldStop.pointee = true // 停止操作
                return
            }
        } completion: { success in
            task.setTaskCompleted(success: success)
        }
    }
    

五、重要注意事项与最佳实践

  1. 执行时间非保证: 系统分配的时间是最大上限,实际可能更短。任务必须高效且能处理提前终止。
  2. 触发非即时: 提交请求并不等于立即执行。系统决定最佳时机,可能延迟很久甚至不触发(尤其在低电量模式)。
  3. 超时处理程序 (expirationHandler) 必须实现: 这是保证任务能优雅中断、避免资源浪费和数据损坏的关键。
  4. setTaskCompleted(success:) 必须调用: 无论成功失败,必须调用以释放资源。忘记调用会导致后续任务被限制。
  5. 任务的幂等性: 确保后台任务具备幂等性,即多次执行不会导致数据不一致或其他副作用。这在处理网络请求或数据库操作时尤为重要。
  6. 资源管理: 在执行后台任务时,合理管理资源,避免长时间占用 CPU 或内存,防止系统终止任务。
  7. 审核要求: 确保后台任务逻辑合理且对用户有价值。滥用后台更新(如频繁刷新非必要数据)可能导致审核被拒。

六、总结:BGTaskScheduler 的价值

  • 用户受益: 显著提升电池续航和设备流畅度,后台更新更智能、更不易察觉。
  • 开发者受益: 提供了标准化、可靠且节能的后台执行途径,无需再与复杂限制“斗智斗勇”。
  • 生态受益: 系统全局优化后台活动,确保所有应用都在统一、公平、节能的框架下运行。

BGTaskScheduler 代表了 iOS 后台任务管理的未来方向。通过理解并正确应用 BGAppRefreshTaskBGProcessingTask,开发者能在满足功能需求与尊重系统资源之间找到完美平衡点,最终为用户带来更流畅、更持久的 iOS 体验。