多线程编程基础

iOS 多线程

进程与线程

进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,是正在运行的程序的实例(An instance of a computer program that is being executed)。

进程的概念主要有两点:

  • 进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(Text Region)、数据区域(Data Region)和堆栈(Stack Region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区存储着活动过程调用的指令和本地变量。
  • 进程是一个执行中的程序。

线程

线程是程序执行流的最小单元。一个标准的线程由线程ID、当前指令指针、寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分配的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可以与同属一个进程的其它线程共享进程所拥有的全部资源。

进程与线程的关系与区别

进程是资源分配的基本单位。所有与该进程有关的资源,都被记录在进程控制块PCB中,以表示该进程拥有这些资源或正在使用它们。另外,进程也是抢占处理机的基本单位,它拥有一个完整的虚拟地址空间。当进程发生调度时,不同的进程拥有不同的虚拟地址空间,而同一进程内的不同线程共享同一地址空间。

与进程相对应,线程与资源分配无关,它属于某一个进程,并与进程内的其它线程一起共享进程的资源。线程只由相关堆栈、寄存器和线程控制表TCB组成。寄存器可被用来存储线程内的局部变量,但不能存储其它线程的相关变量。

通常在一个进程中可以包含多个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更叫高效的提高系统内多个程序之间并发执行的程度。

线程和进程区别:

  • 地址空间和其它资源:进程之间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
  • 通信:进程间通信主要方式有管道、系统IPC(消息队列、信号、共享存储)、套接字(Socket)。而线程间可以直接读写进程数据段来进行通信(需要进程同步和互斥手段的辅助,以保证数据的异质性)
  • 调度和切换:线程上下文切换比进程快的多。
  • 在多线程OS中,进程不是一个可执行的实体

线程安全

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其它线程不能进行访问直到该线程读取完,其它线程才可以使用。

iOS中的多线程

目前在iOS中有四种多线程解决方案:

  • Pthreads
  • NSThread
  • GCD
  • NSOperation & NSOperationQueue

Pthreads

POSIX线程,简称Pthreads,是线程的POSIX标准。该标准定义了创建和操作线程的一整套API。在类Unix操作系统中,都使用Pthreads作为操作系统的线程。

NSThread

NSThread是经常Apple封装的完全面向对象的。你可以直观方便的操控线程对象,但是生命周期需要手动管理。

GCD

GCD是Apple为多核的并行运算提出的的解决方案,能够自动合理地利用更多的CPU内核,并自动管理线程的声明周期。GCD使用C进行编写,并使用了Block。

####任务

即代码所要完成的操作。

任务的执行方式有两种:同步执行和异步执行。

同步执行操作,它会阻塞当前线程并等待任务执行完毕,然后当前线程才会继续往下运行。

异步执行操作,则不会阻塞当前线程,当前线程会直接往下执行。

队列

用于存放要执行的任务。

串行队列中的任务,GCD会遵循FIFO原则来执行,串行队列的同步执行任务,会在当前线程一个一个执行,而异步执行任务,则会在它线程中一个一个执行。

并行队列中的任务执行顺序则要复杂一点,任务会根据同步或异步有不同的执行方式。并行队列中的同步执行任务会在当前线程中一个一个执行,而异步执行则会开很多线程一起执行。

如何创建队列?
  • 主队列:特殊的串行队列,主要用于刷新UI,任何需要刷新UI的工作都必须在主队列中执行。

dispatch_queue_t mainQueue = ispatch_get_main_queue();

  • 自己创建的队列:

    创建串行队列

dispatch_queue_t serialQueue = dispatch_queue_create("CustomSerialQueue",DISPATCH_QUEUE_SERIAL);

​ 创建并行队列

dispatch_queue_t concurrentQueue = dispatch_queue_create("CustomConcurrentQueue",DISPATCH_QUEUE_CONCURRENT);

  • 全局队列:这是一个并行队列,并行任务一般都加入到这个队列。

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)

如何创建任务?

  • 创建同步任务

    dispatch_sync(,^{// execute code));

  • 创建异步任务

    dispatch_async(,^{// execute code});

队列组

队列组可以将很多队列添加到一个组里,当组中的所有任务都执行完了,队列组将会通知给用户。

dispatch_group_enterdispatch_group_leave 分别表示一个任务追加到队列组和一个任务执行完毕离开了队列组。

只有当group中未执行完毕的任务数量为0时,才会使 dispatch_group_wait 解除阻塞,以及执行追加到 dispatch_group_notify 的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIOITY_DEFAULT,0);
dispatch_group_async(group, queue, ^{
// execute code
});
// 向group中添加在主队列中执行的任务
dispatch_group_async(group, dispatch_get_main_queue(),^{
// execute code
});
// 向group中追加任务
dispatch_group_enter(group);
dispatch_async(queue,^{
// execute code
dispatch_group_leave(group);
});

// 直到前面加入到group中的所有任务都执行完毕后,才会执行
dispatch_group_notify(group,dispatch_get_main_queue(),^{
// execute code
});
OR
// 直到前面加入到group中的所有任务都执行完成后,才会继续往下执行
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

GCD的信号量机制

并发队列可以分配多个线程,同时处理不同的任务,虽然提升了效率,但是多线程的并发是通过时间片轮转的方法实现的,线程的创建、销毁、上下文切换等会消耗资源。适当的并发可以提高效率,但是无节制的并发,则会抢占CPU资源,造成性能下降。此外,提交给并发队列的任务中,有些任务内部会有全局的锁,会导致线程休眠、阻塞,一旦这类任务过多,并发队列还需要创建新的线程来执行其它任务,会造成线程数量的增加。

因此控制并发队列中的线程数量就成了不能忽视的问题。

GCD并发线程数量控制

GCD中的信号量(dispatch_semaphore)是一个整形值,有初始计数值,可以接收通知信号和等待信号。当信号量收到通知信号时,计数+1;当信号量收到等待信号时,计数-1。如果信号量为0,线程会被阻塞,直到信号量大于0,才会继续执行。

使用信号量机制可以实现线程的同步,也可以控制最大并发数。

使用GCD信号量机制实现并发线程数量控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch_queue_t	workConcurrentQueue	=	dispatch_queue_create(@"WORK_CONCURRENT_QUEUE",DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t workSerialQueue = dispatch_queue_create(@"WORK_SERIAL_QUEUE",DISPATCH_QUEUE_SERIAL);
int maxConcurrent = 10;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(maxConcurrent);
for (NSInteger i = 0; i < 10; i++) {
dispatch_async(workSerialQueue, ^{
// 使信号量-1,当信号量为0时就一直等待,即阻塞所在线程
// 这里使信号量 maxConcurrent-1,表示最大并发数量已被占用一个位置
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(workConcurrentQueue, ^{
// 发送一个信号,让信号量+1
// 这里使信号量 maxConcurrent+1,表示任务被执行,释放了最大并发数量中的一个位置
dispatch_semaphore_signal(semaphore);
});
});
}
使用GCD信号量机制实现线程同步

有时候我们会遇到需要异步执行一些耗时任务,并在这些任务完成后进行一些额外的操作,相当于将异步执行任务转化为同步执行任务。

1
2
3
4
5
6
7
8
9
10
11
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

dispatch_async(queue, ^{
// 执行耗时任务
···
// 任务完成后使信号量+1,被阻塞的线程继续执行
dispatch_semaphore_signal(semaphore);
});
// 如果信号量为0,则会阻塞当前线程,直到信号量
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
使用GCD信号量机制实现线程安全

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其它变量的值也和预期的是一样的,就是线程安全的。

1
2
3
4
5
6
7
8
9
dispatch_semaphore_t semaphoreLock	=	dispatch_semaphore_create(1);
// 相当于加锁
dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
{
// 需要保证安全的执行代码
}
// 相当于解锁
dispatch_semaphore_singal(semaphoreLock);

GCD的一些使用场景

使用GCD实现延迟执行
1
2
3
4
5
dispatch_queue_t queue	=	dispatch_ger_gloabl_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
double delay = 3;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW), (int64_t)(delay*NSEC_PER_SEC)), queue, ^{
// execute code
});
使用GCD实现单例模式
1
2
3
4
5
6
7
8
static id _instance;
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// initial code
});
retur _instance;
}
任务同步

我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于 栅栏 一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到dispatch_barrier_async方法在两个操作组间形成栅栏。

dispatch_barrier_async 函数会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在 dispatch_barrier_async 函数追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dispatch_queue_t queue = dispatch_queue_create("com.codelei.queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
// Task 1
});
dispatch_async(queue, ^{
// Task 2
});

dispatch_barrier_async(queue, ^{
// 追加的任务 Task 3
});

dispatch_async(queue, ^{
// 追加的任务 Task 4
});

