1.Background Tasks:APP 在前台时启动某项任务,然后在未结束之前突然 切换到了后台,那么 APP 可以在切换回调里使用某些 API 来继续向系统请求一些时间来继续完成这个任务;完成之后通知系统,之后系统会将 APP 挂起;
2.Downloading/Uploading:在后台启动从网络下载/上传文件的任务 – 对于文件下载/上传,iOS 有专门的机制;
3.Specific Backgournd Tasks:应用需要在后台一直执行代码;
Apple 文档建议,如果要启动一个后台任务(异步任务),可以使用 API beginBackgroundTaskWithExpirationHandler
来指定,即使启动任务的时候,程序是处在前台的,也没有关系,当位于前台时,该方法请求得到的时间是DBL_MAX
,也就是 double
数据类型最大值,你可以认为是无限大,当任务执行过程中 APP 被切换到后台时,任务还没有完成,这个时间又会自动调整为一个时间片段(具体多少我没找到文档说明,都是说可以通过backgroundTimeRemaining
属性得到)。需要注意的是, 这个方法是成对使用的,对于一个固定 task ,每次调用beginBackgroundTaskWithExpirationHandler
,都会产生一个token
值(UIBackgroundTaskIdentifier
实际是个整型),必须在任务执行结束时,调用 endBackgroundTask
并传递这个token
,来结束后台任务。另外,作为最佳实践,都应该传递一个 超时 handler
,以防申请到的时间片段内,还是没能完成任务的话,做最后的清理和标注工作!如果不传的话,那么结果就是 iOS 直接 kill 掉你的APP,闪退.
下面是一段代码例子
// 在某处定义一个 token 变量
UIBackgroundTaskIdentifier _bgTaskToken;
// 进入后台 委派方法回调
- (void)applicationDidEnterBackground:(UIApplication *)application
{
_bgTaskToken = [application beginBackgroundTaskWithName:@"MyTask" expirationHandler:^{
// 时间到了,任务还没完成,只能清理
...
// 取消后台任务
[application endBackgroundTask:_bgTaskToken];
_bgTaskToken = UIBackgroundTaskInvalid;
}];
// 异步启动任务,这样不会阻塞 本委派方法回调
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 巴拉巴拉,做自己的任务
...
// 任务在时间限制内结束啦,取消后台任务
[application endBackgroundTask:_bgTaskToken];
_bgTaskToken = UIBackgroundTaskInvalid;
});
}
ios13新增了一个API模块:BackgroundTasks可以更轻松的管理后台任务,该框架区分了两大类背景任务:
应用程序刷新任务:短命的任务,使应用程序一整天都保持最新。
背景处理任务:用于执行可推迟的维护任务的长期任务 详细的API可参考BackGround Tasks
下面是一段代码例子
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
private let server: Server = MockServer()
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
}
func applicationDidEnterBackground(_ application: UIApplication) {
scheduleAppRefresh()
scheduleDatabaseCleaningIfNeeded()
}
// 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)")
}
}
func scheduleDatabaseCleaningIfNeeded() {
let lastCleanDate = PersistentContainer.shared.lastCleaned ?? .distantPast
let now = Date()
let oneWeek = TimeInterval(7 * 24 * 60 * 60)
// Clean the database at most once per week.
guard now > (lastCleanDate + oneWeek) else { return }
let request = BGProcessingTaskRequest(identifier: "com.example.apple-samplecode.ColorFeed.db_cleaning")
request.requiresNetworkConnectivity = false
request.requiresExternalPower = true
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule database cleaning: \(error)")
}
}
// MARK: - Handling Launch for Tasks
// Fetch the latest feed entries from server.
func handleAppRefresh(task: BGAppRefreshTask) {
scheduleAppRefresh()
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
let context = PersistentContainer.shared.newBackgroundContext()
let operations = Operations.getOperationsToFetchLatestEntries(using: context, server: server)
let lastOperation = operations.last!
task.expirationHandler = {
// After all operations are cancelled, the completion block below is called to set the task to complete.
queue.cancelAllOperations()
}
lastOperation.completionBlock = {
task.setTaskCompleted(success: !lastOperation.isCancelled)
}
queue.addOperations(operations, waitUntilFinished: false)
}
// Delete feed entries older than one day.
func handleDatabaseCleaning(task: BGProcessingTask) {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
let context = PersistentContainer.shared.newBackgroundContext()
let predicate = NSPredicate(format: "timestamp < %@", NSDate(timeIntervalSinceNow: -24 * 60 * 60))
let cleanDatabaseOperation = DeleteFeedEntriesOperation(context: context, predicate: predicate)
task.expirationHandler = {
// After all operations are cancelled, the completion block below is called to set the task to complete.
queue.cancelAllOperations()
}
cleanDatabaseOperation.completionBlock = {
let success = !cleanDatabaseOperation.isCancelled
if success {
// Update the last clean date to the current time.
PersistentContainer.shared.lastCleaned = Date()
}
task.setTaskCompleted(success: success)
}
queue.addOperation(cleanDatabaseOperation)
}
}
这类后台任务,必须使用 iOS 指定的机制才可以,那就是 NSURLSession
。使用 NSURLSession
建立的下载任务,会被系统直接在另外一个独立的系统进程里进行管理,不会因 APP 进入后台或挂起等而受到影响,iOS 会统一管理所有的下载任务。并且,即使你的 APP 已经挂掉啦,下载任务还是会继续,等到下载完成啦,系统会唤起你的 APP 进程,并通知你,但如果是用户主动杀掉的你的进程,那么系统会自动取消下载任务。
具体使用方法:
NSURLSessionConfiguration
类的 backgroundSessionConfigurationWithIdentifier
方法创建一个 NSURLSessionConfiguration
对象,参数为一个字符串,作为一个token
,完成时会用到,不能为空或 nil;sessionSendsLaunchEvents
属性为 YES
;(属性解释:当后台会话中的任务完成时,允许在后台恢复 或启动应用程序 或需要验证时。这只适用于使用+backgroundSessionConfigurationWithIdentifier
创建的配置:默认值是YES。)discretionary
属性也设置为 YES
;(这个属性允是许根据系统的判断来调度后台任务,以获得最佳性能)NSURLSessionConfiguration
对象,作为参数,创建 NSURLSession
实例对象;NSURLSession
开始下载task,之后就是调用下载或上传Task代理方法,代理方法可以看 《URL Session Programming Guide》如果在下载完成之前,你的APP已经挂起或者死掉啦,那么当系统完成下载之后,系统会唤醒你的 APP,并回调 你的 app 委托方法 application:handleEventsForBackgroundURLSession:completionHandler:
,在这其中,参数会传进来一个 token
,这个就是你第一步里 传入的 字符串,使用这个 字符串,再重新创建一个 NSURLSessionConfiguration
,并进行与开始任务之前一样的配置,那么就可以使用这些对象来获取已经完成的任务的详细情况了。当后台下载或者上传任务完成之后会通过URLSessionDidFinishEventsForBackgroundURLSession
这个代理方法来通知我们.
在 iOS 里只有特定的一些应用类型才会被允许可以在后台一直运行,APP 必须显式的声明一些特定权限,才可以在后台进行长时间运行而不被挂起。
一些应用类型有 6 种:
要实现这些类型服务的 APP,需要进行专门的声明,这样系统才会采取相应的操作。
先来看看怎么声明。
通过 XCode 的 project setting
里就可以配置类型,选择之后会自动 在你 工程的 Info.plist
文件里 增加 UIBackgroundModes
键值对;一个 APP 可以同时声明多种支持的后台长期任务类型,在 XCode 里勾选上即可;
下表给出了所有 在 XCode 可选的 类型 及 具体含义;
Xcode background mode | UIBackgroundModes 值 | 描述 |
---|---|---|
Audio and AirPlay | audio | 应用可以在后台播放或录制音频,包括 Apple 自家的 AirPlay 流媒体音视频;对于录制,需要在APP 第一次运行时,用户授予权限才可进行。 |
Location updates | location | APP 不断更新 GPS 位置信息,并通知给用户,即使 APP 处于后台 |
Voice over IP | voip | APP 提供通过网络连接来打电话的功能 |
Newsstand downloads | newsstand-content | 杂志应用,可以在后台下载杂志并处理 |
External accessory communication | external-accessory | 一些外设控制 APP, 比如一些控制 第三方 MFI 配件的应用,声明这种 类型,可以让APP 在后台不断的与 外设进行沟通 |
Uses Bluetooth LE accessories | bluetooth-central | iPhone 作为蓝牙中心设备使用,也就是做为 server;需要在后台不断更新蓝牙状态的 |
Acts as a Bluetooth LE accessory | bluetooth-peripheral | iPhone 作为蓝牙外围设备使用,也就是做 client,需要在后台不断的访问其他蓝牙设备获取数据的 |
Background fetch | fetch | APP 需要在后台不断地 频繁有规律的从网络获取数据 |
Remote notifications | remote-notification | APP 先在后台关注某个 push 推送,但这个 push 推送到达的时候,及时在后台开始对应的下载任务,以尽可能减少用户直接点开 通知 后 查看内容的等待时间 |
一些典型的应用例子:
注意:手机上是有可能会有多个 APP 同时拥有后台 audio 操作权限的,这时候系统会根据 每个 APP 开始操作音频时的 audio session 配置来决定如何进行操作,而且你应该非常小心的处理一些中断事件,如来电,其他系统提示音等,这些都有相关的 API 和机制,可以参考 《Audio Session Programming Guide》
有三种方式来实现 位置的访问:
The significant-change location service
,字面理解,就是只有位置有变化时才会发出通知,有人说这个时机是依据基站,切换了基站时,就会发出一次通知,所以频率会受基站的密度影响,所以市区更新频率会比郊区高。但好处 是这个服务不管你的 APP 是在前台还是后台,不管是否已经被挂起,或已经死掉了,他都会唤醒你的进程进行相应处理,所以应该是最省电的。后两种都是标准的定位服务,只不过一个只能工作在前台,而一个可以在后台工作;
注意:官方对于使用后台定位服务的 APP 审核是非常严格的,所以使用时一定要小心,并提供足够的说明和解释。
至于如何实现一个定位 APP ,请看 《Location and Maps Programming Guide 》
网络通话软件,skype 就是其中一个。VoIP应用能够使用户通过互联网拨打和接听电话,而不是通过蜂窝移动网。VoIP应用严重依赖网络,打VoIP电话导致高的能耗就不足为奇了。当VoIP应用处于不活跃的状态时,不管怎样应用都应该彻底地空闲已到达省电的目的。
大致步骤:
杂志应用,居然还有专门的处理。但我看介绍,跟前面讲解的 后台下载文件没啥区别啊!!另外好像也是用 通知推送 触发啊。About Newsstand Kit Framework
外设设备有很多,比如一些心率监控器,会在必要的时候向手机推送数据。声明了UIBackgroundModes
为 external-accessory
后,系统就不会主动关闭 APP 与 外设之间的连接,而是替 APP 监视这个连接,当有数据过来时,会唤醒 APP 进行处理,每次唤醒 APP 只有 10 S
种时间进行数据处理,所以应当越快越好,万不得已,如果10S不够,需要使用 beginBackgroundTaskWithExpirationHandler:
方法再申请一段时间进行处理;
【Note】:Apple 要求此类应用 需要提供一个 开启 和 关闭 连接的界面供用户使用;
类似上一节的 配件,如果心率监控器跟 手机之间使用的连接方式是蓝牙,那么就一模一样啦,连 唤醒的时间限制都一样,都是 10 S
!
有人依靠这种手段来实现后台永存,但现在不好使啦,除非你是真的每次都在下载东西,而且每次时间都很短。用户的流量啊。因为声明了这个 mode 之后,并不保证 系统一定会给你分配时间来执行后台任务,因为它自己有一套逻辑,如果你经常性唤醒,但却每次都耗时很久,又没有做从网络下载东西的操作,那么以后你被分配给唤醒的几率就会越来越小。另外还有审核!
正常情况下,声明了这个类型之后,系统在你的 APP 进入后台后,会间隔性的给机会将你的 APP 唤醒,并回调你的 委托方法application:performFetchWithCompletionHandler:
,你需要在这个回调里检查是否有新内容可用,如果有,就开启后台下载,推荐使用 NSURLSession 来建立,下载完成后,你必须调用这个方法出入 的 completionHandler 并传入一个 整型值 来表示 你的处理是否正常,UI是否已经更新,让系统来决定更新 snapshot等;
这个方式,是你的应用中包含通知功能时,你在服务端推送的通知内容里加入 键值对 content-available = 1
,那么 手机收到这个通知后,会自动启动 APP 到后台,或 唤醒(依旧保持后台执行),并回调 委托方法 application:didReceiveRemoteNotification:fetchCompletionHandler:
,在这个方法里进行内容下载。
【Note】:需要服务端推送配合
当一些特定事件发生时,系统会唤醒已经被挂起的进程,转换到后台运行状态,这些事件针对不同类型的APP 有所不同:
content-available = 1
的推送通知到达了手机;background fetch
类型,系统给予了 APP 唤醒的机会;NSURLSession
进行后台下载的APP,在下载过程完成或出现问题时,系统会主动唤醒对应 APP;提示:绝大多数情况下,系统不会重启被用户手动强制关闭的 APP,但在 iOS 8 之后, location apps 是个例外。其他的所有被用户手动强制关闭的APP 都不会被系统主动唤起,直到 用户再次 主动启动这个 APP,或者手机重启并在用户输入了解锁密码之后才会恢复机制。
Apple 教育我们,如果你要实现一个后台 APP,应该做一个有责任的APP,不要乱搞。
Bonjour
相关的操作,还不清楚这个是啥东西,不过 Apple 说即使你不取消,它在把你挂起之前也会都给你取消;