第七章:多线程

时间:2021-08-08 04:24:55

一、线程概述

几乎所有的操作系统都支持同时运行过个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。

1、线程和进程

几乎所有的操作系统都支持进程的概念,所有运行中的任务通常对应一个进程(Process)。当一个程序进入内存运行后,即变成一个进程。进程是出于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。

一般而言,进程包含如下3个特征:

  • 独立性:进程是系统中独立存在的实体,他可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间;

  • 动态性:进程和程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。

  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会相互影响;

注意:并发性(concurrency)和并行性(parallel)是两个概念,并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。

2、多线程的优势

线程在程序中是独立的、并发的执行流,与分隔的进程相比,进程中的线程之间的隔离程度要小。他们共享内存、文件句柄和其他每个进程应用的状态。

总结起来,使用多线程编程有如下几个优势:

  • 进程间不能共享内存,但线程之间共享内存非常容易;

  • 系统创建进程需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高;

  • iOS提供了多种多线程实现方式,从而简化了iOS的多线程编程。

iOS大致提供了如下3种多线程编程的技术:

  • 使用NSThread实现多线程;

  • 使用NSOperation与NSOperationQueue实现多线程;

  • 使用GCD(Grand Central Dispatch)实现多线程。

二、使用NSThread实现多线程

iOS使用NSThread类代表线程,创建新线程也就是创建NSThread对象。

1、创建和启动线程

创建NSThread有两种方式:
①、

- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument NS_AVAILABLE(10_5, 2_0);

创建一个新线程对象;
②、

+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;

创建并启动新线程;

上面的两种方式本质都是将target对象的selector方法转换为线程执行体,其中selector方法最多可以接受一个参数,而argument就代表传给selector方法的参数。

提示:target对象的selector方法的方法体代表了线程需要完成的任务,因此相当于把target对象的selector方法转换为线程执行体。

两种创建方式的区别:

第一种方式是一个实例方法,该方法返回一个NSThread对象,必须调用start方法启动吸纳成;

第二种方式不会返回NSThread对象,因此这种方式会直接创建并启动线程。

例如:

- (void)viewDidLoad {
[super viewDidLoad];
for (int i = 0; i < 100 ; i ++) {
NSLog(@"===%@===%d",[NSThread currentThread],i);
if (i == 20) {
//创建新线程对象
NSThread* thread = [[NSThread alloc]initWithTarget:self selector:@selector(run) object:nil];
[thread setName:[NSString stringWithFormat:@"我是线程%d",i/20]];
//启动新线程
[thread start];
//创建并启动新线程
// [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
}
}
}
- (void)run{
for (int i = 0; i < 100; i ++) {
NSLog(@"---%@---%d",[NSThread currentThread],i);
}
}
+ (NSThread *)currentThread;

该方法用于返回当前正在执行的线程对象。

2、线程的状态

当线程被创建并启动之后,它既不是已启动就进入执行状态,也不是一直处于执行状态。即时线程开始运行以后,它不可能一直“霸占”CPU独自运行,所有CPU需要在多个线程之间切换,于是线程状态也会多次在运行、就绪之间切换。

注意:启动线程使用start方法,线程启动之后并不是立即进入运行状态,线程被启动后处于就绪状态,当系统调度线程后,线程才会进入运行状态。

提示:如果程序希望调用子线程的start方法之后子线程立即开始执行,程序可以使用[NSThread sleepForTimeInterval:0.001];让当前运行的线程(主线程)睡眠1毫秒——1毫秒就够了,因为这1毫秒内CPU不会空闲,他会去执行另一个处于就绪状态的线程,这样就可以让子线程立即获得执行。

3、终止子线程

线程会以以下3种方式之一结束,结束后就处于死亡状态:

  • 线程执行体方法执行完成,线程正常结束;

  • 线程执行过程中出现了错误;

  • 直接调用NSThread类的exit方法来终止当前正在执行的线程。