dispatch_async(queue, ^{
// 追加的任务 Task 5
});

直到 Task 1 和 Task 2 执行完成后,才会执行使用 dispatch_barrier_asynce 追加的任务 Task 3,然后在 Task 3 执行完成后,并行队列会正常执行。

NSOperation & NSOperationQueue

NSOperation是Apple对GCD的封装,完全面向对象。NSOperation和NSOperationQueue分别对应GCD的任务和队列。

添加任务

NSOperation只是一个抽象类,并不能直接封装任务。它有两个子类NSInvocationOperation和NSBlockOperation用来完成封装任务的操作。创建一个Operation后,需要调用start方法来启动任务,它默认在当前队列同步执行。如果需要在执行途中取消执行一个任务,调用cancel方法即可。

  • NSInvocationOperation

    1
    2
    NSInvocationOperation *invocationOperation = [[NSInvocation alloc] initWithTarget:self selector:@selector(executeMethod)];
    invocationOperation start];
  • NSBlockOperation

    1
    2
    3
    4
    5
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    // execute code
    }];
    [blockOperation start];

    上面提到,NSInvocationOperation和NSBlockOperation创建的任务默认在当前线程执行,但是NSBlockOperation可以通过addExecutionBlock:方法向Operation中添加多个可执行的Block。这样的Operation中的任务会并发执行,它会在主线程和其它多个线程执行这些任务。

    1
    2
    3
    4
    5
    6
    7
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    // execute code
    }];
    [blockOpertation addExecutionBlock:^{
    // execute code
    }];
    [blockOperation start];

    NOTE:addExecutionBlock:方法必须在start方法之前执行,否则会报错。

  • 自定义的Operation

    自定义Operation类需要继承Operation类,并实现其main()方法,因为在调用start()方法的时候,内部会调用main()方法完成相关逻辑。

创建队列

通过调用一个NSOperation类的start()方法来启动的任务,默认在当前线程同步执行。如果要避免占用当前线程,就需要使用到队列NSOperationQueue。只要将Operation添加到队列,就会自动调用任务的start()方法。

  • 主队列

    添加到主队列中的任务时串行执行的。

    1
    NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
  • 其它队列

    其它队列中的任务会在其它线程并行执行。

    1
    2
    3
    4
    5
    NSOperationQueue *queue = [NSOperatin alloc] init];
    NSBlockOperatin *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    // execute code
    }]
    [queue addOperation:blockOperation];

    如果需要任务在队列中串行执行,可以通过设置NSOperationQueue的maxConcurrentOperationCount来实现。

    1
    2
    NSOperationQueue *queue = [NSOperatin alloc] init];
    [queue setMaxConcurrentOperationCount:1];

    你还可以通过addOperationWithBlock:方法来向队列中添加新任务。

    1
    2
    3
    4
    NSOperationQueue *queue = [NSOperatin alloc] init];
    [queue addOperationWithBlock:^{
    // cxecute code
    }]

添加任务依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
NSBlockOperation	*blockOperationFirst  = [NSBlockOperation blockOperationWithBlock:^{
// execute code
}];
NSBlockOperation *blockOperationSecond = [NSBlockOperation blockOperationWithBlock:^{
// execute code
}];
NSBlockOperation *blockOperationThird = [NSBlockOperation blockOperationWithBlock:^{
// execute code
}];
[blockOperationSecond addDependency:blockOperationFirst];
[blockOperationThird addDependency:blockOperationSecond];
NSOperationQueue *queue = [NSOperatin alloc] init];
[queue addOperations:@[blockOperationFirst,blockOperationSecond,blockOperationThird]];

NOTE:

  • 添加相互依赖会造成死锁。
  • 使用removeDependency方法来移除依赖关系

其它方法

  • NSOperation类的一些其它方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 判断任务是否正在执行
    BOOL exccuting;
    // 判断任务是否完成
    BOOL finished;
    // 设置任务完成后的后续操作
    void (^completionBlock) (void);
    // 取消任务
    - (void)cancle;
    // 阻塞当前线程直到此任务执行完毕
    - (void)waitUntilFinished;
  • NSOperationQueue

    1
    2
    3
    4
    5
    6
    7
    8
    // 获取队列的任务数量
    NSUInteger operationCount;
    // 取消队列中的所有任务
    - (void)cancelAllOperations;
    // 阻塞当前线程直到此队列中的所有任务执行完毕
    - (void)waitUntilAllOperationsAreFinished;
    // 暂停或继续队列
    BOOL suspended;

后台运行探索与解析

iOS 后台运行


当用户没有主动的使用你的应用程序时,系统会将它转换为后台运行状态。对于大多说应用程序来说,后台运行状态只是应用程序在被系统挂起前的一个阶段。挂起应用程序是一个改善电池寿命并让系统为前台应用程序释放重要资源的方式。

大多数应用程序都能很容易的挂起,但是拥有合理的理由在后台继续运行的应用程序也是有的。一个远足应用程序想要随着时间来定位用户的位置,让它能够在地图上显示用户的运动进程。一个音频应用程序可能需要在屏幕锁定时继续播放音乐。其它的应用程序可能想要在后台下载内容以便能够减少将内容呈献给用户的延迟。当你发现你的应用程序需要保持在后台继续运行的时候,iOS系统能够帮助你有效率的且尽可能少地占用系统资源的完成这个目的。

iOS提供的技术分为三类:

  • 在前台开始短任务的应用程序可以在进入后台状态时向系统请求完成这个任务的额外时间。
  • 在前台启动下载的应用程序可以将这些下载的管理移交给系统,从而允许在下载过程中暂停或终止该应用。
  • 需要在后台运行以支持特定类型任务的应用程序可以声明对一个或多个后台执行模式的支持。

尽可能避免在后台执行任何任务除非这样做能全面的提升用户的体验。应用程序可能会因为启动了另一个应用程序,锁定了屏幕或现在不使用它而被转入后台。在这些情况下,用户都表明你的应用程序现在不需要做任何有意义的工作。在这种情况下继续运行只会浪费设备的电量并可能导致用户强制的完全退出你的应用程序。所以你应该审慎的考虑在后台运行应用程序。

执行有限长度的任务

被转入后台的应用程序期待尽可能快的进入非活动状态以便它们能够被系统挂起。如果你的应用程序正在执行某个任务而且完成这个任务还需要一些时间的话,你可用调用 UIApplicationbeginBackgroundTaskWithName:expirationHandler:beginBackgroundTaskWithExpirationHandler:方法来请求一些额外的执行时间。调用这两个方法的任一个都会暂时的推迟你的应用程序被挂起,从而为完成你正在进行的任务赢得一些额外的时间。一旦你的应用程序完成了任务,你必须调用endBackgroundTask:方法让系统知道你的应用程序已经完成了任务,可以被挂起了。

每个调用beginBackgroundTaskWithName:expirationHandler:beginBackgroundTaskWithExpirationHandler: 方法的应用程序都会生成一个与相应的任务相关的标记。当你的应用程序完成任务时,它必须以这个标记来调用 endBackgroundTask:方法告知系统任务已经完成。调用 endBackgroundTask:方法失败会导致你的应用程序被终止。如果你在启动任务的时候提供了一个完成处理模块,系统会调用这个模块并给你最后一次结束任务避免程序被终止的机会。

你不需要一直到等到应用程序进入后台才指定后台任务。一个更有用的设计是在开始任务之前调用beginBackgroundTaskWithName:expirationHandler:beginBackgroundTaskWithExpirationHandler:方法,一旦完成就调用endBackgroundTask:方法.

下面的的代码展示了当你的应用程序进入后台时如何开始一个长时运行的任务。在这个例子中,开始后台任务的请求包含了一个完成处理模块以防这个任务耗时太长。这个任务稍后会被提交到一个异步执行的队列中以便applicationDidEnterBackground:方法能够正常返回。blocks的使用简化了维护一些重要变量引用所需要的代码。bgTask变量是指向存储当前任务标识符的指针的类的成员变量,它在调用这个方法之前被初始化。

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

注意: 你总是要在开始一个任务的时候提供完成处理模块,但是如果你想知道你的应用程序还能运行多长时间,你可以通过UIApplication对象的backgroundTimeRemaining属性来获得。

在你的完成处理模块中,你可以加入一些必须的代码来关闭你的任务。但是,完成处理模块中的任何代码都不应该耗费太长的时间去执行,因为你的完成处理模块一旦被调用,就说明你的应用程序已经快要被关闭了。因此,只执行最少的清理状态信息并结束任务。

在后台执行下载任务

