BackgroundTasks实践

BackgroundTasks 框架包含两种后台任务,一种是用于后台更新应用程序内容的 BGAppRefreshTask,一种是用于执行繁重任务比如模型训练、数据同步等的 BGProcessingTask。 ^a71227

由于两种后台任务的开启方式大同小异,所以接下来的内容主要以 BGAppRefreshTask 来进行。

1. 开启后台任务的前期准备

为了让应用程序能够支持后台任务执行,首先要让应用程序支持后台任务。选中应用程序对应的 target,在 Capability 一栏中搜索 Background Modes 并添加(如果已添加则跳过),将 Background fetch 勾选上(如果是 BGProcessingTask 则需要勾选 Background processing)。之后打开 Info.plist,添加一个新的属性 Permitted background task scheduler identifiers,这是一个数组类型,在数组种添加一个字符串类型新项目,这即是后台任务的 identifier,每个后台任务都需要有一个 identifier。identifier 的名字建议还是以 com.app.taskName 的格式来命名,此处设置为 com.demo.refresh。

2. 注册后台任务

在应用程序启动时,我们需要使用 BGTaskScheduler 的单例来注册后台任务。BGTaskScheduler 是一个用于调度在后台启动应用程序的任务请求的类。调用 register(forTaskWithIdentifier:using:launchHandler:) 注册后台任务。注册方法一共需要传递 3 个参数。
第一个是后台任务的 identifier,也即之前在 Info.plist 添加的 identifier。
第二个参数是一个线程队列,此队列必须是串行的,以保证一致的顺序,可以传 nil,系统会自动运行在一个默认的后台队列上。
第三个参数 launchHandler 会在后台任务被触发时被调用,闭包包含一个 BGTask 参数,在闭包中要设置 task 的 expirationHandler 以及要在所有任务结束后调用 setTaskCompletedWithSuccess:

需要注意的是,注册必须在应用程序启动完成之前执行,如果是在启动完成之后或是注册多个相同 identifier 的任务会报错,且注册只能在宿主应用而不是扩展中注册。

上述完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.demo.refresh", using: nil) { task in
// 因为这是一个刷新的Task,所以要强转
self.handleAppRefresh(task as! BGAppRefreshTask)
}
return true
}

private func handleAppRefresh(_ task: BGAppRefreshTask) {
// 再次启动
// scheduleAppRefresh()   
/* 执行任务 operation */
operation.finished = {
task.setTaskCompleted(success: true)
}

    task.expirationHandler = {
        // cancel refresh operation
        operation.cancel()
}
}

3. 启动后台任务

做好了前面的准备,我们终于可以启动我们的后台任务了。那么什么时候启动后台任务呢?最佳的时机应该是应用进入到后台的时候。所以在applicationDidEnterBackground 或者 sceneDidEnterBackground 中调用启动后台任务。

我们使用 BGAppRefreshTaskRequest 来创建一个后台任务请求,然后将请求提交到 BGTaskScheduler。请求有一个 earliestBeginDate 用来表示后台任务最早可以在什么时候开始,可以设置为一周以内的时间,此处我们设置了 30 分钟。

BGProcessingTaskRequest 还有两外两个参数可以设置:

  • requiresNetworkConnectivity,是否需要网络,默认为 false。如果设置为 true,则后台任务只会在联网状态下才会启动;
  • requiresExternalPower,是否需要外部电源,也就是充电状态,默认为 false。如果设置为 true 则只会在充电状态下才会启动后台任务。需要注意的是,即使设置为 false,后台任务也不一定会启动,这取决于设备和系统的状态。

启动和提交后台任务的代码如下:

1
2
3
4
5
6
7
8
9
private func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.demo.refresh")
    request.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60)
    do {
    try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app refresh: \(error)")
}
}

因为后台任务只会响应一次,所以要在接收到后台任务响应的时候再次启动才能让后台任务一直存在,也就是 handleAppRefresh 方法中注释的第一行。

4. 模拟后台任务的启动和失效

由于后台任务是由系统来控制执行时机,所以开发者无法确定相关代码什么时候会执行,这样就不利于调试,不过我们可以模拟相关事件的产生。

模拟后台任务启动,我们先将应用程序运行起来,进入后台,再进入前台,此时按下暂停,在调试窗口输入如下代码后回车:

1
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.demo.refresh"]

如果没有意外的话,控制台会输出 Simulating launch for task with identifier com.demo.refresh。这表示后台程序已经执行了。

模拟后台任务过期的代码如下,和模拟启动基本一致,就不再赘述。

1
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.demo.refresh"]

模拟后台任务的启动和过期时不能使用模拟器,必须使用真机,且要将手机设置中的 后台App 刷新 开关打开,不然模拟会报错 BGTaskSchedulerErrorCodeUnavailable

参考资料
Advances in App Background Execution
BGTaskScheduler.Error.Code