iOS 后台模式

应用状态

来自苹果开发者文档 The App Life Cycle 的图.

1

下面这张图说明了应用程序的状态, 如下所示:

1

具体的说一下这5中状态:

1.Not running

未运行, 程序没启动或者被系统被用户杀死

2.Inactive

未激活, 程序在前台运行,不过没有接收到事件.
在没有事件处理情况下程序通常停留在这个状态.

3.Active

激活, 程序在前台运行而且接收到了事件.
这也是前台的一个正常的模式

4.Backgroud

后台, 程序在后台而且能执行代码,大多数程序进入这个状态后会在在这个状态上停留一会.
时间到之后会进入挂起状态(Suspended). 有的程序经过特殊的请求后可以长期处于 Backgroud 状态.

5.Suspended

挂起, 程序在后台不能执行代码.
系统会自动把程序变成这个状态而且不会发出通知.
当挂起时, 程序还是停留在内存中的, 当系统内存低时, 系统就把挂起的程序清除掉, 为前台程序提供更多的内存.

关于 Backgroud 状态, 是我们今天要说的重点部分.

多任务介绍

iOS 的多任务是在 iOS4 的时候被引入的,在此之前 iOS 的 APP 都是按下 Home 键就被干掉了.
iOS4 虽然引入了后台和多任务,但是实际上是伪多任务,一般的 APP 后台并不能执行自己的代码,只有少数几类服务在通过注册后可以真正在后台运行,并且在提交到 AppStore 的时候也会被严格审核是否有越权行为,这种限制主要是出于对于设备的续航和安全两方面进行的考虑.之后经过iOS5 和 iOS6 的逐渐发展,后台能运行的服务的种类虽然出现了增加,但是 iOS 后台的本质并没有变化.
在iOS7之前,系统所接受的应用多任务可以大致分为几种:

  • 后台完成某些花费时间的特定任务.
  • 后台播放音乐等.
  • 位置服务.
  • IP电话(VOIP).
  • Newsstand.

iOS7 后台任务申请的最长时间 10分钟.
iOS8+ 后台任务申请最长时间 3分钟.

示例: APP 退到后台会被挂起

今天跟大家分享的是一般应用如何在后台延长生命周期的知识, 关于其他特殊的 App 如上面提到的5中情况, 不是今天讨论的重点.

我们先看一个例子, 例子很简单.

这里要说明一下概念, 直接锁屏和点击 Home 键, 都会导致应用处于后台模式, 这里为了说明问题, 统一点
击 Home 作为代名词.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)viewDidLoad {

[super viewDidLoad];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
}

- (void)onDidEnterBackground:(NSNotification *)notification {

MZLOG(@"App Background. Enter onDidEnterBackground.");

int delta = 1;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delta * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

MZLOG(@"App Background. Enter onDidEnterBackground diapatch.");

UILocalNotification *notification = [UILocalNotification new];
notification.fireDate = [NSDate dateWithTimeIntervalSinceNow:3];
notification.alertBody = @"走, 去high吧!";
notification.soundName = UILocalNotificationDefaultSoundName;
// 可以自定义数据
notification.userInfo = @{@"user_info_key": @"user_info_value_json_str"};

[[UIApplication sharedApplication] scheduleLocalNotification:notification];
});
}

UIApplicationDidEnterBackgroundNotification 可以监听到用户将 APP 退到后台.

当 APP 退到后台, 会调用 onDidEnterBackground 这个方法.

在 onDidEnterBackground 这个方法中, 我故意延时执行代码, 这里使用的是 dispatch_after.

编译运行这个工程, 运行成功后, 可以点击 Home 键将应用退到后台.

可以在 Xcode 的控制台看到 App Background. Enter onDidEnterBackground. 的打印信息, 但是迟迟不见 dispatch_after 里面的代码执行.

这里说明, APP 退到后台后被系统挂起了.

另外一个例子就是使用 NSTimer, 在 APP 退到后台后, 也会被终止.

完整例子, 一会在文章后台附录给出.