当下载文件时,应用程序应该使用NSURLSession对象来开始下载,这样系统就能控制下载的进程即使应用程序被挂起或终止。当你配置NSURLSession对象进行后台传输时,系统用一个单独的队列管理这些任务并以常规方式将传输的状态报告给你的应用程序。如果你的应用程序在传输正在进行时被终止,系统会在后台继续传输,并在传输完成或一个甚至多个任务需要你的应用程序时启动你的应用程序(若果使用)。

为了支持后台传输,你必须合适地配置你的NSURLSession对象。为了配置NSURLSession有必须先创建一个NSURLSessionConfiguration对象并给它的一些属性赋予合适的值。然后将这个NSURLSessionConfiguration对象在初始化NSURLSession对象时传递过去。

创建支持后台下载的NSURLSessionConfiguration对象的过程如下:

1.使用NSURLSessionConfigurationbackgroundSessionConfigurationWithIdentifier:方法创建配置对象。

2.将配置对象的sessionSendLaunchEvents属性的值设置为YES

3.如果你的应用程序在前台进行转移,建议你将sessionSendsLaunchEvents属性设置为YES

4.根据需要配置配置对象的任何其他属性。

5.使用配置对象创建您的NSURLSession对象。

一旦配置完成,你的NSURLSession对象会在合适的时间将上传和下载任务移交给系统。如果任务在你的应用程序仍在运行时完成(无论是在前台或是后台),NSURLSession对象都会以常规方式通知它的代理。如果任务还未完成时你的应用程序就被终止,系统会自动地在后台管理任务。如果用户终止了你的应用程序,系统会停止任何待处理的任务。

当所有与后台会话管理的任务完成时,系统会重启被终止的应用程序(假定sessionSendsLaunchEvents属性被设置为YES而且这个应用程序不是被用户强制终止的)并调用应用代理的application:handleEventsForBackgroundURLSession:completionHandler:方法。(系统还可以重新启动应用程序来处理身份验证或其他需要你的应用程序注意的任务的相关事件)在执行该委托方法时,请使用提供的标识符创建一个新的与之前相同的NSURLSessionConfigurationNSURLSession对象。 系统将你的新会话对象重新连接到先前的任务,并将其状态报告给会话对象的委托。

执行长时间运行的任务

对于需要更多后台执行时间的任务,你必须请求特定的权限才能在后台运行它们而不被挂起。iOS中,只有特定类型的应用程序被允许在后台运行:

  • 在后台播放音频内容给用户,比如音乐播放器
  • 在后台录制音频文件
  • 使用户随时了解其位置的应用程序,比如导航应用程序
  • 支持VoIP的应用程序
  • 需要经常性的下载和处理新内容的应用程序
  • 周期性的从外部配件接收更新的应用程序

使用这些服务的应用程序必须声明其支持的服务,并使用系统框架来实现这些服务的相关方面。

声明你的应用程序支持的后台模式

你必须在应用程序使用后台任务之前声明你要支持的后台任务类型。在Xcode 5和之后,你需要在你工程的Capabilities选项卡中声明你要支持哪些后台模式。启用后台模式选项将UIBackgroundModes键添加到应用程序的Info.plist文件中。

下表列出了你可以指定的后台模式的值:

Xcode后台模式 UIBackgroundModes 值 描述
音频和AirPlay audio 应用程序在后台播放或录制音频内容。用户必须在第一次使用前就授权使用麦克风。
位置更新 location 使用户随时知道他们的位置,即使应用在后台运行。
网络电话 voip 应用程序提供给用户通过网络连接进行通话的能力。
Newsstand下载 newsstand-content Newsstand类型的应用程序在后台下载并处理报纸或杂志内容。
外部附件通信 external-accessory 应用程序与需要通过外部附件框架定期提供更新的硬件配件配合使用。
使用蓝牙设备 bluetooth-central 应用程序使用需要通过Core Bluetooth框架定期发送更新的蓝牙设备。
作为蓝牙LE附件 bluetooth-peripheral 应用程序通过Core Bluetooth框架支持外设模式下的蓝牙通信。使用此模式需要用户授权
后台抓取 fetch 应用程序定期的从网路下载并处理少量数据。
远程推送 remote-notification 应用程序想要在接收到一个远程推送时开始下载内容。

以上每种模式都让系统知道应该在合适的时间唤醒或启动应用程序来响应相关的事件。

追踪用户位置

在后台追踪用户的位置的方式有好几种,大多数方式实际上并不需要你的应用程序在后台不断的运行。

  • 重大位置变更
  • 仅在前台定位服务
  • 后台定位服务

对于不需要高精度位置数据的应用程序来说,推荐使用重大位置变更定位服务。这个服务只有在用户的位置发生非常重大的变化时才会产生位置更新;它对社交类应用程序或者给用户提供不是很重要的位置相关信息的应用程序来说是非常理想的定位方式。如果当一个位置更新发生时应用程序被终止,系统会在后台唤醒它来处理这个更新。如果应用程序开始使用了这个服务然后被终止,当新的位置更新产生时,系统会自动重启它。

仅前台定位服务和后台定位服务都使用标准的Core Location服务获取位置数据。唯一的区别是,如果应用程序被挂起,则仅前台定位服务停止发送更新。 前台定位服务适用于只在前台需要位置数据的应用程序。

你可以在Xcode工程中的Capabilities选项卡中启用支持定位服务(你也可以通过在Info.plist文件中设置UIBackgroundModes的值为location来启用这个服务)。启用这个服务并不会阻止系统挂起你的应用程序,但是它会告知系统无论何时新的位置更新被发送过来都应该唤醒应用程序来处理。

重要提示:鼓励你谨慎使用标准的定位服务或改用重要的位置更改服务。 定位服务需要经常使用iOS设备的板载无线电硬件。 连续运行这个硬件会消耗大量的电量。 如果你的应用程序不需要向用户提供精确且连续的位置信息,则最好尽量减少使用位置服务。

在后台播放或录制音频

一个需要连续播放或录制音频的应用程序(即使应用处在后台)可以注册后台服务,实现即使在后台也能执行这些任务。你可以在Xcode工程中的Capabilities选项卡中启用服务(你也可以通过在Info.plist文件中设置UIBackgroundModes的值为audio来启用这个服务)。在后台播放音频内容的应用程序必须播放听得见的内容而不能是无声的。

后台音频应用程序的典型例子包括:

  • 音乐播放程序
  • 音频录制程序
  • 支持通过AirPlay播放音频或视频的程序
  • VoIP程序

UIBackgroundModes键包含audio值时,系统的媒体框架会自动阻止相关的应用程序被挂起。只要应用程序还在播放视屏或音频内容,录制音频,它就还能在后台运行。然而,一旦录制或播放停止,就会被系统挂起。

你可以使用任何系统音频框架来处理后台音频播放,并且使用这些框架的过程和在前台使用相同(对于通过AriPlay播放视频内容来说,你可以使用Media Player或AVFoundation框架来实现)。因为应用程序在播放媒体文件时不会被挂起,所以即使应用程序在后台也能正常的进行回调操作。在你的回调中,你应该只做为播放提供数据的工作,不应该在回调中执行任何与播放无关的任务。

在任何给定的时刻,因为可能有不止一个应用程序支持音频服务,所以由系统来决定哪个应用程序能够播放或录制音频。前台应用程序总是有优先的音频操作权利。可能有不止一个应用程序被允许在后台播放音频,这个时候决定哪个应用程序能够播放音频就取决于每个应用程序的音频会话的配置。

实现一个VoIP应用程序

一个Voice over Internet Protocol应用程序允许用户通过互联网而不是设备的蜂窝网络设备进行语音通话。这样的一个应用程序需要为它的服务维持一个持续的网络连接,以便它能接收到打进来的电话和其它相关数据。系统允许VoIP应用程序被挂起并给它提供了监测它们的sockets的便利,而不是让它一直处于唤醒状态。当检测到传入流量时,系统唤醒VoIP程序并将socket的控制权交还给它。

为了配置VoIP应用程序,你必须:

  • 在Xcode工程中的Capabilities选项卡中启用服务(你也可以通过在Info.plist文件中设置UIBackgroundModes的值为voip来启用这个服务)。
  • 为VoIP应用程序配置一个socket。
  • 在移动到后台之前,调用setKeepAliveTimeout:handler:方法来安装一个定期执行的程序。你的应用程序可以使用这个处理程序来保持服务连接。
  • 配置你的音频会话来操作进入或退出活跃的使用状态的转换。

设置UIBackgroundModes的值为voip让系统知道,当VoIP应用程序需要管理它的网络会话时,系统应该允许它在后台运行。为了让VoIP应用程序总是可用,系统会在启动后立刻重新启动拥有这个键值的应用程序。

