iOS-Background Execution

一般情况下,App切到后台后,会被挂起。

对于,需要在后台持续运行的App,包含以下几类:

Executing Finite-Length Tasks

App如果在被切到后台时,正在执行一个任务,需要一些额外的时间来完成,可以通过以下两个方法来创建BackgroundTask

1
2
beginBackgroundTaskWithName:expirationHandler:
beginBackgroundTaskWithExpirationHandler:

创建BackgroundTask时,系统暂时不会挂起App,在结束BackgroundTask时,需要调用:

1
endBackgroundTask:

如果没有成功调用endBackgroundTask,将会导致App被终止。如果设置了ExpirationHandler,则系统会调用该handle,给最后一次机会调用endBackgroundTask,避免App被终止。

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (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;
});
}

如果需要知道还剩下多少时间可以运行,调用:

1
[[UIApplication application] backgroundTimeRemaining]

在iOS 10系统实际测试中,发现,即使bgTask被终止了,App依然不会被终止,且依然可以执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)applicationDidEnterBackground:(UIApplication *)application
{
UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithName:@"MyTask" expirationHandler:^{
NSLog(@"Game Over..");
[application endBackgroundTask:bgTask];
}];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while(true){
NSLog(@"Still Exist, Remain: %lf", [application backgroundTimeRemaining]);
[NSThread sleepForTimeInterval:1.0];
}
});
}

日志为:

1
2
3
4
5
6
7
8
9
10
11
Still Exist, Remain: 179.988507
Still Exist, Remain: 178.914609
...
Still Exist, Remain: 5.492562
Game Over..
Can't endBackgroundTask: no background task exists with identifier 766972502f797261, or it may have already been ended. Break in UIApplicationEndBackgroundTaskError() to debug.
...
Still Exist, Remain: 0.198337
Still Exist, Remain: 0.000000
Still Exist, Remain: 0.000000
...

从日志来看,可执行的时间为180秒,即3分钟,过了3分钟,bg即被终止,但是,while循环依然可以执行,这可能是Bug,但是App倒是可以利用这一点做很多事情,例如保持Socket不会被挂起。

Downloading Content in the Background

对于需要在后台持续下载内容的App,应该使用NSURLSession,在切到后台时,系统会将NSURLSession单独划分到一个进程中,并且通知App下载的状态。流程如下:

一旦设置完成,NSURLSession对象将被系统接管:

如果任务完成时,app任然处于运行中(foreground或background状态),则:

NSURLSession对象会调用回调:

1
application:handleEventsForBackgroundURLSession:completionHandler:

在回调中,沿用之前的configuration,创建新的NSURLSessionConfigurationNSURLSession对象,系统会自动将其与之前的Session对象连接在一起。

如果NSURLSession在下载过程中,App被终止了,则:

系统会继续其下载,在下载完成时,如果sessionSendsLaunchEvents为YES,则拉起App。如果需要进行权限验证或者任务相关的事件需要App支持时,系统也会拉起App。

如果用户手动终止了App,则:

该App所有在等待的Tasks将被取消。

Implementing Long-Running Tasks

对于需要长时间后台运行的App,需要声明相应的权限,包含以下类型:

  • 后台播放音乐;
  • 后台录音;
  • 后台定位;
  • VoIP;
  • 下载和处理内容;
  • 接收外部设备连接的更新;

声明支持的后台任务类型

设置Info.plist文件的UIBackgroundModes属性:

Xcode background mode UIBackgroundModes value Description
Audio and AirPlay audio 后台播放或录制音乐(包含通过AirPlay的流媒体)
Location updates location 后台定位
Voice over IP voip 后台通话
Newsstand downloads Newsstand 后台下载杂志或者报纸内容
External accessory communication external-accessory 与硬件设备进行交互
Uses Bluetooth LE accessories bluetooth-central 与蓝牙设备进行交互
Acts as a Bluetooth LE accessory bluetooth-peripheral 通过peripheral模式进行蓝牙交互
Background fetch fetch 从网络下载以及处理较小的内容
Remote notifications remote-notification 在接收到推送时,App需要开始下载内容

Tracking the User’s Location

后台定位包含三种类型:

(1)Significant-Change Location Service

对于不需要精度定位数据的App,例如社交类或者新闻类,Significant-Change已经满足使用了。当定位刷新时,系统会将自动将App拉起。注意,Significant-Change只对包含蜂窝电波的设备有效。

(2)Foreground-Only Location Services

获取标准的定位数据,只在Foreground有效。

(3)Background Location Service

获取标准的定位数据,在Background时也有效。

Playing and Recording Background Audio

对于后台播放音乐的App,在后台时,如果在播放音乐,将不会被挂起,而一旦停止播放,会被系统挂起。对于多个App同时在后台播放音乐时,系统会根据Audio Session来管理音频焦点。

Implementing a VoIP App

对于支持VoIP的App,需要一直保持与服务器的联系,系统会挂起App,同时接管其Socket的状态,如果有流量进来,则唤醒App,并将Socket的控制交还给App。