注意:当主线程结束时,其他线程不受影响,并不会随之结束,一旦子线程启动起来,就拥有和主线程相同的地位,它不受主线程的影响;

@property (readonly, getter=isExecuting) BOOL executing NS_AVAILABLE(10_5, 2_0);//该方法返回当前线程是否正处于执行状态;
@property (readonly, getter=isFinished) BOOL finished NS_AVAILABLE(10_5, 2_0);//该方法返回单签线程是否执行完成;

如果希望在UI线程中终止子线程,NSThread并没有提供方法来终止某个子线程,虽然NSThread提供了cancel方法,但该方法仅仅是改变该线程的状态,导致该线程的isCancelled方法返回NO,而不是真正终止该线程。为了在UI线程中终止线程,可以向子线程发送一个信号(比如调用子线程的cancel方法),然后在子线程的线程执行体中进行判断,如果子线程收到过终止信号,程序应该调用NSThread类的exit方法来终止当前正在执行的循环。

- (void)run{
for (int i = 0; i < 100; i++) {
if ([NSThread currentThread].isCancelled) {
//终止当前正在执行的线程
[NSThread exit];
}
NSLog(@"-----%@----%d",[NSThread currentThread].name,i);
//没执行一次,线程暂停0.5秒
[NSThread sleepForTimeInterval:0.5];
}
}
- (IBAction)cancelThread:(id)sender {
//取消thread线程,调用该方法后,thread的isCanceled方法将会返回NO
[thread cancel];
}

4、线程睡眠

如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用NSThread类的静态。sleepXxx方法来完成。NSThread类提供了如下两个控制线程暂停的类方法。

+ (void)sleepUntilDate:(NSDate *)date;//让当前正在执行的线程暂停到aDate代表的时间,并进入阻塞状态;

+ (void)sleepForTimeInterval:(NSTimeInterval)ti;//让当前正在执行的线程暂停ti秒,并进入阻塞状态;

当当前线程调用sleepXxx方法进入阻塞状态后,在其睡眠时间段内。该线程不会获得执行的机会,即使系统中没有其他可执行的线程,处于阻塞状态的线程也不会执行,因此sleepXxx方法常用来暂停线程的执行。
例如:

 [NSThread sleepForTimeInterval:0.5];

实例:使用线程下载网络图片
具体看demo

5、改变线程优先级

每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。每个子线程默认的优先级为0.5;

NSThread提供了如下实例方法和类方法来设置获取线程的优先级:

+ (double)threadPriority;//该类方法获取当前正在执行的线程的优先级;

+ (BOOL)setThreadPriority:(double)p;//该类方法用于设置当前正在执行的线程的优先级;

@property double threadPriority NS_AVAILABLE(10_6, 4_0);//该实例方法用于设置该方法的线程对象的优先级;

在上面的方法中需要传一个double类型的浮点数,范围为0.0~1.0,其中1.0代表最高优先级,0.0代表最低优先级。

三、线程同步与线程通信

1、线程安全问题

关于线程安全问题,有一个经典问题:银行取钱的问题。银行取钱的基本流程可以分为如下几个步骤:

-①、用户输入账户、密码,系统判断用户的账户、密码是否匹配;
- ②、用户输入取款金额;
- ③、系统判断账户余额是否大于取款余额;
- ④、如果余额大于取款金额,则取款成功;如果余额小于取款金额,则取款失败;

实例:看demo

在同时使用同一个账号取款时可能会出现的问题,1000元取出1600元;所以在取款的时候需要添加线程锁。

2、使用@synchronized实现同步

为Objective-C的多线程支持引入了同步,使用同步的通用方法就是@synchronized修饰代码块,被@synchronized修饰的代码块简称为同步代码块。同步代码块的语法格式如下:

@synchronized(obj){
//此处的代码就是同步代码块
}

@synchronized后面括号里的obj就是同步监视器。上面的代码的含义是:线程开始执行同步代码块之前,必须先获得同步监视器的锁定;

注意:任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程就会释放对同步监视器的锁定。