大多数的VoIP应用程序也需要配置后台音频模式,因为它也需要在后台发送音频内容。因此,你应该将UIBackgroundModes的值设置为audiovoip。如果你不这样做的话,你的应用程序将不能在后台播放或录制音频内容。

适时的获取少量内容

需要定期的检查新内容的应用程序可以请求系统唤醒它们,以便它们可以初始化一个获取内容的拉取操作。为了支持这种模式,你需要在Xcode工程中的Capabilities选项卡中启用服务(你也可以通过在Info.plist文件中设置UIBackgroundModes的值为fetch来启用这个服务)。启用这个服务并不能保证系统会给你的应用程序任何时间来执行后台拉取操作。系统必须在你的应用程序拉取内容的请求和其它应用程序以及系统自身之间做出平衡。在评估所有信息后,如果有很好的机会的话,系统会给请求拉取内容的应用程序一些执行时间。

当有好机会产生时,系统会唤醒或启动你的应用程序到后台并调用应用程序代理对象的application:performFetchWithCompletionHandler:方法。使用这个方法来检查新内容,如果新内容可用则开始下载操作。一旦新内容下载完成,你必须将新内容是否可用的结果传递给提供好的完成处理块。执行这个块告诉系统它可以将你的应用程序转换到挂起状态并评估其使用功率。可以快速下载少量内容并在它们有可用的下载内容时准确反应的应用程序,比起花费更长的下载时间或声称有可用的下载内容但没有下载任何东西的应用程序,更可能在未来获得执行时间。

在下载任何内容时,推荐你使用NSURLSession类来开始和管理你的下载。

使用推送通知来开始下载

如果你的服务器在有新的应用程序可用内容时向用户的设备发送了一个推送通知,你可以请求系统在后台运行你的应用程序并立即开始下载新的可用内容。这种后台模式意图在于尽可能减少从你的用户看见推送通知到你的应用程序可以展示相关内容之间的时间。应用程序通常会在用户看见推送通知的差不多相同的时间被唤醒,但是仍会给你更多的准备时间。

为了支持这种模式,你需要在Xcode工程中的Capabilities选项卡中启这项用服务(你也可以通过在Info.plist文件中设置UIBackgroundModes的值为remote-notification来启用这个服务)。

对于一个使用推送通知来触发下载操作的应用程序来说,通知的有效内容必须包含值为1content-available的键。当这个键值对被检测到时,系统会启动或唤醒你的应用程序到后台同时调用应用程序的代理对象的application:didReceiveRemoteNotification:fetchCompletionHandler:方法。你应该在这个方法里实现对相关内容的下载并将下载好的内容在加入到你的应用程序中。

在下载任何内容时,推荐你使用NSURLSession类来开始和管理你的下载。

在后台下载新闻站内容

下载新闻和新的杂志文章的新闻站应用程序可以注册在后台进行这些下载。你需要在Xcode工程中的Capabilities选项卡中启这项用服务(你也可以通过在Info.plist文件中设置UIBackgroundModes的值为newsstand-content来启用这个服务)。当你提供这个关键字时,如果你的应用程序没有在运行,系统会启动你的应用程序以便它能开始下载新的文章。

当你使用Newsstand Kit框架来开始一个下载时,由系统为你的应用程序操作下载的进程。即使你的应用程序被挂起或终止,系统仍会继续下载文件。当下载操作完成后,系统将下载好的文件传输到你的应用程序的沙盒并向你的应用程序发送一个通知。如果应用程序没有在运行,这个通知会唤醒它并给它一个处理新的下载文件的机会。如果在下载过程中发生了错误,你的应用程序也会这样被唤醒来处理这个错误。

与外部附件通信

使用外部附件的应用程序可以请求在附件发送了一个更新时被唤醒,即使它处在挂起状态。这种支持对于某些定期发送数据的附件来说是非常重要的,比如心率监视器。你需要在Xcode工程中的Capabilities选项卡中启这项用服务(你也可以通过在Info.plist文件中设置UIBackgroundModes的值为external-accessory来启用这个服务)。当你启用这个模式时,外部附件框架不会关闭活跃的外部附件会话。当外部附件发送来新的内容时,这个框架唤醒你的应用程序以便它能处理这些数据内容。在外部附件建立连接或失去连接时,系统也会唤醒应用程序来进行处理。

任何支持附件更新后台处理的应用程序都必须遵循以下基本准则:

  • 应用程序必须提供一个界面,让用户来开始或者停止附件更新事件的发送。

  • 一旦被唤醒,应用程序大约有10S时间来处理数据。理想情况下,它应该尽可能快速地处理数据然后转换到挂起状态。但是,如果需要更多的时间,应用程序可以使用beginBackgroundTaskWithExpirationHandler:方法来申请额外的执行时间。

与蓝牙附件通信

使用蓝牙外设的应用程序可以请求在外设发送了一个更新时被唤醒,即使它处在挂起状态。这种支持对于需要定期发送数据的Bluetooth-LE外设来说是非常重要的。你需要在Xcode工程中的Capabilities选项卡中启这项用服务(你也可以通过在Info.plist文件中设置UIBackgroundModes的值为bluetooth-central来启用这个服务)。当你启用这个模式时,Core Bluetooth框架会保持相应外设的任何活跃会话连接。此外,当有新的数据从外设传来,系统会唤醒应用程序让他能够处理数据。在外设建立连接或失去连接时,系统也会唤醒应用程序来进行处理。

iOS 6中,一个使用蓝牙外设的应用程序也可以在外设模式下运行。要充当蓝牙外设,你需要在Xcode工程中的Capabilities选项卡中启这项用服务(您也可以通过在应用程序Info.plist文件中包含UIBackgroundModes键和bluetooth-peripheral值来启用此支持)。启用此模式可让Core Bluetooth框架在后台简单地唤醒应用程序,以便它可以处理外设的相关请求。

支持蓝牙数据后台处理的任何应用程序都必须基于会话,并遵循以下基本准则:

  • 应用程序必须提供一个界面,允许用户启动和停止蓝牙事件的传递。

  • 被唤醒后,应用程序大概需要10秒钟才能处理数据。理想情况下,它应该尽可能快地处理数据,并允许自己再次暂停。但是,如果需要更多的时间,应用程序可以使用beginBackgroundTaskWithExpirationHandler:方法来请求更多的时间,它应该只有在绝对必要的时候才这样做。

在后台获得用户的注意

通知是处在挂起,在后台运行或没有运行的应用程序获得用户的注意的一种方式。应用程序可以使用本地通知来显示提醒框,播放声音,标记应用程序的图标或者全部一起使用。比如,一个闹钟应用程序可能会使用本地通知来播放闹铃声并显示一个提醒框来使闹钟不可用。当一个通知被发送给用户,用户必须决定是否授权让应用程序来到前台。如果应用程序已经在前台,本地通知将会被静默的发送给你的应用程序而不是发送给用户。

为了安排本地通知的发送,需要创建一个配置了通知的各个参数的UILocalNotification类的实例并调用UIApplication类的方法。本地通知对象包含了发送通知的类型和在什么时间发送它的信息。UIApplication类的方法提供了是立即发送还是按时间表发送通知的选项。

下面的代码片段展示了如何安排一个由用户设置的使用了日期和时间的闹钟的例子。这个例子在一个时间只配置了一个闹钟并在安排它之前终止了前一个闹钟(你的应用程序在任何给定的时刻都不能拥有超过128个处在活跃状态的本地通知,它们每一个都能设置成以固定的时间间隔重复)。如果闹钟被触发时,应用程序没有运行或者处在后台,闹钟会弹出提醒框并在后台播放音频。如果应用程序是活动且处在前台,则会调用应用程序的代理对象的application:didReceiveLocalNotification:方法。

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

本地通知使用的音频文件和推送通知使用的要求相同。自定义的音频文件必须位于你应用程序主要包中且必须是以下几种格式:Linear PCM,MA4,μ-Law或者a-Law。你也可以指定UILocalNotificationDefaultSoundName常量来使设备播放默认的提醒声音。当通知被发送且音频被播放时,系统也会触发设备震动来配合。

何时启动应用程序进入后台

支持后台执行的应用程序可能会被系统重启来处理发生的事件。如果应用程序不是被用户强制退出,系统会启动应用程序在下列事件发生时:

  • 对于定位应用程序
    • 系统收到一个满足发送给应用程序标准的位置更新。
    • 设备进入或退出一个已注册的区域。
  • 对于音频应用程序,音频框架需要应用程序来处理某些数据。
  • 对于蓝牙应用程序
    • 应用程序作为从连接的外设接收数据的核心角色。
    • 应用程序作为从连接核心接收命令的外设。
  • 对于后台下载应用程序
    • 应用程序收到一个包含 content-available 键并且值为 1的远程通知。
    • 系统在随机时间唤醒应用程序来开始新内容的下载。
    • 使用NSURLSession对象在后台下载内容的应用程序,当这个会话相关的任务全部成功完成或发生了一个错误。
    • Newsstand应用程序内容下载完成。

