对于依赖于实时信息、位置服务或与外部设备通信的 iOS App ,开发者可以用后台刷新来提高用户体验,允许 App 在后台执行任务。特别是在下载或上传大量数据时,后台执行网络请求会相当有帮助。
iOS 限制 App 在后台运行,也很有道理。如果 App 没处于活动状态,就不应该使用大量系统资源,尤其是在涉及数据传输时。但随着 App 越来越多地与后端服务连接,后台获取数据对于良好的用户体验已经变得更加重要。
不幸的是,并没有一种实现网络后台请求的最佳方式。最新的 iOS SDK 提供了很多选项,熟悉不同的后台抓取 API 有助于决定使用哪个技术。
由于不受控制的后台任务可能导致设备的电池寿命大量消耗,并且很难复现,正确使用 iOS 后台刷新 API 很关键。本文介绍了相关问题,并且介绍了一些常见的坑。
理解 iOS App 执行状态
大多数 iOS 用户都熟悉 iOS 9 中的多任务界面,双击 home 键的时候会显示最近使用的 App 列表。向上滑动会强制关闭它。但是,多任务界面里显示的 app 并不一定在执行代码或获取数据。它们可能被暂停或根本没有在运行(这长期困扰了想节省电量的 iOS 用户)。
使用 Swift, App 的执行状态可以这么获得:
UIApplication.sharedApplication().applicationState
如果状态是 active,应用在屏幕上是可见的,准备好接收事件。不可见的话可能是 background 或 inactive。苹果开发者网站上有一张很棒的全状态示意图 。
大多数开发者使用 UIApplication 里的代理方法或借助大量通知类型来响应状态的改变。Xcode 7 的 iOS 模板包含了这些用来响应改变的代理方法:
// App 准备好运行了
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool
// App 即将从活跃到非活跃状态
func applicationWillResignActive(application: UIApplication)
// 后台模式刚被激活
func applicationDidEnterBackground(application: UIApplication)
// App 现在可见了,并可以接收事件
func applicationDidBecomeActive(application: UIApplication)
// App 即将终止
func applicationWillTerminate(application: UIApplication)
默认情况下,当应用进入后台时,没什么值得兴奋的——它只是在 app 被暂停之间的短暂过渡而已。甚至可以禁用后台状态(但苹果不鼓励这么做 )。尽管后台刷新有几种不同的使用情境,包括和蓝牙设备的通信、播放音频等,但许多应用使用后台刷新来下载东西。
使用 NSURLSession 在后台下载和上传
当 App 需要上传或下载数据时,如果用户发送短信和切换到其它应用,操作最好继续。幸运的是,当应用程序变得不活动时,NSURLSession
类可以移交下载和上传到操作系统。与几乎所有后台执行 API 一样,如果用户从多任务界面强行退出,后台操作会终止。(注意如果 App 在追踪位置,用户强退了,它会重新启动。)
要使 NSURLSession
具有后台能力,需要实例化有后台初始化方法和标识符(重用于所有后台会话)的 NSURLSessionConfiguration
对象:
let sessionConfig = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("com.newrelic.bgt")
如果一个 App 特别礼貌,在 NSURLSessionConfiguation
中有一个标志称为“discretionary”,允许 iOS 优化性能的请求,因此在某些情况下(如电池电量不足时不好的连接),请求不会真正发生。
backgroundSessionConfig.discretionary = true
只要应用程序发出 HTTP 或 HTTPS 请求,那么 NSURLSession
需要使用配置对象和委托来实例化,以便在下载或上传完成时接收通知。 这里 有一些其它限制:
let session = NSURLSession(configuration: backgroundSessionConfig, delegate: self, delegateQueue: NSOperationQueue.mainQueue())
例如为了下载静态 PDF 文件,具有后台配置的会话可以在标准下载任务中使用:
let downloadTask = session.downloadTaskWithURL(NSURL(string: "https://try.newrelic.com/rs/newrelic/images/nr_getting_started_guide.pdf")!)
downloadTask.resume()
当操作完成或者有错误时,NSURLSession
委托方法会被调用。会有一个磁盘上的临时文件的路径,可以打开以读取或移动到另一个位置。
关于 NSURLSession
的最后一点:它从 iOS 9 开始支持 HTTP/2。关于使用API 的更多细节可以在 苹果的开发者网站 上获得。
选择机会下载东西
在 iOS 7 里,苹果添加了对后台抓取的支持——智能、每个 App 都有机会被唤醒。没有办法强制后台抓取在指定的时间执行。iOS 在调度未来的执行时会检查早之前的后台抓取中使用的数据和电池用量。
添加支持要编辑应用程序的 property list(参阅 UIBackgroundModes
)并在App 生命周期的早期设置获取间隔:
application.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalMinimum)
当 iOS 决定开始后台抓取时,会调用此 UIApplicationDelegate
方法:
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void)
这个方法有大约 30 秒时间将一个 UIBackgroundFetchResult
返回给 completionHandler
函数,然后 App 就会终止。UIBackgroundFetchResult
的值用于确定何时再次调用后台抓取委托方法。如果在特定时间频繁需要数据(例如,清晨的新闻 App),这有助于 iOS 了解何时执行后台抓取:
enum UIBackgroundFetchResult : UInt { case NewData case NoData case Failed}
后台抓取也可以由远程推送通知触发,并且具有非常类似的委托方法,带有相同的 completion handler。
要在 iOS 模拟器中测试后台抓取事件,Xcode 在 Scheme 编辑器中有一个“Launch due to background fetch event”选项,并在 debug menu 下有“Simulate Background Fetch”项。
在 2016 年初,开发者发现是用 iOS 模拟器测试后台抓取会有问题,所以最好是 clean 之后把 App 安装在真机上。
此外,Xcode 调试器改变了操作系统挂起应用程序的方式,并且还可能在 测试非活动状态时出现问题 。在没有连接调试器的设备上进行测试(正如用户和 App 交互一样)有时是唯一可靠地再现某些状态的方法。
App 终止的特殊情况
用户在多任务界面强退可能是出现不可复现的崩溃的根源。如果 App 被杀死且没有任何通知的话就可能发生。例如,App 被挂起了,但系统由于内存不足而终止了它,就不会发送任何通知。只有 iOS 想要终止未暂停并处于后台状态的 App 时,才会调用 applicationWillTerminate
。
在 iOS 9 中,App 不应该依赖于 applicationWillTerminate:
的调用。最好在 applicationDidEnterBackground:
中保存状态并执行清理。
然而,重要的是 applicationWillTerminate
被调用的时候清理和终止所有正在运行的后台任务,因为如果 iOS 必须强制杀死正在运行的后台任务,可能会导致崩溃。这有时是难以复现的 bug 的来源。
出于性能和电池寿命的考虑,iOS 限制了后台的时间量。在后台执行状态中剩余的时间量可从以下获取:
UIApplication.sharedApplication().backgroundTimeRemaining
backgroundTimeRemaining
的数量并不总是正确。强制退出将停止任何后台任务,无论剩余多少时间。
关于执行状态的代理方法总是被调用(甚至是按照特定顺序)的假设实际上也并不一定。仔细检查建设一个执行状态总是发生在另一个状态之前的代码。
总结
当编写在后台执行的 iOS 代码时:
- 确定要使用哪个后台刷新 API。对于需要很多秒才能完成的网络请求,NSURLSession 会很有帮助。使用 iOS 提供的机会性后台抓取代理对于需要按计划获取内容的 app 会很有帮助。
- 远程推送通知可以是触发后台刷新的有效机制。
Log 执行状态的变更,在有和没有连接调试器的真机上测试,小心模拟器带来的奇怪问题。是用开源的 iOS logging 库,例如 CocoaLumberjack 或 XCGLogger 会很有帮助。 - 访问钥匙串或使用 iOS 数据保护功能时要小心。后台刷新可能发生在锁屏时,可能导致读写受保护的资源出现问题。
- 高性能后台代码很关键:iOS 会优先处理前台的 App,严格限制 App 完成后台任务的资源和时间。
随着移动数据使用量的增加和新的 iOS 9 功能(如 iPad 上的多任务处理拆分视图),管理应用执行状态对于构建高质量应用程序非常重要——App 打开时持续不断的进度指示条肯定会让用户很烦。后台刷新是苹果对开发人员的妥协,旨在平衡用户体验与使用数据网络和高网络延迟时导致的电池消耗。利用后台抓取 API 保持信息最新,并注意避免常见的坑,这有助于满足用户对 App 始终快速且永不崩溃的期望。