通过这种方式可以非常方便的实现线程安全的类,线程安全的类具有如下特征:

  • 该类的对象可以被多个线程安全的访问;

  • 每个线程调用该对象的任意方法之后都将得到正确结果;

  • 每个线程调用该对象的任意方法之后,该对象依然保持合理状态;

提示:可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全锁带来的负面影响,程序可以采用如下策略:

  • 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步。例如上面的FKAccount类中的accountNo属性就无须同步,所以程序只对draw方法进行同步控制;

  • 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本——线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用哪个线程安全版本;

3、释放对同步监视器的锁定

任何线程在进入同步代码块之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显示释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定:

  • 当前线程的同步代码块执行结束,当前线程即释放同步监视器;

  • 当线程在同步代码块中遇到goto,return终止该代码块、该方法的继续执行时,当前线程将会释放同步监视器;

  • 当线程在同步代码块中出现了错误,导致该代码块异常结束时,将会释放同步监视器。

4、同步锁(NSLock)

具体看demo
提示:使用NSLock与使用同步方式有点相似,只是使用NSLock时显式使用NSLock对象作为同步锁,二使用同步代码块时系统显式使用某个对象多为同步监视器,同样都符合“加锁 → 修改 → 释放锁”的操作模式,而且使用NSLock对象时每个NSLock对象对应一个FKAccount对象,一样可以保证对同一个FKAccoubt对象,同一时刻只能有一个线程能进入临界区。

5、使用NSCondition控制线程通信

当线程在系统内运行时,线程的调用具有一定的透明性,程序通常无法准确控制线程的轮换执行,但我们可以通过一些机制来保证线程协调运行,也就是处理线程之间的通信:

Foundation提供了一个NSCondition类来处理线程通信,NSCondition实现了NSLocking协议,因此也可以调用lock、unlock来实现线程同步。除此之外,NSCondition可以让那些已经锁定NSCondition对象却无法继续执行的线程释放NSCondition对象,NSCondition对象也可以唤醒其他等待状态的线程:

NSCondition类提供了如下3个方法:

- (void)wait;//该方法导致当前线程一直等待,知道其他线程调用该NSCondition的signal方法或broadcast方法来唤醒该线程。wait方法有一个变体: - (BOOL)waitUntilDate:(NSDate *)limit;用于控制等待到指定时间点,如果到了该时间点,该线程将会被自动唤醒;

- (void)signal;//唤醒在此NSCondition对象上等待的单个线程。如果所有线程都在该NSCondition对象上等待,则会选择唤醒其中一个线程。选择时任意性的。只有当前线程放弃对该NSCondition对象的锁定后(使用wait),才可以执行被唤醒的线程。

- (void)broadcast;//唤醒在此NSCondition对象上等待的所有线程。只有当前线程放弃对该NSCondition对象的锁定后,才可以执行被唤醒的线程。

实例:生产者——消费者

本程序中FKAccount使用NSCondition对象来控制同步,并使用NSCondition对象来控制线程的通信。程序通过一个旗标来标识账户中是否已有存款,当旗标为“NO”时,标明账户中没有存款,存款者线程可以向下执行,当存款者把钱存入账号中,将旗帜设为“YES”,并调用signal或broadcast方法来唤醒其他线程;当存款者线程进入线程体后,如果旗标为“YES”,就调用wait方法让该线程等待。

当旗标为“YES”时,标明账户中已经存入了钱,则取钱者线程可以向下执行,当取钱者从账户中取出后,将旗帜设为“NO”,并调用signal活broadcast方法来唤醒其他线程;当取钱者线程进入线程体后,如果旗标为“NO”,就调用wait方法让该线程等待。

具体看demo

四、使用GCD实现多线程