在大多数情况下,系统并不会重启被用户强制退出的应用程序。定位应用程序是个例外,在iOS8和之后。如果不是的话,用户必须显式的启动应用程序或者重启设备,系统才能自动的启动程序进入后台。当启用密码保护时,在用户第一次解锁设备之前,系统不会启动任何应用程序进入后台。

成为一个负责的后台运行应用程序

在使用系统资源和硬件时,前台应用程序始终比后台应用程序的优先级高。后台应用程序需要为这个差异做准备并调整它们的行为:

  • 不要在你的代码中进行任何OpenGL ES调用。在后台运行时,你绝不能创建EAGLContext对象或发出任何OpenGL ES绘图命令。使用这些会使你的应用程序被立即终止。应用程序必须保证任何先前提交的命令在移入后台之前完成。
  • 在挂起之前停止任何Bonjour-related服务。在你的应用程序移入后台但未被挂起之前,它应该从Bonjour注销并关闭任何与网络服务相关的监听sockets。一个挂起的应用程序无论如何不能响应任何传入的服务请求。如果你没有关闭Bonjour服务,在应用程序挂起时,系统会自动的关闭它们。
  • 准备好处理基于网络的sockets的连接失败。系统可能会销毁socket连接,当你的应用程序因为某些原因被挂起时。只要你的基于网络的socket代码准备好了如何处理其他类型的网络失败,比如使用失败信号或网络转化,socket被销毁就不会导致出现任何不寻常的问题。当你的应用程序重新开始运行时,如果使用socket遭遇故障,只需重新建立一个。
  • 在转入后台之前保存你的应用程序的状态。处于低内存的情况下,后台应用程序可能会被从内存中移除以释放空间。挂起的应用程序首先被移除,并且不会在移除之前通知应用程序。因此,应用程序应该利用iOS 6及之后提供的状态保存机制将应用程序状态信息保存到磁盘。
  • 在转入后台时移除对任何不需要的对象的强引用
  • 在挂起之前停止使用共享系统资源。与共享系统资源进行交互的应用程序应该在被挂起之前停止使用这些资源。前台应用程序总是拥有这些资源的优先使用权。当你的应用程序挂起时,如果被发现它在使用这些共享资源,则会被系统杀死进程。
  • 避免更新窗口和视图。因为你的应用程序的窗口和视图在后台是不可见的,你应该避免更新它们。如果你的应用程序需要在系统拍摄快照前更新窗口的内容的话则是一种例外情况。
  • 响应外部附件的连接和断开通知。对于和外部附件通信的应用程序来说,当应用程序转入后台时系统会自动的发送一个断开通知。应用程序必须注册这个通知并用它来关闭当前附件的会话。当应用程序转入前台时,一个匹配连接的通知被发送,给应用程序重新连接的机会。
  • 在转入后台时清理活动的提醒框的资源。为了在各个应用程序之间进行切换时保存上下文,在你的应用程序进入后台时,系统不会自动地释放上拉菜单或提醒视图。在应用程序转入后台之前,你需要提供合适的清理行为。
  • 在转入后台之前从视图中移除敏感信息。当应用程序转入后台时,系统会对应用程序的主窗口拍摄快照,当应用程序转入到前台时,这个快照会短暂的显示。在从applicationDidEnterBackground:方法返回之前,你应该隐藏或混淆可能被作为快照一部分拍摄的密码和其它敏感的个人信息
  • 在后台运行时尽可能的少做工作。给与后台应用程序的执行时间相比于前台应用程序来说有更多的限制。应用程序不应该在后台执行太长时间,这可能导致应用程序被终止。

选择退出后台执行

如果你不希望你的应用程序在后台运行,你可以通过将值为YESUIApplicationExitsOnSuspend键加入Info.plist来显式的退出后台模式。当一个应用程序选择退出后台,它的生命周期将在未运行,非活动和活动状态之间转换,永远不会进入后台或被挂起状态。当用户按下Home按钮来退出应用程序时,应用程序代理对象的applicationWillTerminate:方法将会被调用,应用程序将有大约5秒时间在它被终止或移入未运行状态之前来执行清理任务并退出。

强烈不鼓励退出后台执行,但在某些情况下可能是首选。 具体来说,如果后台执行的编码对你的应用程序来说增加了很大的复杂性,那么终止应用程序可能会更简单。 此外,如果你的应用程序消耗大量内存,并且无法轻松释放任何内存,系统可能会迅速杀死你的应用程序,为其他应用程序腾出空间。 因此,选择终止,而不是切换到后台,可能会产生相同的结果,并节省你的开发时间和精力。

应用程序生命周期和状态转换操作策略

iOS 应用程序生命周期


基本概念介绍

The Main Function - 程序的入口

main函数是每个基于C的应用程序的入口,iOS也是这样。在Xcode中开发iOS程序的话,Xcode会自动为你创建这个函数作为你工程的一部分。

1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

main函数控制UIKit framework的工作。UIApplicationMain函数通过创建应用程序的核心对象,从可用的storyboard文件加载应用程序的用户界面,调用自定义代码,以便让你可以在程序启动时进行一些初始设置,并将应用程序的运行循环启动。

The Structure of an App - 程序的结构

在启动过程中,UIApplicationMain函数设置了几个关键对象,启动应用程序并开始运行它。 每个iOS应用程序的核心都是UIApplication对象,其工作是为了方便系统与应用程序中的其他对象之间的交互。

一个iOS应用程序中包含的关键对象:

一个iOS应用程序中各个对象的所起到的作用:

Object Description
UIAppplication UIApplication对象管理着App的事件循环和一些高级App行为,并向它的委托对象报告App的状态转换和一些特殊事件。
Appdelegate Appdelegate是你编写的程序代码的核心 ,它与UIApplication对象一起工作,处理应用程序初始化,状态转换和许多高级App行为。 这个对象也是唯一一个保证在每个应用程序中都出现的对象,因此通常用于设置应用程序的初始数据结构。
ViewController View Controller对象管理着你的App内容在屏幕上是如何呈现的。一个View Controller对象管理着一个视图和这个视图的子视图。
UIWindow UIWindow对象协调屏幕上一个或多个视图的呈现。 大多数应用程序只有一个窗口,它在主屏幕上显示内容,但应用程序可能会在外部显示器上显示内容的附加窗口。要更改应用程序的内容,可以使用视图控制器更改相应窗口中显示的视图。 你永远不会更换窗口本身。除了托管视图之外,Windows还可以使用UIApplication对象将事件传递给的视图和控制器。
View,Control and Layer View和Control提供了应用内容的可视化表示。 除了包含View和Control之外,应用程序还可以将Core Animation图层纳入其视图和控制层次结构中。 层对象实际上才是表示可视内容的数据对象。

The Main Run Loop - 主运行循环

应用程序的主运行循环处理所有与用户相关的事件。 UIApplication对象在启动时设置主运行循环,并使用它来处理事件并处理基于视图的接口的更新。

顾名思义,主运行循环在应用程序的主线程上执行,这确保了与用户相关的事件按照接收的顺序连续处理。

当用户与设备进行交互时,与这些交互相关的事件由系统生成,并通过由UIKit设置的特殊端口传送到应用。 事件在应用程序内部进入队列,并逐个分派到主运行循环执行。UIApplication对象是接收事件的第一个对象,并且决定需要做什么来处理这个事件。 触摸事件通常被分派到主窗口对象,主窗口对象又将其发送到触摸发生的视图。

可以在iOS应用程式中分发许多类型的事件。 最常见的如下表所示。 这些事件大多是使用应用程序的主运行循环分发的,但也有些不是。 一些事件被分发到一个委托对象或被分发给你提供的一个block。

事件类型 分发对象 备注
触摸事件 事件发生的视图 视图是响应者对象。任何没有被当前视图处理的触摸事件都会被沿着响应链继续向下分发进行处理。
遥控器事件和摇动运动事件 首要响应对象 遥控器事件用于控制媒体播放,并由耳机和其他附件产生。
加速度计、磁力仪和陀螺仪 你指定的对象 与加速度计,磁力计和陀螺仪硬件相关的事件传递给你指定的对象。
定位 你指定的对象 你注册定位服务以接收使用Core Location框架位置的事件。
重绘 需要更新的视图 重绘事件不涉及事件对象,而是简单地调用视图来绘制自身。

一些事件,如触摸和遥控事件,由你的应用程序的响应者对象处理。响应者对象在你的应用程序中无处不在。 大多数事件都针对特定的响应者对象,但如果需要处理事件,则可以将其传递给其他响应方(通过响应者链)。 例如,不处理事件的视图可以将事件传递到其父视图或父视图控制器。

