3、有限长度后台任务:beginBackgroundTask方法详解、expirationHandler的使用、任务结束后的清理工作
好,咱们进入第三个核心知识点。
前面我们聊了后台任务的整体框架,也知道了iOS给我们的时间其实很有限。那具体怎么申请这段时间?怎么保证任务不被系统暴力杀死?任务超时了怎么办?
这一节,我把这些细节掰开揉碎讲清楚。
3.1 beginBackgroundTask:申请时间的入口
说白了,beginBackgroundTask(withName:expirationHandler:) 就是向系统喊一声:“嘿,我有个事没干完,再给我点时间。”
系统听到后,会给你一个 后台任务标识符(UIBackgroundTaskIdentifier)。这个标识符很重要,后面结束任务时要用它来“还债”。
我个人的习惯是,在开始一个可能被中断的耗时操作前,立刻调用这个方法。比如文件下载、数据同步、数据库写入等。
来看一个最基础的调用示例:
// 声明一个属性来存储任务标识符
var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
// 开始后台任务
backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "数据同步任务") {
// 这里是 expirationHandler,我们稍后细讲
UIApplication.shared.endBackgroundTask(self.backgroundTaskID)
self.backgroundTaskID = .invalid
}
// 执行你的耗时操作
syncData()
嗯,这里要注意:beginBackgroundTask 必须在主线程调用。虽然苹果文档没强制,但我在项目中遇到过在子线程调用导致崩溃的情况。所以,保险起见,放在主线程。
3.2 expirationHandler:系统给你的“最后通牒”
为什么要有 expirationHandler?
你想想看,系统给你后台时间,但你不能无限占用。当时间快用完时,系统会调用这个闭包,告诉你:“兄弟,赶紧收拾东西,我要杀进程了。”
这个闭包是 强制性的。你不处理?那系统会在超时后直接终止你的应用,而且不会给你任何保存数据的机会。
我曾经在一个项目中,因为忘记在 expirationHandler 里结束任务,导致用户数据丢失。那次教训让我养成了一个习惯:每次写 beginBackgroundTask,立刻把 expirationHandler 写好。
正确的做法是这样的:
backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "下载任务") {
// 1. 停止当前正在进行的操作
self.downloadTask?.cancel()
// 2. 保存当前进度(如果有必要)
self.saveProgress()
// 3. 标记任务已过期
self.isTaskExpired = true
// 4. 结束后台任务
UIApplication.shared.endBackgroundTask(self.backgroundTaskID)
self.backgroundTaskID = .invalid
}
expirationHandler 里不要做耗时操作!系统只给你几秒钟的时间来清理。如果你在里面又开了一个网络请求,那大概率会 crash。
另外,expirationHandler 不一定在主线调用,所以 UI 操作要小心。
3.3 任务结束后的清理工作
任务正常完成了,或者被 expirationHandler 强制结束了,都需要做清理。
清理的核心就两个动作:
- 调用
endBackgroundTask(_:):告诉系统我完事了,你可以回收时间了。 - 将标识符重置为
.invalid:避免后续误用。
我见过不少新手犯的错误:只调用了 beginBackgroundTask,却忘了调用 endBackgroundTask。结果就是系统一直认为你有任务在执行,最终导致应用被杀死。
来看一个完整的生命周期管理示例:
class DataSyncManager {
private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
func startSync() {
// 申请后台时间
backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "数据同步") {
// 超时处理
self.cleanup()
}
// 执行同步
performSync { [weak self] success in
guard let self = self else { return }
// 同步完成,清理
self.cleanup()
}
}
private func cleanup() {
// 1. 取消所有未完成的操作
// 2. 保存状态
// 3. 结束后台任务
if backgroundTaskID != .invalid {
UIApplication.shared.endBackgroundTask(backgroundTaskID)
backgroundTaskID = .invalid
}
}
}
💡 我的小技巧:
我会把清理逻辑封装成一个独立的方法(比如上面的 cleanup()),这样无论是正常结束还是超时结束,都调用同一个方法,避免遗漏。
3.4 几个容易踩的坑
| 常见问题 | 后果 | 解决方案 |
|---|---|---|
| 忘记调用 endBackgroundTask | 应用被系统杀死 | 在 expirationHandler 和任务完成回调中都调用 |
| 在 expirationHandler 里做耗时操作 | 应用卡死或崩溃 | 只做必要的保存和清理,不要开新任务 |
| 多次调用 beginBackgroundTask | 消耗系统资源,可能被拒绝 | 确保每次 begin 都有对应的 end |
| 在子线程调用 beginBackgroundTask | 偶发崩溃 | 强制在主线程调用 |
3.5 实际项目中的最佳实践
我个人习惯在 AppDelegate 里维护一个后台任务的管理器。这样所有后台任务的状态都集中管理,方便调试。
另外,我会在 applicationDidEnterBackground 里检查当前是否有未完成的任务。如果有,就申请后台时间继续执行;如果没有,就直接返回。
func applicationDidEnterBackground(_ application: UIApplication) {
// 检查是否有未完成的任务
guard hasPendingTasks else { return }
// 申请后台时间
backgroundTaskID = application.beginBackgroundTask(withName: "后台清理") {
// 超时处理
self.forceCleanup()
}
// 执行清理
performCleanup()
}
嗯,这里还要提醒一点:不要过度依赖后台任务。iOS 给的时间很有限(大约30秒),如果你需要更长时间,就得考虑使用 URLSession 的后台下载或者 BGTaskScheduler。这些我们后面章节会详细讲。
总结一下这一节的核心:
beginBackgroundTask是申请后台时间的入口,必须配对使用endBackgroundTaskexpirationHandler是系统给你的最后机会,一定要处理- 清理工作要彻底,标识符要重置为
.invalid - 封装清理逻辑,避免重复代码
下一节,我们会深入 URLSession 的后台下载配置,那才是真正能处理大文件下载的利器。