本文主要内容来自WWDC 2019: Advances in App Background Execution
Apple 中很多后台执行都是用户从前台进入后台之后,依然保持了一段时间的活跃,最常见的比如使用UIApplication.shared.beginBackgroundTask()
来申请更长的代码执行时间,同时不同的线程后台申请执行任务的时间也不同,这一点在我的另一篇文章中亦有深入的探索。
在iOS 13.0之后,Apple出了新的框架 BackgroundTasks
,这个和前者是有很大的不同的,那就是它并不会从前台到后台之后立马执行,而是会规划后台任务执行的时间,系统自动选择合适的时间来执行该任务,比如手机充电或者闲置的时候。
它一共提供了两个Task来执行,分别是 BGProcessingTask
和 BGAppRefreshTask
。
纵观iOS的后台任务的机制,基本可以分为两类,一类是立即执行的后台任务,比如从前台到后台申请后台执行时间完成前台任务、收到后台推送处理内容等等,这一类是立即执行后台任务的类型,还有一类就是延时执行的后台任务,由系统选择合适的时间来执行任务。
立即执行的后台任务
App如何进入立即执行后台任务的状态呢?也就是立即进入 Background 状态,一般是两种方式:
- App请求:App想完成某些任务比如下载等等,所以向系统申请后台执行时间
- 事件触发:App需要执行后台任务来响应某些事件,比如消息推送等等
下面以Message App为例,它涉及到诸多场景都是这种立即执行后台任务的情况。
Send Messages
当服务器响应很慢的时候,用户可能发送了消息之后就将手机锁屏了,这种情况需要去确保消息在后台状态下也可以成功发送。 这种在后台完成前台的任务还有一些场景,比如保存文件到磁盘中、完成用户请求等等。
这种在前台进入后台后需要额外的时间来执行任务的场景需要使用 beginBackgroundTask(expirationHandler:)
方法,如果app是在Extension中运行的话,那就需要使用 ProcessInfo.performExpiringActivity(withReason:using:)
方法,代码实例如下:
func send(_ message: Message) {
let sendOperation = SendOperation(message: message)
var identifier: UIBackgroundTaskIdentifier!
identifier = UIApplication.shared.beginBackgroundTask(expirationHandler: {
sendOperation.cancel()
postUserNotification("Message not sent, please resend")
// Background task will be ended in the operation's completion block below
})
sendOperation.completionBlock = {
UIApplication.shared.endBackgroundTask(identifier)
}
operationQueue.addOperation(sendOperation)
}
注意 beginBackgroundTask
和 endBackgroundTask
需要成对使用。也有可能在系统分配到时间内依然无法完成改任务,那么这个时候就会执行 expirationHandler
,在这里将做失败处理,在样例代码中发送了一条本地通知,提醒用户消息并未成功发送!
Phone Calls
当有人给你打电话的时候需要向用户呈现来电提醒,这个场景使用了 VoIP push notifications
这个API,这是一种特殊的推送可以启动App,来让用户接听电话,需要在PK推送注册中注册VoIP类型:
func registerForVoIPPushs() {
self.voipRegistry = PKPushRegistry(queue: nil)
self.voipRegistry.delegate = self
self.voipRegistry.desiredPushTypes = [.voIP]
}
但是在2019年有一个新的改进,那就是在 didReceiveIncomingPush
回调中必须使用 CallKit
框架来报告来电,**不然系统将停止杀死App。**如果一直无法处理该通知,那么系统可能在接收到 VoIP 推送之后再也不会启动App了。那么新的改变如下:
let provider = CXProvider(configuration: providerConfiguration)
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
if type == .voIP {
if let handle = payload.dictionaryPayload["handle"] as? String {
let callUpdate = CXCallUpdate()
callUpdate.remoteHandle = CXHandle(type: .phoneNumber,
value: handle)
let callUUID = UUID()
provider.reportNewIncomingCall(with: callUUID,
update: callUpdate) { _ in
completion()
}
establishConnection(for: callUUID)
}
}
}
有几点需要注意的细节:
- 推送中带有足够的来电人的信息,可以用来展示UI界面。
- 将
apns-expiration
推送设置为0或者很小的值,这样来电之后的通知也会是相关通话的通知,而不是几分钟甚至更久之后,来电结束了才收到通知。 - 使用标准推送(standard push),不用全屏推送,无需在呼叫UI中全屏展现通知。
- 还可以使用 Notification Service Extension 来修改内容。
Muted Threads (静音群组)
像微信一样,有多个联系人,以及群聊的时候,有时用户不想让某些群聊的消息有提醒,但是进入App中之后又想要立即查看信息,只是不想每次都震动设备并收到通知。为了达到这一点,我们需要 Background Pushes 后台推送机制。这个机制可以告诉设备有新数据可用而无需提醒用户。
这就需要设置推送 content-available: 1
, 而不是 alert
, sound
, 或者 badge
,从而实现静默推送的目的,系统收到通知之后会选择一个合适的时间来启动app来下载相关内容,时间线如下:
同时后台推送功能增加了一些新的机制:
apns-priority = 5
,必须优先级设置为5,不然系统无法后台启动app。- 比如设置
apns-push-type = background
,这对 watchOS 是必须的,但是Apple建议所有平台对于后台静默推送都采用这种方式。
Download Past Attachments(下载之前的附件)
如果用户在一台新的设备上登录了它的账户,需要立即下载回话列表以及最近的消息记录,但是对玉一些很老的内容,如果可以在设备充电或者闲置时下载的话,何必在前台下载呢?所以这就需要推迟后台执行下载的时间,实现方式是 Discretionary Background URL Session
。
let config = URLSessionConfiguration.background(withIdentifier: "com.app.attachments")
let session = URLSession(configuration: config, delegate: ..., delegateQueue: ...)
// 设置系统自主性:依据性能来决定开始时间
config.discretionary = true
// 设置时间间隔
config.timeoutIntervalForResource = 24 * 60 * 60
config.timeoutIntervalForRequest = 60
// 创建请求
var request = URLRequest(url: url)
request.addValue("...", forHTTPHeaderField: "...")
let task = session.downloadTask(with: request)
// 设置请求安排的最早时间,这里是两小时后
task.earliestBeginDate = Date(timeIntervalSinceNow: 2 * 60 * 60)
// 设置预计的工作量
task.countOfBytesClientExpectsToSend = 160
task.countOfBytesClientExpectsToReceive = 4096
task.resume()
延时的后台任务
以上都是一些立即执行的后台任务,接下来要介绍的不是马上就执行的后台任务,而是延迟执行的后台任务,会在设备闲置或者充电的时候统一来进行任务的处理:
Background Processing Task的特点
注意,这里是的数据来自Apple的WWDC视频,所以应该要相信它的准确性。
-
系统会在合适的时候分配几分钟的运行时间
- 执行可推迟的可维护性工作:同步数据、备份、本地数据库清理等等
- Core ML的训练等等
-
对于计算密集型的操作,可以关掉 CPU 的监控使后台任务充分利用硬件性能
其实这就是为了后台进行模型训练来特意整出来的!!!
-
在前台申请过,那么在后台就可以执行
在使用 BGProcessingTaskRequest
时有几个属性需要注意:
-
requiresNetworkConnectivity
如果在执行后台任务的时候需要使用网络,而不仅仅是本地的操作,那属性就要设置为 true 。
-
requiresExternalPower
后台任务执行计算密集型的操作的时候,想要取消 CPU 的监控,可以设置改属性为 true 来实现这一点。
Background App Refresh Task的特点
新的API,后台刷新任务。
-
该任务提供30秒的运行时间
-
用于获取新内容使App保持最新的数据状态
-
后台刷新任务执行的时机取决于用户使用App的方式
如果用户在早中晚使用App,那么它可以在使用之前启动该App的后台任务来获取最新数据。
使用频率不高的情况:会在启动之前,调用后台刷新任务
还有一点要注意的是使用新的API之后,不要使用旧的API了,旧的API已经被废弃了:
UIApplication.setMinimumBackgroundFetchInterval(_:)
UIApplicationDelegate.application(_:performFetchWithCompletionHandler:)
使用BackgroundTasks的原理
App以及它的Extension都可以创建 BGTask
,并将其提交给 BGTaskScheduler
,它是一个全局的管理后台任务的进程,它会在合适的时候选择执行相应的Task,唤醒App并在后台启动它执行对应的任务,完成任务之后,需要调用 setTaskCompleted
方法,将任务标记为完成并挂起App。
同时Extension提交的任务只会唤醒主App,也就是说 BackgroundTask
永远由主App来执行。 系统也可能选择后台启动App来同时执行多个任务,但是系统只会按照每次启动来分配一定的时间来同时执行任务,并不会按照任务来单独分配时间。
使用Background Task的流程
这个在Apple的文档中讲述的非常得清楚:****Using background tasks to update your app。**主要是有几个重点的步骤:
- 在项目的
capabilities
中开启想要的后台任务:BGAppRefreshTask
以及BGProcessingTask
- 在Target的Info中添加
[BGTaskSchedulerPermittedIdentifiers](https://developer.apple.com/documentation/bundleresources/information_property_list/bgtaskschedulerpermittedidentifiers)
中相应的identifier字符串来标识task,后续需要在代码中注册
- 使用设置好的Identifier注册
BGTaskScheduler
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let feedVC = (window?.rootViewController as? UINavigationController)?.viewControllers.first as? FeedTableViewController
feedVC?.server = server
PersistentContainer.shared.loadInitialData()
// MARK: Registering Launch Handlers for Tasks
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.example.apple-samplecode.ColorFeed.refresh", using: nil) { task in
// Downcast the parameter to an app refresh task as this identifier is used for a refresh request.
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.example.apple-samplecode.ColorFeed.db_cleaning", using: nil) { task in
// Downcast the parameter to a processing task as this identifier is used for a processing request.
self.handleDatabaseCleaning(task: task as! BGProcessingTask)
}
return true
}
- 在合适的时机提交相应的Request
func applicationDidEnterBackground(_ application: UIApplication) {
scheduleAppRefresh()
}
// MARK: - Scheduling Tasks
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.example.apple-samplecode.ColorFeed.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // Fetch no earlier than 15 minutes from now
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app refresh: \(error)")
}
}
另外还有一些额外需要注意的点:
-
不要设置
earliestBeginDate
太远,最好在一周之内 -
确保在设备被锁住的时候,依然可以访问文件
FileProectionType.completeUntilFirstUserAuthentication
-
UIScene apps较为特殊,需要用到
UIApplication.requestSceneSessionRefresh(_:)
API -
BGTaskScheduler.submit
为了使用的简洁是设置为一个阻塞的同步调用,所以如果要在启动的时候提交,那么应该在 background queue 中使用, 而非 main queue。
总结:后台任务如何选择?
既然上面已经描述了这么多的后台任务,那么究竟该如何选择呢?以及是几种常见的场景下的选择,具体Case来自Apple的官方文档:Choosing Background Strategies for Your App。
1、在后台继续前台任务
使用[beginBackgroundTask(withName:expirationHandler:)]
申请时间继续执行前台的任务。
2、延迟执行计算密集型工作
使用 [BGProcessingTask]
,由系统来决定最佳的任务执行时间点。
3、更新App中的内容
如果App是周期性的从服务器拉取数据,那就可以使用 [BGAppRefreshTask]
,由系统选择最佳的任务执行时间点,并且这种方式可以提供最多30秒的后台执行时间。
4、使用后台推送唤醒App
使用后台推送在后台静默唤醒App,不涉及 alert、sound 以及 badge。这个上述已经介绍过了,就不赘述了。
5、使用后台推送通知用户
如果app需要在后台执行任务,并且还要向用户展示通知,那么可以使用 Notification Serverce Extension
。在收到推送通知之后,这个 service extension
会被唤醒,并且通过 [didReceive(_:withContentHandler:)]
来申请后台执行时间。
当 extension 完成任务之后,它必须调用 content handler
闭包来处理给用户的内容。extension 的执行时间也是有限的。
问题:App想长时间在后台运行怎么办?
在Apple官方文档中Preparing your UI to run in the background中总结了App在进入后台之后还可以执行任务的几种情况:
- Audio communication using AirPlay, or Picture in Picture video.
- Location-sensitive services for users.
- Voice over IP.
- Communication with an external accessory.
- Communication with Bluetooth LE accessories, or conversion of the device into a Bluetooth LE accessory.
- Regular updates from a server.
- Support for Apple Push Notification service (APNs).
而如果想一直在后台运行,那就需要持续的在后台执行任务,占据系统资源,一般来说有以下三种情况:
- 播放音频或者视频
- 后台持续定位
- 连接Bluetooth LE accessories
要注意的是,这三种情况都需要在开发的时候设置 Background Modes。
引用
[1] Apple 文档 Preparing your UI to run in the background
[2] Apple 文档 Choosing Background Strategies for Your App
[3] Apple 文档 Using background tasks to update your app
[4] WWDC 2019: Advances in App Background Execution