触摸控件(如按钮)中发生的事件的处理方式与在许多其他类型的视图中发生的触摸事件的处理方式不同。 通常只有有限数量的交互可能与控件相关联,因此这些交互被重新封装到动作消息中并传递到适当的目标对象。 这种目标动作(Target-Action)设计模式可以轻松地使用控件来触发你应用中自定义代码的执行。

Execution States for Apps - 应用程序的执行状态

在任何给定的时刻,你的应用程序都处在下表所列的状态之一。 系统会改变应用程序的状态以响应整个系统中发生的操作。

状态 描述
Not running 应用程序没有运行或被系统终止运行。
Inactive 应用程序正在前台运行,但当前没有收到事件 (它可能正在执行其他代码)。应用程序通常在转换到不同的状态时暂时保持在此状态。
Active 应用程序正在前台运行,并且正在接收事件(这是前台应用程序的正常模式)。
Background 应用程序在后台执行代码。 大多数应用程序短暂地进入此状态。 但是,请求额外执行时间的应用程序可能会保持此状态一段时间。
Suspended 应用程序是在后台,但不执行代码。 系统将应用程序自动转换到这个状态,在这样做之前并不会通知应用程序。 挂起时,应用程序保留在内存中,但不执行任何代码。当出现低内存条件时,系统可能会清除已挂起的应用程序,以为前台应用程序腾出更多空间。

下图展示了应用程序在各种状态之间的转换:

大多数状态转换伴随着对应用程序委托对象的方法的相应调用 ,这些方法是你以适当方式响应状态变更的地方。

  • application:willFinishLaunchingWithOptions:

    告诉代理对象启动过程已经开始,但还没有发生状态恢复。

  • application:didFinishLaunchingWithOptions:

    告诉代理对象启动过程快要完成,应用程序快要准备好运行。

  • applicationDidBecomeActive:

    告诉代理对象应用程序已经处于Active状态。

  • applicationWillResignActive:

    告诉代理对象应用程序即将进入Inactive状态。

  • applicationDidEnterBackground:

    告诉代理对象应用程序已经进入后台。

  • applicationWillEnterForeground:

    告诉代理对象应用程序即将进入前台。

  • applicationWillTerminate:

    告诉代理对象应用程序即将被终止。

App Termination - 应用程序的终止

应用程序必须随时准备终止运行,不应等待保存用户数据或执行其他关键任务。 System-initiated termination是应用程序生命周期的正常部分。该系统通常终止应用程序,以便它可以回收内存并为用户启动的其他应用程序腾出空间,但是系统也可能会终止行为不当或未及时响应事件的应用程序。

挂起的应用程序终止时不会收到通知,系统将杀死该进程并回收相应的内存。如果一个应用程序当前在后台运行并且没有挂起,系统会在终止之前调用应用程序委托的applicationWillTerminate:。当设备重新启动时,系统不会调用此方法。

除了系统终止你的应用程序,用户可以使用多任务UI显式地终止你的应用程序。System-initiated termination与终止挂起的应用程序具有相同的效果。该应用程序的进程被杀死,并且不会有通知发送到该应用程序。

Threads and Concurrency - 线程和并发

系统创建你的应用程序的主线程,你可以根据需要创建其他线程来执行其他任务。

对于iOS应用,首选技术是使用Grand Central Dispatch(GCD),操作对象和其它异步编程接口,而不是自己创建和管理线程。 GCD等技术可以让你定义想要执行的工作以及要执行的顺序,但让系统决定如何在可用的CPU上执行该工作。让系统处理线程管理简化了必须编写的代码,使得更容易确保代码的正确性,并提供更好的整体性能。

在考虑线程和并发时,请考虑以下几点:

  • 涉及视图,核心动画和许多其他UIKit类的工作通常必须发生在应用程序的主线程上。

  • 长时间的任务(或潜在的长度任务)应始终在后台线程上执行。任何涉及网络访问,文件访问或大量数据处理的任务都应使用GCD或操作对象异步执行。

  • 在启动时,尽可能将任务从主线程移出。在启动时,您的应用程序应尽可能快地设置用户界面。只有有助于设置用户界面的任务才能在主线程上执行。

    所有其他任务应该是异步执行的。


操作应用程序的状态转换


对于应用程序的任何一个可能的运行状态,系统都对你的应用程序有不同的期望。当应用程序的状态发生转换时,系统会通知应用对象,应用对象又通知其代理。你可以使用UIApplicationDelegate协议的状态转换方法来检测这些状态变化并进行适当的响应。

应用程序启动时应该做什么

当应用程序启动(进入前台货后台)时,你应该使用应用程序委托对象的application:willFinishLaunchingWithOptions:application:didFinishLaunchingWithOptions:方法执行一些操作:

  • 检查启动选项字典的内容(launch options dictionary),了解应用程序启动的原因,并作出适当的响应。

  • 初始化应用程序的关键数据结构。

  • 准备应用程序的窗口和视图以进行内容显示。

    • 使用OpenGL ES进行绘图的应用程序不能使用这些方法来准备绘图环境,使用OpenGL ES绘图应该在applicationDidBecomeActive:方法中进行准备。

    • Show your app window from your application:willFinishLaunchingWithOptions: method. UIKit delays making the window visible until after the application:didFinishLaunchingWithOptions: method returns.

    • application:willFinishLaunchingWithOptions:方法中配置如何显示应用程序的Window。UIKit延迟Window的显示直到application:didFinishLaunchingWithOptions:方法进行了返回。

在启动时,系统会自动的加载应用程序的主要storyBoard文件,并加载初始视图控制器。对于支持状态恢复的应用程序,状态恢复机制会在调用application:willFinishLaunchingWithOptions:application:didFinishLaunchingWithOptions:方法之间将应用程序恢复到以前的状态。使用application:willFinishLaunchingWithOptions:方法显示应用程序窗口,并确定状态恢复是否应该发生。使用application:didFinishLaunchingWithOptions:方法对应用程序的用户界面进行任何最终调整。

application:willFinishLaunchingWithOptions:application:didFinishLaunchingWithOptions:方法应始终尽可能轻量级,以减少应用程序的启动时间。如果应用程序没有及时完成其启动周期,系统将使其无法响应。因此,任何可能减慢启动速度的任务(如访问网络)都应该在辅助线程上执行。

The Launch Cycle - 启动周期

当你的应用程序启动后,在短暂的停留在非活动状态(Inactive)后,将会从非运行状态(Not Running)转换到活动状态(Active)或后台状态(Background)。作为启动周期的一部分,系统会为你的应用程序创建一个进程和一个主线程,并在主线程中调用应用程序的main函数。

下图显示了应用程序启动到前台时发生的事件序列,包括调用的应用程序委派方法。

当你的应用程序启动到后台时,通常要处理一些后台事件,启动周期会有些许改变。主要的不同在于,在后台的应用程序不是处于活动状态,只是进入后台来处理一些事件,之后可能会被挂起。当启动到后台时,系统仍然会加载用户界面文件,但不会显示应用程序的窗口。

你可以通过在代理方法 application:willFinishLaunchingWithOptions:application:didFinishLaunchingWithOptions:中检查UIApplicationapplicationState属性来决定应用程序是启动到前台还是后台。当你的应用程序启动到前台时,这个属性的值是UIApplicationStateInactive,当你的应用程序启动到后台时,这个属性的值是UIApplicationStateBackground

Launching in Landscape Mode - 以横屏模式启动

对于界面只使用横屏的应用程序来说,必须明确地请求系统以横屏模式启动。通常来说,应用程序以纵屏模式启动,只有在需要适应设备的方向的时候才旋转屏幕的方向。 对于支持纵向和横向方向的应用程序,应始终为纵向模式配置视图,然后让视图控制器处理任何旋转。
但是,如果您的应用程序支持横向而不是纵向方向,请执行以下任务,让使其以横向模式启动:

  • 像你应用程序中的Infl.plist文件中添加UIInterfaceOrientation字段,并将其值设为UIInterfaceOrientationLandscapeLeftUIInterfaceOrientationLandscapeRight
  • 在横屏模式下布局你的视图并确保视图的布局和自动调整大小选项设置正确。
  • 重写试图控制器的shouldAutorotateToInterfaceOrientation:方法并在左向横屏或右向横屏时返回YES,在纵屏模式下返回NO

重要提示: 应用程序应该总是用视图控制器来管理基于窗口(Window)的内容。

