iOS 在后台下载

文档
app在后台时会被暂停,暂停的apps会提高电池的使用寿命,并且会让系统将重要的系统资源投入到引起用户注意的前台应用中。
大部分apps可以容易地进入到挂起的状态,但是也有一些app需要在后台是保持运行状态。运动的app 想要跟踪用户在一段时间内的位置以便于他可以在地图上显示用户的路径;音乐app在锁屏时需要继续播放音乐;其他的app可能需要在后台做下载或上传操作。当你发现你的app需要在后台运行时,iOS可以帮助你高效地那样做:

  • 如果在前台时已经开启了一些小的任务,当程序进入后台时可以请求到一些时间保持app运行处理这些任务;
  • 在前台开启了下载或上传任务的app可以把那些任务的管理提交到系统,因此即使app在后台暂停了或杀掉了,任务依然可以继续进行;
  • 需要在后台运行以支持特定类型任务的应用app可以声明对后台执行模式的支持。
    尽量避免app在后台工作,除非这样能提升整体的用户体验。用户可能启动了别应用或者锁住了屏幕来让你的应用进入了后台,无论哪种情况,用户都表示你的应用现在不需要做任何有用的工作。在后台继续运行你的app只会消耗电池和系统资源。

执行有限长度的任务

进入后台的app希望尽快地进入到静止的状态以便于系统可以将它挂起,如果app正在处理任务并且需要一些额外的时间来在后台完成这个任务,可以调用- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithName:(nullable NSString *)taskName expirationHandler:(void(^ __nullable)(void))handler方法或者- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler:(void(^ __nullable)(void))handler方法来请求一些额外的时间来处理任务。调用这两个方法中的任意一个会临时地推迟app的挂起,从而给app一些额外的时间处理任务。处理完任务后,你的app必须调用- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)identifier方法来让系统知道app已经完成了任务并且可以被挂起。
每一次调用- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithName:(nullable NSString *)taskName expirationHandler:(void(^ __nullable)(void))handler方法或者- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler:(void(^ __nullable)(void))handler方法会生成唯一的token关联到对应的任务里。当app结束任务时,它必须调用- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)identifier使用对应的token让系统知道任务完成了。对于一个后台任务来说,调用- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)identifier失败将导致应用程序的终止。如果在启动任务时提供了一个过期处理,系统将调用该处理程序并且给予你做后的机会来结束这个任务并且避免app被终结。
下面的代码显示了当你的app进入后台时如何开启一个后台任务。
在这个例子中,开启后台任务的请求包括了一个过期的处理以免这个任务耗时过长。之后把这个任务提交到了异步执行的并发队列里以保证applicationDidEnterBackground:方法可以正常返回。block的使用简化了维持变量的引用的代码,例如后台任务的标示。bgTask变量是一个存储了指向当下后台任务标示的指针的类的成员变量,并且在方法中使用之前已经初始化。

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    bgTask = [application beginBackgroundTaskWithName:@"MyTask" expirationHandler:^{
        // Clean up any unfinished task business by marking where you
        // stopped or ending the task outright.
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
 
    // Start the long-running task and return immediately.
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 
        // Do the work associated with the task, preferably in chunks.
 
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    });
}

在后台下载

在下载文件时,我们应该使用NSURLSession来开启下载。因为它可以让系统来控制下载的进程,即使app被挂起后或在后台被系统杀掉也不会受到影响。当你配置了一个NSURLSession对象用于后台下载时,系统会在一个独立的进程里管理那些下载,并且会把下载的状态反馈到你的app里。如果在下载过程中你的app在后台被系统杀掉了,系统会继续在后台里下载,并且在下载完成时系统会重新启动你的app。
为了支持后台下载,你必须适当地配置你的NSURLSession对象。
首先要创建一个NSURLSessionConfiguration对象并且设置一些属性:

  • 创建NSURLSessionConfiguration 对象使用方法+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier
  • 设置这个对象的sessionSendsLaunchEvents属性值为YES;
  • 如果你的app在前台时开启了下载,推荐你把discretionary属性设置为YES;
  • 根据需要适当地配置这个对象的其他属性;
  • 使用这个对象创建NSURLSession
    配置完之后,NSURLSession对象会在合适的时间无缝地把上传或下载的任务提交到系统。如果任务完成时你的app仍在运行中(在前台或者在后台),这个 NSURLSession对象通常会通知他的代理。如果这个上传或下载的任务还没有完成时系统杀掉了你的app,系统在后台里会继续自动地管理这个任务。如果是用户杀掉了这个app,系统会取消任何未完成的任务。
    当所有关联在后台session里的任务完成后,系统会重启已经被杀掉的app(假定sessionSendsLaunchEvents设置为了YES并且用户没有主动杀掉app)并且会调用Appdelegate的- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(nonnull NSString *)identifier completionHandler:(nonnull void (^)())completionHandler方法。在这个代理方法的声明里,使用identifier来创建一个和之前的配置一样的NSURLSession对象,系统回答新的NSURLSession对象和之前的任务重新连接起来并且在session的代理里面报告任务的状态。