为了简化多线程应用的开发,iOS提供了GCD来实现多线程。GCD的两个核心概念如下:

  • 队列:队列负责管理开发者提交的任务,GCD队列始终以FIFO(先进先出)的方式来处理任务——但由于任务的执行时间并不相同,因此先处理的任务并不一定先结束。队列既可是串行队列,也可是并发队列,串行队列每次只处理一个任务,必须前一个任务执行完成后,才能执行下一个任务:并发队列则可同时处理多个任务,因此将会有多个任务并发执行。队列底层会维护一个线程池来处理用户提交的任务,线程池的作用就是执行队列管理的任务。串行队列底层的线程池只要维护一个线程即可,并发队列的底层则需要维护多个线程。

  • 任务:任务就是用户提交给队列的工作单元,这些任务将会提交给队列底层维护的线程池执行,因此这些任务会以多线程的方式执行。

对于打算使用GCD实现多线程的开发者来说,使用GCD只要遵守两个步骤即可:

  • ①、创建队列;
  • ②、将任务提交给队列。

1、创建队列

GCD的队列可分为两种:

  • 串行队列:串行对垒底层的线程池只要一个线程,因此只提供一个线程用来执行任务,所以后一个任务必须等到前一个任务执行结束才能开始执行。

  • 并发队列:线程池提供多个线程来执行任务,所以可以按FIFO的顺序并发启动、执行多个并发任务。

下面的函数用于创建或访问队列:

dispatch_queue_t dispatch_get_current_queue(void);//获得当前执行代码所在的队列;

dispatch_queue_t dispatch_get_global_queue(long identifier, unsigned long flags);//根据指定优先级、额外的旗标来获取系统的全局并发队列。第一个参数可接受DISPATCH_QUEUE_PRIORITY_HIGH(2)、 DISPATCH_QUEUE_PRIORITY_DEFAULT(0)、 DISPATCH_QUEUE_PRIORITY_LOW (-2)和 DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN这几个优先级。目前第二个参数(额外的旗标参数)暂未使用,只是为将来做准备的,一般传入0即可。

dispatch_queue_t dispatch_get_main_queue(void);//获取应用主线程所关联的串行队列;

dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);//根据指定字符串标签创建队列。第二个参数可控制创建串行队列还是并发队列,如果将第二个参数设为“ DISPATCH_QUEUE_SERIAL”,则代表创建串行队列;如果将第二个参数设为“ DISPATCH_QUEUE_CONCURRENT”,则代表创建并发队列。在迷宫有启用ARC机制的情况下,通过这种方式创建的队列需要调用 dispatch_release()函数释放引用计数。

const char * dispatch_queue_get_label(dispatch_queue_t queue);//获取指定队列的字符串标签。函数中涉及一个dispatch_queue_t,这种类型就代表一个队列。

程序可以创建如下几种队列:

  • (1)、获取系统默认的全局并发队列:
    获取系统默认的全局并发队列可通过如下代码完成:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  • (2)、获取系统主线程关联的串行队列:
    获取系统主线程关联的串行队列可通过如下代码完成:
dispatch_queue_t mainQueue = dispatch_get_main_queue();

如果将任务提交给主线程关联的串行队列,那么就相当于直接在程序主线程中去执行任务;

  • (3)、创建串行队列:
    创建串行队列可通过如下代码完成:
dispatch_queue_t serialQueue = dispatch_queue_create("fkjaca.queue", DISPATCH_QUEUE_SERIAL);

如果将多个任务提交给串行队列,多个任务只能按顺序执行,必须等前一个任务完成后,才能开始执行后一个任务。

  • (4)、创建并发队列:
    创建并发队列可通过如下代码完成:
dispatch_queue_t concurrentQueue = dispatch_queue_create("fkjaca.queue", DISPATCH_QUEUE_CONCURRENT);

如果将多个任务提交给并发队列,并发队列可以按FIFO的顺序启动多个并发执行的任务,由于任务的耗时长短并不相同,因此后提交的任务完全可能先完成,得到队列之后,接下来就可以将任务提交给队列,并由队列底层管理的线程池来执行这些任务。

2、异步提交任务