Info.plist文件中的UIInterfaceOrientation字段告诉iOS系统该如何配置应用程序状态栏的方向(如果显示的话)以及任何视图控制器管理的视图在启动时的方向。视图控制器根据这个字段设置它的视图的初始方向。 使用此字段相当于在执行你的applicationDidFinishLaunching:方法的早期调用应用程序的setStatusBarOrientation:animated:方法。

Installing App-Specific Data Files at First Launch - 配置应用程序数据文件

你可以使用应用程序的第一个启动周期来设置任何应用程序运行所需要的数据或配置文件。 应用程序特定的数据文件应在应用程序沙箱的Library/Application Support/<bundleID>/目录中创建,其中是应用程序的标识符。你可以根据需要细分目录来组织你的数据文件。

如果应用程序包中包含你打算进行修改的数据文件,你应该将要修改的数据文件从应用程序的数据包中复制出来进行修改。你不能在应用程序包中修改数据文件,因为iOS应用程序是代码签名的,在应用程序包中修改数据文件会使应用程序的签名失效,使得你的应用程序无法启动。将打算修改的数据文件复制到Application Support(或者其它可写的目录)目录下进行修改是唯一安全的方式。

应用程序临时被打断时应该做什么

基于警报的打断会导致你的应用程序临时性的失去控制。这种情况下,你的应用程序仍然运行在前台,但是不接收任何来自系统的触摸事件(但是会继续接收推送通知等其他类型的时间,比如加速度计时间等)。你应该在applicationWillResignActive:方法中进行如下操作来处理这种情况:

  • 保存数据以及任何相关的状态信息。
  • 停止计时器和其他周期性任务。
  • 停止任何进行的数据请求任务。
  • 不要创建任何新的任务。
  • 停止视屏播放(除了通过AriPlay播放的视屏)
  • 如果你的应用程序是游戏的话,是游戏进入暂停状态。
  • 提高OpenGL ES 帧速率。
  • 暂停执行非关键代码的任何调度队列或操作队列。 (可以在不活动的情况下继续处理网络请求和其他时间敏感的后台任务)

当你的应用程序返回活动状态时,在applicationDidBecomeActive:方法中应该进行与applicationWillResignActive:相反的操作步骤。因此,一但重新活动,你的应用程序应重新启动计时器,恢复调度队列,并再次调低OpenGL ES帧速率。 但是,游戏不应该自动恢复,它们应该保持暂停,直到用户选择恢复它们为止。

当用户按下睡眠或唤醒按钮时,含有NSFileProtectionComplete保护选项的文件的应用程序必须关闭这些被保护文件的任何引用。对于设置了相应密码的设备,按下睡眠或唤醒按钮时应该锁定屏幕并强制系统丢弃对拥有完全保护文件的解密密钥。当屏幕锁定时,任何尝试访问相关文件的操作都会失败。所以你如果有这样的文件,你应该在applicationWillResignActive:方法中关闭对它们的任何引用,在applicationDidBecomeActive:中打开新的引用。

重要提示: 你应该总是在适当的时候保存用户数据。虽然你可以使用应用程序状态转换来强制对象将为保存的变更写入磁盘,但是合适的做法是永远不要等到状态转换时才去保存数据。

Responding to Temporary Interruptions - 响应临时打断

当基于警报的打断发生时(比如打进电话),应用程序暂时进入非活动状态已便系统可以提示用户如和继续操作。应用程序保持在这个状态直到用户拒绝了这个警报,这时应用程序会返回前台进入活动状态或进入后台。

以横幅形式显示的通知不会像基于警报的通知那样是你的应用程序进入非活动状态。尽管横幅放置在你应用程序窗口的顶部边缘,但是你的应用程序还是能够继续接收触摸事件。然而如果用户下拉横幅以便查看通知中心,你的应用程序就会像基于警报的打断发生时那样进入非活动状态。你的应用程序保持在非活动状态直到用户退出通知中心或启动另一个应用程序,这时,你的应用程序相应的会进入非活动转台或后台。

按下睡眠或唤醒按钮是另一种类型的可以导致你的应用程序暂时进入非活动状态的打断。当用户按下这个按钮,系统会使触摸事件变得不可用并让应用程序进入后台,同时将applicationState的值设置为UIApplicationStateBackground并锁定屏幕。

应用程序在前台时做什么

当你的应用程序返回前台进入活动状态时,你可以重新启动在进入后台时停止的任务。移动到前台时出现的步骤如下图所示,applicationWillEnterForeground:方法应该撤消在applicationDidEnterBackground:方法中完成的任何内容,并且applicationDidBecomeActive:方法应该继续执行与启动时相同的激活任务。

Be Prepared to Process Queued Notifications - 准备处理通知队列

一个处于挂起状态的应用程序必须做好在返回前台或进入后台执行状态时处理任何在队列中的通知。处于挂起状态的应用程序不执行任何代码,因此不能处理通知引发的屏幕方向改变,时间改变,偏好改变以及其它会影响应用程序的外观和状态的改变。为了确保这些改变不会丢失,系统将相关的通知放入队列并在应用程序开始执行代码(无论在前台还是后台)时分发通知给它。为了防止应用程序在恢复通知时变得超负荷,系统会将事件合并,并提供一个单一的通知(每种相关类型),反映自您的应用程序被挂起以来的净变化。

下表列出了分发给你的应用程序的可以合并的通知。这些通知的大多数都直接分发给已经注册的观察者。

Event Notifications
An accessory is connected or disconnected. EAAccessoryDidConnectNotification EAAccessoryDidDisconnectNotification
设备方向改变 UIDeviceOrientationDidChangeNotification除了这个通知,视图控制器自动的改变自己的方向。
重要的时间改变 UIApplicationSignificantTimeChangeNotification
电池状态改变 UIDeviceBatteryLevelDidChangeNotification UIDeviceBatteryStateDidChangeNotification
The proximity state changes. UIDeviceProximityStateDidChangeNotification
被保护的文件的状态的改变 UIApplicationProtectedDataWillBecomeUnavailable UIApplicationProtectedDataDidBecomeAvailable
内建显示器连接或失去连接 UIScreenDidConnectNotification UIScreenDidDisconnectNotification
屏幕显示模式改变 UIScreenModeDidChangeNotification
通过设置应用程序改变了应用程序的设置 NSUserDefaultsDidChangeNotification
当前语言或本地化设置改变 NSCurrentLocaleDidChangeNotification
用户的iCloud账户状态改变 NSUbiquityIdentityDidChangeNotification

通常在任何触摸事件或用户输入事件之前,在主线程中分发队列化的通知。大多数的应用程序应该足够快地处理这些事件,避免在重启是导致可观察到的延迟。

应用程序返回前台时也会接收到要更新自从上次更新以来被标记为dirty的视图的通知。一个运行在后台的应用程序仍然能调用setNeedsDisplaysetNeedsDisplayInRect:方法来请求更新视图。然而,因为这些请求更新的视图不可见,系统合并了这些请求并在应用程序进入前台后才更新它们。

Handle iCloud Changes - 处理iCloud改变

当iCloud的状态发生改变时,系统会发送一个NSUbiquityIdentityDidChangeNotification通知给你的应用程序。用户登入或登出iCloud账号,关闭了文档和数据的同步,都会引起iCloud状态的改变。这个通知表示应用程序应该更新缓存和与iCloud有关的用户界面元素。

如果你的应用程序已经提示了用户是否要在iCloud存储文件的话,不要在iCloud状态发生改变时再次提醒。在第一次提醒了用户后,将用户的选择保存在本地的偏好设置中。

Handle Locale Changes - 处理本地化改变

如果用户在你的应用程序挂起时改变了当前的本地化方案,当你的应用程序返回前台时,你可以使用NSCurrentLocaleDidChangeNotification通知来强制更新包含了对本地化方案敏感的信息,比如日期,时间和数字。当然,最好的避免发生本地化相关的问题的方法是用能简单更新视图的方法来编写代码:

  • 使用autoupdatingCurrentLocale类方法当你检索到NSLocal对象。这个方法返回一个可以自动更新自己来响应本地化方案改变的本地化对象,所以你永远不需要重新创建它。然而,当本地化方案发生改变时,你仍然需要更新包含来自于当前本地化方案的信息的视图。
  • 重新创建任何缓存的日期和数字格式,无论当前本地化方案何时发生变化。

Handle Changes to Your App’s Settings - 处理偏好设置的改变

如果你的应用程序含有被设置应用程序管理的设置选项,你应该监测NSUserDefaultsDidChangeNotification通知,因为用户可以改变这些设置当你的应用程序被挂起或在后台。你可以使用这个通知来响应并处理在这些设置上的重要改变。

一旦收到NSUserDefaultsDidChangeNotification通知,你的应用程序应该重新加载任何相关的设置,如果需要,恰当地重置用户界面。

应用程序在后台时做什么