通过后台模式延长 APP 运行

上面的例子充分说明了, 在我们没有做任何处理的情况下, iOS 系统在 APP 退到后台的情况下, 会被系统挂起, 从而终止 APP 的代码行为.

下面通过实例, 来开启后台任务, 让 APP 尽可能的延长声明周期.

在工程的基础上, 新建一个文件 MZBackgroundTask

1
2
3
4
@interface MZBackgroundTask : NSObject
+ (instancetype)sharedTask;
- (void)startTask;
@end

具体实现, 只给出关键代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
- (void)startTask {

if (![self _checkSupportBackgroundTask]) {

MZLOG(@"BackgroundTask. Current device don't support backgroundTask.");
return;
}

UIApplication *application = [UIApplication sharedApplication];

__block UIBackgroundTaskIdentifier taskId;

/// 申请后台执行
/// 注意: 在iOS7和该版本前,后台可以用下面的的方式在后台存活5-10分钟,在iOS8及后,最多存活3分钟
{
taskId = [application beginBackgroundTaskWithName:NSStringFromClass([self class]) expirationHandler:^{

MZLOG(@"BackgroundTask. BackgroundTask is Over. The remained time: %f", application.backgroundTimeRemaining);

[application endBackgroundTask:taskId];

taskId = UIBackgroundTaskInvalid;
}];
}

if (UIBackgroundTaskInvalid == taskId) {

MZLOG(@"BackgroundTask. Apply backgroundTask failed.");

return;
}

/// 可以监控后台任务剩余的时间, 针对业务可以去处理
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

__block NSTimeInterval remainedTime;

while (true) {

// 剩余可以后台执行的时间
dispatch_async(dispatch_get_main_queue(), ^{

// application.backgroundTimeRemaining 必须在主线程获取
remainedTime = application.backgroundTimeRemaining;

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

MZLOG(@"BackgroundTask. The remained time: %f", remainedTime);

if (remainedTime < 10) {

// 可以告诉其他业务, 后台申请的时间即将结束了
}

if (remainedTime < 2) {

/// 这里可以做一些清除工作
{
// clean up
}

[application endBackgroundTask:taskId];

taskId = UIBackgroundTaskInvalid;

return;
}

// 睡眠(延时)1s
[NSThread sleepForTimeInterval:1.f];
});
});
}
});
}
}

startTask 开启后台任务.

在 AppDelegate 中, 调用这个方法.

1
2
3
4
- (void)applicationDidEnterBackground:(UIApplication *)application {

[[MZBackgroundTask sharedTask] startTask];
}

再次运行工程, 可以根据日志看出, 之前的 dispatch_after 和 timer 可以运行了, 并且可以运行3分钟(180s).

根据 backgroundTimeRemaining 这个属性, 可以看出具体的后台可执行的剩余时间.

1

注意: 我测试的时候使用的是 iOS10 设备.

后记

除了苹果规定的几种类型(如定位, 录音, VOIP 等)的应用外, 其他 APP 想申请更多的后台驻留时间, 就需要一些 旁门左道 的方法了.

苹果对后台操作做了这么多限制, 也是从用户的角度出发, 如安全, 省电, 省流量等.

比如, 在后台播放没有声音的音乐.

再比如, 申请定位服务的权限, 这样也可以保持 APP 在后台不被挂起.
但是, 依照苹果一贯的审核做法来看,如果声明了需要某项后台权限,你却没有相关实现的话,会直接被拒掉的.

这些是技术上的实现, 我没有推荐大家这么干, 现在苹果审核比以前还要严格, 大家还是悠着点干吧.如果你的 APP 不需要上架到 AppStore 的话, 就尽情的放纵吧…

推荐博文

1.iOS实现无限后台background的方法

2.WWDC 2013 Session笔记 - iOS7中的多任务