实现常运行的任务

对于那些需要更多执行时间来实现的任务,你必须请求特定的权限来使app在后台运行而不会被挂起。在iOS里,只有特定的应用类型被允许在后台运行。

  • 在后台为用户播放音频内容的app,例如音乐播放器;
  • 在后台录制音频内容的应用;
  • 让用户一直知道自己位置的应用,比如导航的app;
  • 支持网络电话(VoIP)的应用;
  • 需要有规律的下载和处理新内容的应用;
  • 收到来自与外部附件有规律的更新的应用。
    实现这些服务的程序必须要声明它所支持的服务并且使用系统的框架来实现那些服务的相关方面。

Declaring Your App’s Supported Background Tasks

支持某些后台运行的类型必须在使用它们的app里提前声明。在xcode5及更高的版本中,你从你的工程设置的Capabilities tab里声明你的app支持的后台模式。使能后台模式的选项会把UIBackgroundModes key添加到你的app的Info.plist文件里。下表列出了你可以指定的后台模式以及在你的Info.plist文件里xcode分配给UIBackgroundModes key的值。

Xcode background mode UIBackgroundModes values Description
Audio and AirPlay audio app在后台为用户播放语音或录制音频(这个内容包括了使用AirPlay的流式音频或视频)。在第一次使用之前用户必须授予app使用麦克风的权限,有关详情,请参阅 Supporting User Privacy
Location updates location 即使app在后台运行时,也会一直通知用户地理位置
Voice over IP voip app提供给了用户使用网络打电话的能力
Newsstand downloads newsstand-content 该应用是一个在后台下载和处理杂志或报纸内容的报亭应用。
External accessory communication external-accesory app同硬件配件一起工作,该配件需要通过External Accessory framework来定期地提供更新
Uses Bluetooth LE accessories bluetooth-central app同蓝牙配件一同工作,该配件需要通过Core Bluetooth framework定期地提供更新
Acts as a Bluetooth LE accessory bluetooth-periphral app支持通过Core Bluetooth framework在外设模式下的蓝牙通信。使用这个模式需要用户授权,详细信息,请参阅Supporting User Privacy
Background fetch fetch app定期从网络下载和处理少量的内容
Remote notifications remote-notification 当一个推送通知到达时,app想要开始下载内容。使用这个通知来最小化显示推送相关内容的延迟

每个模式都让系统知道你的app再合适的时间被唤醒或启动,用来响应相关的事件。例如,一个开始播放音乐然后移动到后台app仍然需要时间来填充音频的缓冲区。启用Audio模式来告诉系统框架仍它们将继续以适当的间隔对app进行适当的回调。如果app没有选择这个模式,当app移动到后台时,任何被app播放或录制的音频会被停止。
追踪用户的位置
有几种方法可以再后台里追踪用户的位置,大部分不需要app在后台里一直运行。

  • The significant-change location service (推荐)
  • Foreground-only location services (仅限前台的定位服务)
  • Background location services (后台定位服务)

对于不需要高精度定位数据的app强烈推荐使用The significant-change location service,使用这个服务,只有当用户的位置发生重大改变时位置更新才会被生成。因此,对于社交的app或者对于用户来说位置信息不重要的app这种服务是理想的。当位置更新发生时,如果app处于挂起状态,系统会在后台唤醒app来处理更新。如果app开启了这个服务但是被杀掉了,当新位置更新时系统会自动地重启app。这个服务在iOS4及更高版本中可用,并且仅仅在包含了蜂窝无线电的设备上可以使用。
仅限前台的定位服务和后台的定位服务都使用了标准的Core Location service来检索位置数据。唯一的不同即使当app挂其时,仅在前台服务会停止传输位置更新。仅在前台定位服务适用于那些仅需要在前台定位的app。
你可以从xcode工程的Capabilities tab里的Background modes里打开定位支持。你也可以打开这个支持从app的Info.plist文件里使用UIBackgroundModes key和位置值。打开这个模式不会阻止系统挂起app,但它会告诉系统无论什么时候有新的位置更新时它都会唤醒app。因此,这个key高效地让app在后台运行来来处理位置更新。
鼓励您使用标准的服务或者The significant-change location service。定位服务需要主动使用iOS设备的无线电硬件。运行这个硬件可能连续不断地消耗大量的电力。如果你的app不需要为用户提供连续不断的和精准的位置信息,最好减少定位服务的使用。
在app中如何使用哪种定位服务,请参阅Location and Maps Programming Guide