当应用程序从前台转为后台执行时,使用你的代理对象的applicationDidEnterBackground:方法来做:

  • 准备应用程序的照片。当你的applicationDidEnterBackground:返回时,系统会对你的应用程序的用户界面拍照并用这个照片来做过度动画。如果你的应用程序里的视图包含敏感信息,你应该在applicationDidEnterBackground:返回之前隐藏或更改这些视图。
  • 保存任何应用程序状态的相关信息。在进入后台之前,你的应用程序应该已经保存好了所有重要的用户数据。使用转换到后台来保存应用程序状态的最后一分钟更改。

你的应用程序代理对象的applicationDidEnterBackground:方法大约有5分钟的时间来结束任何任务并返回。实际上,这个方法应该可能快的返回。如果这个方法没有在时间耗尽前返回,你的应用程序将会被终止并被移出内存。如果你仍然需要更多的时间来执行任务,调用beginBackgroundTaskWithExpirationHandler:方法来请求后台执行时间,然后在一个次要线程里开始任何长时任务。不管你有没有开始一个后台任务,applicationDidEnterBackground:方法都会在5分钟内退出。

提示: 除了调用applicationDidEnterBackground:方法外,系统还会发送UIApplicationDidEnterBackgroundNotification通知。你可以使用这个通知将清理任务分发给应用程序里的其它对象。

The Background Transition Cycle - 后台转换周期

When the user presses the Home button, presses the Sleep/Wake button, or the system launches another app, the foreground app transitions to the inactive state and then to the background state. These transitions result in calls to the app delegate’s applicationWillResignActive: and applicationDidEnterBackground: methods, as shown in Figure 4-5. After returning from the applicationDidEnterBackground: method, most apps move to the suspended state shortly afterward. Apps that request specific background tasks (such as playing music) or that request a little extra execution time from the system may continue to run for a while longer.

当用户按下Home键,按下睡眠或唤醒按钮,或者系统启动了另一个应用程序,运行在前台的应用程序会转换为非活动状态然后进入后台。这些状态转换会导致调用applicationWillResignActive:applicationDidEnterBackground:方法,如下图所示。从applicationDidEnterBackground:方法返回后,大多数应用程序不久之后就会转换到挂起状态。 请求特定后台任务(如播放音乐)或从系统请求一点额外执行时间的应用程序可能会持续运行一段时间。

Prepare for the App Snapshot - 准备应用程序快照

系统在应用程序代理对象applicationDidEnterBackground:方法返回的不久之前,对应用程序的窗口进行快照。相似的,当应用程序被唤醒执行后台任务时,系统会对应用程序进行一次新的反应改变的快照。

如果在进入后台更改视图时,可以调用主视图的snapshotViewAfterScreenUpdates:方法来强制执行这些更改。在一个视图上调用setNeedsDisplay方法对快照是无效的,因为快照发生在下一个绘制周期之前。调用值为“YES”的snapshotViewAfterScreenUpdates:方法将立即使用机器的底层缓冲区更新快照。

Reduce Your Memory Footprint

每个应用程序应该在进入后台时释放尽可能多的内存。 系统尝试尽可能多地在内存中保留应用程序,但是当内存不足时,会终止挂起的应用程序以回收该内存。 在后台消耗大量内存的应用程序是第一个要终止的应用程序。

实际上,你的应用程序应该在不再需要的时候删除对对象的强引用。 删除强引用使编译器能够立即释放对象,以便可以回收对应的内存。 但是,如果要缓存某些对象以提高性能,则可以等到应用程序转换到后台再删除对它们的引用。

应该尽快删除强引用的对象的一些示例包括:

  • 你创建的图像对象。
  • 可以从磁盘重新加载的大型媒体或数据文件
  • 您的应用程序不需要的任何稍后可以重新创建其他对象。

为了帮助你减少应用程序的内存占用空间,系统会自动清除在应用程序移动到后台时代表应用程序分配的数据。

  • 系统清除所有Core Animation层的后备存储。 此功能不会从应用程序的图层对象中删除内存,也不会更改当前图层属性。 它只是防止这些图层的内容出现在屏幕上,这表明应用程序在后台应该不会发生。
  • 它删除任何系统对缓存图像的引用。
  • 它删除了对其他系统管理的数据高速缓存的强引用。

属性修饰符分析

基础概念


atomic & nonatomic

决定编译器生成的setter/getter是否为原子操作 。
atomic是Objc使用的一种线程保护技术,防止在某个操作未完成时被另外一个线程操作,造成数据错误。然而这种多线程安全机制对资源的占用很大,在iPhone等小型设备中,除非需要使用多线程之间通讯编程,采用这种技术需要慎重考量。
一般情况下,使用nonatomic来修饰成员变量,存取器直接返回一个值,而不需要管当不同的线程同时访问时会发生什么。

readwrite & readonly

任何属性都可以声明为readwrite或readonly,且默认设置为readwrite 。
将一个属性声明为readwrite时,编译器将会为这个属性生成setter和getter。
将一个属性生命为readonly时,编译器将只会生成getter。

assign

直接赋值,不更改引用计数,一般用于基础数据类型和C语言类型数据。

retain

释放旧对象,并使传入的新对象引用计数+1。
此属性只能用于NSObject及其子类,而不能用于Core Foundation(因为其没有使用引用计数,需要另外使用CFRetain和CFRelease来进行CF的内存管理)。

strong

释放旧对象,并使传入的新对象引用计数+1。
在ARC下,strong为对象类型属性声明时的默认值。

copy

创建一个引用计数为1的新对象,这个新对象是传入对象的拷贝。
会拷贝传入的对象(即创建一个引用计数为1的新对象,但是内容与传入对象相同),并把新对象赋值给实例变量。

weak

弱引用,要求不保留传入的属性(既不会使传入的对象引用计数+1)。
类似于assign,但与assign不同的是,当它们指向的对象被释放后,weak会被自动置为nil,而assign则不会。


进阶问题


strong & weak

这里采用 stack overflow 上的一个对强/弱引用的解释

如果将对象想象成一只想要逃跑(be deallocated)的狗的话。

强引用就像是拴住狗的绳子。只要你还还牵着拴住狗的绳子,狗就逃跑不了。如果有五个人都将绳子拴住了狗,那么除非五个人都解开了绳子,否则狗就不可能逃跑。

而弱引用,就像一个小孩子指着狗说:看,那里有一直狗。只要狗仍被狗绳拴着,小孩就还能看见狗,他们仍会指着它。然而一但狗绳被解开,狗就会逃跑而不管多少小孩子指着它。

copy & retain

copy实际上是建立了一个新的相同的对象,而retain是将传入对象的引用计数+1 。

深拷贝 & 浅拷贝

浅拷贝就是对内存地址的复制,让目标对象指针和源对象指向同一片内存空间 。浅拷贝只是对对象的简单拷贝,让几个对象共用一片内存,当内存销毁的时候,指向这片内存的几个指针需要重新定义才可以使用,要不然会成为野指针。
在iOS中,使用retain修饰的属性变量,就是一种浅拷贝。它既让几个指针共用同一片内存空间,又可以在release时由于计数的存在,不会轻易的销毁内存,达到更加简单使用的目的。
深拷贝是指拷贝对象的具体内容,而内存地址是自主分配的,拷贝结束之后,两个对象虽然存的值是相同的,但是内存地址不一样,两个对象也互不影响,互不干涉。
值得注意的是,对可变对象如NSMutableArray等无论是copy还是mutableCopy都是进行了深拷贝。

@property & @synthesize

如果我们希望使用默认的实例变量命名方式,那么我们在.m文件中就不需要使用@synthesize声明,系统会帮我们自动完成。如果我们希望自己命名实例变量命,那么我们就使用@synthesize显式声明我们希望的实例变量名 。

block作为实例变量为何要用copy修饰?

因为block如果在栈上的话,其所属的变量作用域结束,该block就被释放掉,block中的__block变量也同时被释放掉。为了解决栈块在其变量作用域结束之后被释放掉的问题,我们就需要把block复制到堆中。

为什么NSString、NSDictionary、NSArray要使用copy修饰?

对于NSString、NSDictionary、NSArray等经常使用copy关键字,是因为它们有对应的可变类型:NSMutableString、NSMutableDictionary、NSMutableArray,它们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性时拷贝一份。

weak、copy、strong、assgin分别用在什么地方?

什么情况下会使用weak关键字?

  • 在ARC中,出现循环引用的时候,会使用weak关键字。
  • 自身已经对它进行了一次强引用,没有必要再强调引用一次。

assgin适用于基本的数据类型,比如NSInteger、BOOL等。

NSString、NSArray、NSDictionary等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary;

除了上面的三种情况,剩下的就使用strong来进行修饰。