iOS 在后台的时候如果不使用后台模式, socket 也会被系统关闭连接, 比如我们使用的 IM 功能.
使用后台模式后, 向系统申请的时间(3分钟内), socket 还是没有被关闭的, 除非断网或者被路由器给断开了, 在申请的这段时间内, socekt 还是可以使用的, 如果想持续的保持 socket 连接, 就需要去了解一下 VOIP Socket 相关的知识了, 实践过后, 分享给大家.

附录

下面是具体的实现代码.

ViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@interface ViewController ()

@property (nonatomic, strong) NSTimer *timer;

@end


@implementation ViewController

- (void)viewDidLoad {

[super viewDidLoad];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];

// 每隔一秒执行一次
_timer = [NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:@selector(onTimerDidRun:)
userInfo:nil repeats:YES];

[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}

#pragma mark - Callback.

- (void)onTimerDidRun:(id)sender {

MZLOG(@"App Background. Timer Running.");
}

- (void)onDidEnterBackground:(NSNotification *)notification {

MZLOG(@"App Background. Enter onDidEnterBackground.");

int delta = 1;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delta * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

MZLOG(@"App Background. Enter onDidEnterBackground diapatch.");

UILocalNotification *notification = [UILocalNotification new];
notification.fireDate = [NSDate dateWithTimeIntervalSinceNow:3];
notification.alertBody = @"走, 去high吧!";
notification.soundName = UILocalNotificationDefaultSoundName;
// 可以自定义数据
notification.userInfo = @{@"user_info_key": @"user_info_value_json_str"};

[[UIApplication sharedApplication] scheduleLocalNotification:notification];
});
}

#pragma mark - SetupViews.

- (void)_setupViews {

self.view.backgroundColor = [UIColor purpleColor];

}

MZBackgroundTask.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#import "MZBackgroundTask.h"

@implementation MZBackgroundTask

+ (instancetype)sharedTask {

static MZBackgroundTask *_task;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_task = [[self alloc] init];
});

return _task;
}

- (void)startTask {

if (![self _checkSupportBackgroundTask]) {

MZLOG(@"BackgroundTask. Current device don't support backgroundTask.");
return;
}

UIApplication *application = [UIApplication sharedApplication];

__block UIBackgroundTaskIdentifier taskId;

/// 申请后台执行
/// 注意: 在iOS7和该版本前,后台可以用下面的的方式在后台存活5-10分钟,在iOS8及后,最多存活3分钟
{
taskId = [application beginBackgroundTaskWithName:NSStringFromClass([self class]) expirationHandler:^{

MZLOG(@"BackgroundTask. BackgroundTask is Over. The remained time: %f", application.backgroundTimeRemaining);

[application endBackgroundTask:taskId];

taskId = UIBackgroundTaskInvalid;
}];
}

if (UIBackgroundTaskInvalid == taskId) {

MZLOG(@"BackgroundTask. Apply backgroundTask failed.");

return;
}

/// 可以监控后台任务剩余的时间, 针对业务可以去处理
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

__block NSTimeInterval remainedTime;

while (true) {

// 剩余可以后台执行的时间
dispatch_async(dispatch_get_main_queue(), ^{

// application.backgroundTimeRemaining 必须在主线程获取
remainedTime = application.backgroundTimeRemaining;

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

MZLOG(@"BackgroundTask. The remained time: %f", remainedTime);

if (remainedTime < 10) {

// 可以告诉其他业务, 后台申请的时间即将结束了
}

if (remainedTime < 2) {

/// 这里可以做一些清除工作
{
// clean up
}

[application endBackgroundTask:taskId];

taskId = UIBackgroundTaskInvalid;

return;
}

// 睡眠(延时)1s
[NSThread sleepForTimeInterval:1.f];
});
});
}
});
}
}

#pragma mark - Private.

/**
* 当前设备是否支持后台任务.
*
* @return YES, 支持后台任务. 否则, 不支持后台任务.
*/
- (BOOL)_checkSupportBackgroundTask {

SEL sel = @selector(isMultitaskingSupported);
BOOL supportBTask = [[UIDevice currentDevice] respondsToSelector:sel];

return supportBTask;
}