iOS提供了如下函数来向队列提交任务。下面这些函数很多都有两个版本:一个接收代码块作为参数的版本,一个接收函数作为参数的版本——其中接收函数作为参数的函数名最后多了_f后缀,而且会多一个参数,用于向函数传入应用程序定义的上下文。

void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);//将代码块以异步方式提交给指定队列,该队列底层的线程池将负责执行该代码块;

void dispatch_async_f(dispatch_queue_t queue, void *context, dispatch_function_t work);//将函数以异步方式提交给指定队列,该队列底层的线程池将负责执行该函数;

void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);//将代码块以同步的方式提交给指定队列,该队列底层的线程池将负责执行该代码块;

void dispatch_sync_f(dispatch_queue_t queue, void *context, dispatch_function_t work);//将函数以同步的方式提交给指定队列,该队列底层的线程池将负责执行该代码块;

void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);//将代码块以异步方式提交给指定队列,该队列底层的线程池将负责在when指定的时间执行该代码块;

void dispatch_after_f(dispatch_time_t when, dispatch_queue_t queue, void *context, dispatch_function_t work);//将函数以异步方式提交给指定队列,该队列底层的线程池将负责在when指定的时间执行该代码块;

void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));//将代码块以异步方式提交给指定队列,该队列底层的线程将会多次重复执行该代码块;

void dispatch_apply_f(size_t iterations, dispatch_queue_t queue, void *context, void (*work)(void *, size_t));//将函数以异步方式提交给指定队列,该队列底层的线程将会多次重复执行该代码块;

void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);//将代码块提交给指定队列,该队列底层的线程池控制在应用的某个生命周期内仅执行该函数一次。其中predicate参数是一个指向dispatch_once_t(本质就是long型整数)变量的指针,该变量用于判断该代码块是否已经执行过。
- (IBAction)serial:(id)sender {
//依次将2个代码块提交给串行队列
//必须等到第一个代码块完成后,才能执行第2个代码块
dispatch_async(serialQueue, ^{
for (int i = 0; i < 100; i ++) {
NSLog(@"%s===%@===%d",dispatch_queue_get_label(serialQueue),[NSThread currentThread],i);
}
});
dispatch_async(serialQueue, ^{
for (int i = 0; i < 100; i ++) {
NSLog(@"%s---%@---%d",dispatch_queue_get_label(serialQueue),[NSThread currentThread],i);
}
});
}
- (IBAction)concurrent:(id)sender{
//依次将2个代码块提交给并发队列
//两个代码块可以并发执行
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 100; i ++) {
NSLog(@"%s===%@===%d",dispatch_queue_get_label(concurrentQueue),[NSThread currentThread],i);
}
});
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 100; i ++) {
NSLog(@"%s---%@---%d",dispatch_queue_get_label(concurrentQueue),[NSThread currentThread],i);
}
});
}

实例:使用GCD下载图片

- (IBAction)downImage:(id)sender {
//将代码块提交给系统的全局并发队列
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSString* url = @"http://www.crazyit.org/logo.jpg";
//从网络获取数据
NSData* data = [[NSData alloc]initWithContentsOfURL:[NSURL URLWithString:url]];
//将网络数据初始化为UIImage对象
UIImage* image = [[UIImage alloc]initWithData:data];
if (image != nil) {
//将代码块提交给主线程关联的队列,该代码块将会由主线程完成
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
}else{
NSLog(@"---下载图片出现错误");
}
});
}

3、同步提交任务

dispatch_sync()函数则会以同步的方式提交代码块,该函数必须等到代码块执行结束才会返回。如果程序使用该函数先后提交了两个代码块(即使提交给并发队列),也必须等第一个任务完成后才会开始执行第二个任务。

例如:

- (IBAction)clicked:(id)sender {
//以同步的方式先后提交2个代码块
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0), ^{
for (int i = 0; i < 100; i ++) {
NSLog(@"%@======%d",[NSThread currentThread],i);
[NSThread sleepForTimeInterval:0.1];
}
});
//必须等第一次提交的代码快执行完成后,dispatch_sync()函数才会返回
//程序才会执行这里,才能提交第二个代码块
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i = 0 ; i < 100; i ++) {
NSLog(@"%@------%d",[NSThread currentThread],i);
[NSThread sleepForTimeInterval:0.1];
}
});
}