播放和录制后台音频
持续播放或者录制音频的app(即使这个app运行在后台)可以注册用来在后台做那些任务。可以再Xcode工程的Capabilities tab里的Background modes打开音频支持。你也可以打开这个支持通过在info.plist文件里添加UIBackgroundModes key和音频值。在后台播放音频内容的app必须播放听得见的音频。
后台音频app的典型示例包括:

  • 音乐播放app
  • 录音app
  • 支持通过airplay播放音频和视频的app
  • VoIP app

当UIBackgroundModes key包含了音频值的时候,系统的媒体框架自动地阻止app在后台时被挂起。只要app正在播放音频、视频或者正在录制音频,app在后台就能继续运。然而,如果播放或者录制音频停止了,系统会挂起app。
你可以使用任何系统音频框架来处理后台的音频内容。并且使用这些框架的过程不会改变。对于通过AirPlay的视频播放,你可以使用Media Player或AV Foundation框架来呈现你的视频。由于在播放媒体文件时你的app不会被挂起,因此当你的app在后台时毁掉也能正常地运行。但是在你的回调中,你仅仅需要做的必要的工作时提供回放的数据。例如,流式音频app需要从服务器下载音乐流数据并且并将当下的语音流推出做回放。app不应该做任何与播放无关的任务。

实现VoIP app
VoIP app 可以让用户通过网络连接打电话来代替蜂窝服务。这样的app需要稳固的网络连接来使它可以收到呼入和其他相关的数据。并不是保持VoIP设备一直运行,系统允许他们被挂起并且提供了设备来监听它们的socket。当检测到传入流量时,系统会唤醒VoIP app并且把socket的控制权返回给他。
要配置VoIP app,你必须做下列的事情:

  • 在Xcode工程的Capabilities tab里打开对Voip的支持,你也可以通过在info.plist里添加UIBackgroundModes key和VoiP的值来打开这个支持。
  • 配置其中一个应用程序的套接字用于VoIP的使用。
  • 在进入后台之前,调用- (BOOL)setKeepAliveTimeout:(NSTimeInterval)timeout handler:(void(^ __nullable)(void))keepAliveHandle方法来安装需要定期执行的程序。你的app可以使用这个处理来维持与服务器的连接。
  • 配置音频会话以处理活动使用的转换。

在UIBackgroundModes key中包含VoIP的值让系统知道它将允许app在后台运行以管理它的网络socket。使用了这个key的app也会在系统启动后立即在后台重新启动以确保VoIp服务总是可用的。
大部分的VoIP app也需要被配置成后台音频app用来在后台时传输音频。因此,你因该在UIBackgroundModes里包含audio和voip的值。如果你不这样做,你的app在后台时不能播放和录制音频。更多的关于UIBackgroundmodes key的信息,请参阅nformation Property List Key Reference
关于实现VoiP应用需要采取的步骤的一些特殊的信息,请参阅Tips for Developing a VoIP App

在后台获取用户的注意力

通知是一种获取用户注意的方式对于那些被挂起,在后台或者没有运行的app。app可以使用本地通知来显示alerts,播放声音,icon的badge,或者是这三个的组合。例如,一个闹钟app可以使用本地通知来显示一个alert并且播放闹钟的声音。当一个通知被传递给用户时,用户必须确定这个信息是否授权把app带回到前台。如果app已经运行在前台,本地通知会静静地传递给app。
为了安排本地通知的传递,需要创建UILocalNotification的实例,配置这个通知的参数,并且使用UIApplication的方法安排它。本地通知对象包含了关于要传输的通知的类型(声音,alert,badge)和传输它的时间。UIApplication的方法提供了立即传递通知或者在计划的时间传递。
下面显示了一个使用用户的设定的日期和时间来安排一个闹钟。这个例子配置了唯一的一个闹钟,并且在安排新的之前取消了之前的闹钟。你自己的app在任意的时间内可以有不超过128个本地通知处于活跃状态。其中的任何一个都可以在指定的间隔里重复。在闹钟出发时如果app不在运行或者在后台时,这个通知会由警告框和语音文件组成。如果app是活跃的,这个app的代理方法- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification会被调用。