配置VoIP流程如下:

  • 设置UIBackgroundModes包含voip字段;
  • 配置一个Socket用于VoIP;
  • 在切换到后台时,调用setKeepAliveTimeout:handler:方法以使handler周期性运行,并在handler中管理其与服务器的联系;
  • 在使用和结束使用时,管理好Audio Session。

Fetching Small Amounts of Content Opportunistically

对于需要周期性检查内容更新的App,可以设置UIBackgroundModes包含fetch字段,但这并不能保证系统一定会给予App时间去执行后台任务,只有在系统判断时机合适时,才会在后台唤醒或者拉起App,并调用回调:

1
application:performFetchWithCompletionHandler:

在该回调中,可以检查内容是否需要更新,并在更新完成后,调用CompletionHandler来通知系统。对于,下载更新内容越少和越精确的App,将比耗时和并没有下载内容的App,更可能被系统给予时间。在下载内容时,推荐使用NSURLSession来管理下载。

Using Push Notifications to Initiate a Download

对于接受到推送时,需要进行内容下载更新的App,设置UIBackgroundModes包含remote-notification字段,且推送的content-available的属性必须设置为1,此时,系统会在后台唤醒或拉起App,并调用回调:

1
application:didReceiveRemoteNotification:fetchCompletionHandler:

Downloading Newsstand Content in the Background

对于Newsstand类型的App,可以注册在后台下载咋着或者新闻内容,设置UIBackgroundModes包含newsstand-content字段,通过Newsstand Kit framework创建一个Download,系统会接管这个下载过程,即使App被挂起或者终止,系统会继续下载。当下载完成或者下载出现异常时,系统会发出通知来后台唤醒App以便处理。

Communicating with an External Accessory

与外部设备进行交互的App可以注册,在外部设备分发事件时,App被唤醒,例如,心率监测器,设置UIBackgroundModes包含external-accessory字段。当外设连接或者断开,以及外设发送数据时,系统会唤醒App来进行处理。

支持这一功能的App必须满足以下要求:

  • App必须提供允许用户启动和终止外设的分发事件,同时开启或者关闭外设的连接Session;
  • 在被唤醒后,App有将近10秒的时间来处理数据,理想情况下,App应该处理完数据,并让自身被再次挂起,而如果需要更多的时间,使用BackgroundTask

Communicating with a Bluetooth Accessory

与蓝牙设备进行交互的App可以注册,在蓝牙设备分发事件时,App被唤醒,例如,蓝牙心率监测器,设置UIBackgroundModes包含bluetooth-central字段。当外设连接或者断开,以及外设发送数据时,系统会唤醒App来进行处理。

iOS 6中,App可以在peripheral模式下进行蓝牙交互,自身作为一个蓝牙设备,设置UIBackgroundModes包含bluetooth-peripheral字段

注意事项,与上节一致。

Getting the User’s Attention While in the Background

当App处于挂起状态时,可以通过UILocalNotification来引起用户的注意。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (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];
}
}

这里的LocalNotification的数量不能超过128个,音频格式支持:Linear PCM, MA4, µ-Law和a-Law。默认的音频可以用UILocalNotificationDefaultSoundName

Understanding When Your App Gets Launched into the Background

对于支持后台运行的App,系统可能会出处理以下事件时,重新拉起:

  • 定位更新时;
  • 设备进入或离开一个特定的领域时,例如地理领域或者iBeacon领域;
  • 音频操作相关时;
  • 从一个连接的蓝牙设备接受数据时;
  • 作为一个蓝牙设备,接收到命令时;
  • 推送的content-available值为1时;
  • 后台下载新内容时;
  • NSURLSession下载完成或出现异常时;
  • Newsstand下载完成时。

对于被用户手动终止的App,除非重启手机,否则一般情况下,系统不会再次拉起。唯一的例外是定位App,在iOS 8以上,会被重新拉起。

Being a Responsible Background App

处于前台的App,比后台的App拥有更高的优先级。后台运行的App需要遵循以下原则:

  • 不要调用OpenGL ES。在后台调用OpenGL ES命令都会导致App立刻被终止;
  • 在被挂起前,取消任何的Bonjour相关的服务。在后台运行时,应该取消注册Bonjour服务以及关闭所有的监听Sockets。如果没有关闭,在App被挂起时,系统会自动关闭;
  • 网络相关的Sockets,应该处理连接失败的情况。在App被挂起时,系统可能会挂起Sockets连接。Sockets相关的代码应该处理各种网络异常的情况;
  • 在切到后台前,保存App状态。在内存过低的情况下,后台运行的Apps可能会被从内存移除以释放内存,此时App不会收到任何通知,因此最好保存App状态;
  • 在切到后台时,移除没必要的strong引用。如果App包含了占用内存大的对象,特别是图片,移除所有的strong引用;
  • 在被挂起前,停止使用共享的系统资源。App与共享系统资源交互,例如通讯录,日历数据库等,在被挂起前,应该停止使用。共享系统资源会优先给前台App使用,如果App被挂起后,依然使用上述资源,会被终止;
  • 避免更新Windows和Views。