4、多次执行任务

dispatch_apply()函数将控制提交的代码重复执行多次,如果该代码块被提交给并发队列,系统可以使用多个线程并发执行同一个代码块;

例如:

- (IBAction)clicked:(id)sender {
dispatch_apply(5, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t time) {
NSLog(@"===执行【%lu】次===%@",time,[NSThread currentThread]);
});
}

5、只执行一次的任务

dispatch_once()函数将控制提交的代码块在整个应用的生命周期内最多执行一次——只有第一次提交该代码块时,该代码块才会获得执行的机会,而且dispatch_once()函数无须传入队列,这意味着系统将直接用主线程执行该函数提交的代码块。

dispatch_once()函数执行时需要传入一个dispatch_once_t类型(本质就是long型整数)的指针(即predicate参数),该指针变量用于判断该代码块是否已经执行过。

例如:

- (IBAction)clicked:(id)sender {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"==执行代码块==");
//线程暂停3秒
[NSThread sleepForTimeInterval:3.0];
});
}

五、后台运行

本书上册已经介绍过iOS应用的生命周期行为,当应用程序进入后台时,系统会自动回调应用程序委托的applicationDidEnterBackground:方法。应用可以在该方法中完成转入后台前做的准备工作,所有的应用需要做以下事情:

  • 释放所有可以释放的内存;

  • 保存用户数据或状态信息。所有没写入磁盘的文件或信息,在进入后台之前,都应该写入磁盘,因为程序可能在后台被杀死。

1、进入后台时释放内存

当程序进入后台之后,为了却确保获得最佳的用户体验,建议释放那些占用内存较大且可以重新获取的资源——这是因为当应用处于后台时,iOS系统会优先终止那些占用内存大的应用。如果应用尽可能释放其所占用的内润,那么应用就可以在后台存活更久。从这个角度看,可以得到一个结论:应用暂停时所占用的内存越少,iOS彻底终止该应用的风险就越低。

如果应用没有启用ARC机制,程序需要在应用进入后台时,将那些需要释放的资源的引用计数器变为0,从而让系统回收这些资源。当应用转入前台时,系统需要重新恢复这些资源。

如果应用启用了ARC机制,程序只要在应用进入后台时,将应用那些需要释放资源的变量赋为nil即可。当应用转入前台时,系统需要重新恢复这些资源。

实例:疯狂飞机大战

具体看demo

2、进入后台时保存状态

当应用进入后台时,如果程序有一些状态数据(比如飞机大战的积分)没有保存,而iOS系统可能在内存紧张的时终止该应用,那么就可能导致该应用丢失这些状态数据。可以使用NSUserDegaults保存数据。

3、请求更多的后台时间

当应用转入后台时,不要在主线程中执行超过5秒的任务,如果应用进入后台花费了太多的时间(即:applicationDidEnterBackground:方法的执行体花费太多时间),应用可能从内存中被删除。

假如应用程序正在执行文件下载或文件传输等,当应用进入后台时,如果该任务还没有执行完成,应用转入后台后该任务就会被暂停。千万不要强制在applicationDidEnterBackground:方法中直接完成该任务——因为这会导致应用进入后台花费太多时间,iOS系统可能直接从内存中删除该应用。正确的做法是:以applicationDidEnterBackground:方法为平台,告诉系统进入后台还有更所的任务需要完成,从而向系统申请更多的后台时间。在这种方式下,当我们的应用处于后台时,即使用户正在使用其他应用,只要系统还有足够的内存,我们的应用就可以保存在内存中,iOS系统会保留应用运行一段时间。