- (void)scheduleAlarmForDate:(NSDate*)theDate
{
    UIApplication* app = [UIApplication sharedApplication];
    NSArray*    oldNotifications = [app scheduledLocalNotifications];
 
    // Clear out the old notification before scheduling a new one.
    if ([oldNotifications count] > 0)
        [app cancelAllLocalNotifications];
 
    // Create a new notification.
    UILocalNotification* alarm = [[UILocalNotification alloc] init];
    if (alarm)
    {
        alarm.fireDate = theDate;
        alarm.timeZone = [NSTimeZone defaultTimeZone];
        alarm.repeatInterval = 0;
        alarm.soundName = @"alarmsound.caf";
        alarm.alertBody = @"Time to wake up!";
 
        [app scheduleLocalNotification:alarm];
    }
}

被用于本地通知的音频文件和用于推送通知的那些有一样的要求。自定义的音频文件必须被放置在app的main bundle里并且支持下列格式中的一种:Linear PCM, MA4, µ-Law, or a-Law。你也可以指定UILocalNotificationDefaultSoundName来播放默认的alert声音。当一个通知被发送并且播放声音时,系统也会出发设备震动。
你可以取消安排的通知或者获取一个通知的列表通过使用UIApplication的方法。关于那些方法的信息,请参阅 UIApplication Class Reference。关于配置本地通知的额外信息,请参阅Local and Remote Notification Programming Guide

理解你应用程序在后台启动

支持后台运行的app可能会被系统重启来处理收到的事件,如果系统被某些原因杀掉了但不是用户强制的,当下列的事件发生时app会被重启:

  • 定位app:
    • 系统收到了满足app的配置的传输标准的位置更新;
    • 设备进入或退出注册区域。 (地区可以是地理区域或iBeacon地区。)
  • 对于音频app,音频框架需要app处理一些数据。音频app包括哪些播放auido或使用麦克风的。
  • 蓝牙app:
    • 扮演中心角色的app收到了连接的外围设备的数据。
    • 扮演了外围设备角色的app收到了连接的中心的命令。
  • 后台下载的app:
    • app收到了推送通知并且通知的payload包含了content-avaaliablekey的值为1.
    • 系统在机会性时刻唤醒应用程序,开始下载新内容。
    • 对于在后台使用NSURLSession下载的app。所有关联在session对象里的任务成功完成或收到错误时。
    • 由报亭应用程序启动的下载完成。
      在大部分情况下,系统不会重启那些被用户杀掉的app,一个例外是定位的app在iOS8或更高的版本中被用户杀掉后会重启。在其他情况下,用户必须先明确的启动app或重启设备,app才能够在后台自动的被系统重启。当设备上使用密码保护时,在用户解锁设备前系统不会在后台启动app。

做可靠地后台app

当涉及到系统资源和硬件时,前台的app总是优先于后台的app。在后台运行的APP需要为这个差异做好准备并调整行为。具体来说,在后台的app应当遵守下面的准则:

  • 不要在代码中做任何OpenGL ES的调用。在后台时,你不能创建EAGLContext对象或者发出任何类型的OpenGL ES的绘图命令。使用那些调用会引起你的app立刻被杀掉。app还必须确保任何之前的被提交的命令在进入后台之前被完成。关于如何在进入后台时处理OpenGL ES,请参阅 Implementing a Multitasking-aware OpenGL ES Application in OpenGL ES Programming Guide for iOS
  • 再挂起之前取消任何Bonjour相关的服务。当你的app进入后台并且在挂起之前,他应当从Bonjour注销并且关闭监听与任何网络服务相关的sockets。挂起的app无法响应传入的服务请求。关闭哪些服务阻止他们在不可用时显示成可用。如果你没有关掉Benjour服务,当你的app挂起时系统会自动关掉那些服务。
  • 准备处理基于网络的sockets的连接失败。当app优于任何原因而被挂起时,系统可能会终端sockets连接。只要你的基于sockets的代码被准备与其他类型的网络故障,例如丢失信号或网络转换,这不应该导致任何异常的问题。当app恢复时,如果它在使用sockets时遭遇了故障,只需要重新建立连接。

推荐阅读更多精彩内容