为了请求更多的后台时间,请按如下步骤执行:

  • ①、调用UIApplication对象的beginBackgroundTaskWithExpirationHandler:方法请求获取更多的后台执行时间,该方法默认请求额外获得10分钟后台时间。该方法需要传入一个代码块作为参数,如果请求获取后台执行时间失败,将会执行代码块。该方法将返回一个UIBackgroundTaskIdentifier类型的变量,该变量可作为后台任务的标识符;
  • ②、调用dispatch_async()方法将制定代码块提交给后台执行;
  • ③、后台任务执行完成后,调用UIAlication对象的endBackgroundTask:方法结束后台任务。

例如:

- (void) enterBack:(NSNotification*)notification{
UIApplication* app = [UIApplication sharedApplication];
//定义一个UIBackgroundTaskIdentifier类型(本身就是NSUInteger)的变量。
//该变量将作为后台任务的标示符
__block UIBackgroundTaskIdentifier backTaskId;
backTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
NSLog(@"===在额外申请的10分钟内依然没有完成任务===");
//结束后台任务
[app endBackgroundTask:backTaskId];
}];
if (backTaskId == UIBackgroundTaskInvalid) {
NSLog(@"===iOS版本不支持后台运行,后台任务启动失败===");
return;
}
//将代码块以异步方式提交给系统的全局并发队列
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"===额外申请的后台任务时间为:%f===",app.backgroundTimeRemaining);
//其他内存清理的代码也可在此处完成
for (int i = 0; i < 2; i ++) {
NSLog(@"下载任务完成");
//暂停10秒模拟正在执行后台下载
[NSThread sleepForTimeInterval:10];
}
NSLog(@"===剩余的后台任务时间为:%f===",app.backgroundTimeRemaining);
//结束后台任务
[app endBackgroundTask:backTaskId];
});
}

六、使用NSOperation与NSOperationQueue实现多线程

除了使用GCD实现多线程之外,NSOperation与NSOperationQueue也是一种非常简单的多线程实现方式。NSOperation与NSOperationQueue的基本理论如下:

  • (1)、NSOperationQueue:代表一个FIFO的队列,它负责管理系统提交的多个NSOperation,NSOperationQueue底层维护一个线程池,会按顺序启动线程来执行提交给该队列的NSOperation任务。
  • (2)、NSOperation:代表一个多线程任务。NSOperation还有NSInvocationOperation、NSBlockOperation两个子类。NSOperation有两种使用方式:①、开发者实现NSOperation的子类;②、开发者直接使用NSInvocationOperation或NSBlockOperation子类;

使用NSOperation、NSOperationQueue开发多线程非常简单,只要如下两步:

  • ①、创建NSOperationQueue队列,并为该队列设置相关属性;
  • ②、创建NSOperation子类的对象,并将该对象交给NSOperationQueue队列,该队列将会按顺序依次启动每个NSOperation。

NSOperationQueue负责管理,执行所有的NSOperation,它底层维护着一个线程池,开发者提交的NSOperation正是由该线程池中的线程负责执行的。NSOperationQueue提供了如下常用方法:

  • (1)、+ (NSOperationQueue *)currentQueue NS_AVAILABLE(10_6, 4_0);类方法,该方法返回执行当前NSOperation的NSOperationQueue队列;
  • (2)、+ (NSOperationQueue *)mainQueue NS_AVAILABLE(10_6, 4_0);类方法,该方法返回系统主线程的NSOperationQueue队列;
  • (3)、- (void)addOperation:(NSOperation *)op;将operation添加到NSOperationQueue队列中;
  • (4)、- (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait NS_AVAILABLE(10_6, 4_0);将NSArray中包含的所有NSOperation添加到NSOperationQueue。如果第二个参数指定为YES,将会阻塞当前线程,知道提交所有NSOperation执行完成;如果第二个参数指定为NO,该方法会立即返回,NSArray包含的NSOperations将以异步方式执行,不会阻塞当前线程;
  • (5)、@property (readonly, copy) NSArray *operations;只读属性,返回该NSOperationQueue管理的所有NSOperation;
  • (6)、@property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);只读属性,返回该NSOperationQueue管理的NSOperation的数量;
  • (7)、- (void)cancelAllOperations;取消NSOperationQueue队列中所有正在排队和执行的NSOperation;
  • (8)、- (void)waitUntilAllOperationsAreFinished;阻塞当前线程,这道该NSOperationQueue中所有排队和执行的NSOperation执行完成才解除阻塞;
  • (9)、@property NSInteger maxConcurrentOperationCount;设置或返回该NSOperationQueue队列最大支持的并发NSOperation数量——其实就是设置或返回该NSOperationQueue最大支持多少个并线程;
  • (10)、@property (getter=isSuspended) BOOL suspended;返回或设置NSOperationQueue是否暂停调度正在排队的NSOperation;

1、使用NSInvocationOperation和NSBlockOperation

用NSInvocationOperation和NSBlockOperation都是NSOperation的子类,都可在程序中直接使用,用于封装需要异步执行的任务。

NSInvocationOperation和NSBlockOperation在用法上非常相似,区别是NSInvocationOperation用于将特定对象的特定方法封装成NSOperation,而NSBlockOperation则用于将代码块封装成NSOperation。

例如:使用NSInvocationOperation

- (IBAction)clicked:(id)sender {
NSString* url = @"http://www.crazyit.org/logo.jpg";
//self的downloadImageFormURL:方法作为执行体,创建NSOperation
NSInvocationOperation* operation = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(downloadImageFromURL:) object:url];
//将NSOperation添加给NSOperationQueue
[queue addOperation:operation];
}

实例:使用NSBlockOperation下载图片

该实例的功能和前面介绍的使用NSThread下载图片的功能基本相似,只是该程序将会使用NSBlockOperation、NSOperationQueue来启动多线程执行下载。

- (IBAction)clicked:(id)sender {
NSString* url = @"http://www.crazyit.org/logo.jpg";
//以传入的代码块作为执行体,创建NSOperation
NSBlockOperation* operation = [NSBlockOperation blockOperationWithBlock:^{
//从网络中获取数据
NSData* data = [[NSData alloc]initWithContentsOfURL:[NSURL URLWithString:url]];
//将网络数据初始化为UIImage对象
UIImage* image = [[UIImage alloc]initWithData:data];
if (image != nil) {
//在主线程中执行updateUI:方法
[self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES];
}else{
NSLog(@"--下载图片出现错误--");
}
}];
//将NSOperation添加到NSOperationQueue
[queue addOperation:operation];
}

2、定义NSOperation子类

NSOperation一般不会直接拿来使用,而是选择创建它的子类,创建NSOperation的子类需要重写一个方法:-(void)main,该方法的方法体将作为NSOperationQueue完成的任务。

#import "NSDownImageOperation.h"
@implementation NSDownImageOperation
- (id)initWithURL:(NSURL *)url imageView:(UIImageView *)iv{
self = [super init];
if (self) {
_imageView = iv;
_url = url;
}
return self;
}
//重写main方法,该方法将作为线程执行体
- (void)main{
//从网络获取数据
NSData* data = [[NSData alloc]initWithContentsOfURL:self.url];
//将网络数据初始化为UIImage对象
UIImage* image = [[UIImage alloc]initWithData:data];
if (image != nil) {
//在主线程中执行updateUI:方法
[self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES];
}else{
NSLog(@"---下载图片出现错误---");
}
}
-(void)updateUI:(UIImage*) image{
self.imageView.image = image;
}
@end

控制器实现部分:

- (IBAction)clicked:(id)sender {
//定义下载的图片的URL
NSURL* url = [NSURL URLWithString:@"http://www.crazyit.org/logo.jpg"];
//创建NSDownImageOperation对象
NSDownImageOperation* operation = [[NSDownImageOperation alloc]initWithURL:url imageView:self.iv];
// 将NSOperation的子类的实例提交给NSOperationQueue
[queue addOperation:operation];
}