概述
优点:
- 把程序中耗时的任务放到后台去处理,如图片、视频的下载等;
- 充分发挥多核处理器的优势,并发执行让系统运行的更快、更流畅、用户体验更佳。
不足:
- 大量的线程操作会降低代码的可读性;
- 大量的线程需要更多的内存空间;
- 当多个线程对同一资源出现争夺的时候会出现线程安全问题。
目前实现多线程的技术有四种:pthread、NSThread、GCD 和 NSOperation。
-
pthread:是一套基于 C 语言的通用多线程 API,线程的生命周期需要我们手动管理,使用难度较大,所以我们几乎不会使用。 -
NSThread:是一套面向对象的 API,线程的生命周期需要我们手动管理,简单易用,可直接操作线程对象。 -
GCD:是一套底层基于C语言的多线程 API,自动管理生命周期,充分利用了多核处理器的优点。 -
NSOperation:是一套底层基于GCD面向对象的多线程 API,自动管理生命周期,并且比GCD多了一些更简单实用的功能。
在这里我们暂且不讨论 pthread 的使用,主要看看后面三个方案都是怎么应用的。
NSThread
一个 NSThread 对象就代表一个线程,NSThread 有多种创建线程的方式:
1. 先创建再启动
// 创建
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"Axe"];
// 启动
[thread start];
2. 直接创建并启动线程
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"Axe"];
3. 创建并启动
[self performSelectorInBackground:@selector(run:) withObject:@"Axe"];
[NSThread sleepForTimeInterval:2.0];
从三种创建线程的方法可以看到:方法 2和 3 都可以创建一个线程并且自启动,而方法一可以很方便的拿到线程对象。
4.应用示例
以下载一张图片为例:
NSURL *url = [NSURL URLWithString:@"http://f.hiphotos.baidu.com/image/pic/item/203fb80e7bec54e753da379aba389b504fc26a7b.jpg"];
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(downloadImageWithURL:) object:url];
[thread start];
- (void)downloadImageWithURL:(NSURL *)url {
NSError *error;
NSData *data = [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:&error];
if (error) {
NSLog(@"error = %@",error);
} else {
UIImage *image = [UIImage imageWithData:data];
[self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
}
}
GCD
GCD 全称是 Grand Central Dispatch,可译为“中枢调度器”。GCD 是 Apple 公司为多核的并行运算提出的解决方案,它会自动利用更多的 CPU 内核来开启线程执行任务。
在了解 GCD 之前先明白同步/异步、并行/串行这几个名词的概念。
- 同步线程:在当前线程可立即执行任务,不具备开启线程的能力,会阻塞当前的线程,必须等待同步线程中的任务执行完并返回后,才会执行下一个任务。
- 异步线程:在当前线程结束后执行任务,具备开启新的线程的能力。
- 并行队列:允许多个任务同时执行。
- 串行队列:只有等上一个任务执行完毕后,下一个任务才会被执行。
创建串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t mainQueue = dispatch_get_main_queue();
创建并行队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
创建同步+串行队列
// 在当前线程,立即执行任务
dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
for (int i = 0; i < 10; i++) {
NSLog(@"%@",[NSThread currentThread]);
}
});
创建同步+并行队列
// 在当前线程,立即执行任务
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(concurrentQueue, ^{
for (int i = 0; i < 10; i++) {
NSLog(@"%d --- %@",i, [NSThread currentThread]);
}
});
创建异步+串行队列
// 另开启线程,多个任务顺序执行
dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
dispatch_async(serialQueue, ^{
for (int i = 0; i < 10; i++) {
NSLog(@"%d --- %@",i, [NSThread currentThread]);
}
});
dispatch_async(serialQueue, ^{
for (int i = 0; i < 10; i++) {
NSLog(@"%d --- %@",i, [NSThread currentThread]);
}
});
});
创建异步+并行队列
// 另开启线程,多个任务一起执行,不分先后
dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
dispatch_async(serialQueue, ^{
for (int i = 0; i < 10; i++) {
NSLog(@"%d --- %@",i, [NSThread currentThread]);
}
});
dispatch_async(serialQueue, ^{
for (int i = 0; i < 10; i++) {
NSLog(@"%d --- %@",i, [NSThread currentThread]);
}
});
});
应用示例
还是以下载一张图片为例
NSURL *url = [NSURL URLWithString:@"http://f.hiphotos.baidu.com/image/pic/item/203fb80e7bec54e753da379aba389b504fc26a7b.jpg"];
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.download.image", DISPATCH_QUEUE_CONCURRENT);
__weak typeof(self) weakSelf = self;
dispatch_async(concurrentQueue, ^{
NSError *error;
NSData *data = [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:&error];
if (error) {
NSLog(@"error = %@",error);
} else {
UIImage *image = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.imageView.image = image;
});
}
});
GCD其他函数的应用
dispatch_barrier 栅栏
功能描述:先执行栅栏前面的队列,再执行栅栏中的队列,等待栅栏中的队列执行完毕后,才会执行栅栏后面的队列。
注意事项:栅栏在并行队列中使用才有它的意义,强行在无序执行的队列中创造出顺序执行的队列任务,当不能使全局的并行队列,一般会自己创建我们的并行队列
代码示例:
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 10; i++) {
NSLog(@"1-->%d --- %@",i, [NSThread currentThread]);
}
});
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 10; i++) {
NSLog(@"2-->%d --- %@",i, [NSThread currentThread]);
}
});
dispatch_barrier_async(concurrentQueue, ^{
for (int i = 0; i < 10; i++) {
NSLog(@"3-->%d --- %@",i, [NSThread currentThread]);
}
});
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 10; i++) {
NSLog(@"4-->%d --- %@",i, [NSThread currentThread]);
}
});
通过 log 输出可以看到,1 和 2 队列的任务会先无序执行,在其两个队列中的任务执行完毕后,才会执行栅栏队列中的任务,最后才执行队列 4。
dispatch_after 延迟
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
dispatch_once 执行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 仅仅执行一次
});
应用:常用用于单例中。
dispatch_apply 快速迭代
NSArray *array = @[@"a",@"b",@"c",@"d",@"e"];
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(array.count, globalQueue, ^(size_t idx) {
NSLog(@"array[%zu] = %@",idx, array[idx]);
});
dispatch_group 队列组
功能描述:将多个队列添加到一个队列组中,当队列组中的任务都执行完毕后,会通知我们执行结果。
代码示例:
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, globalQueue, ^{
NSLog(@"任务1");
});
dispatch_group_async(group, globalQueue, ^{
NSLog(@"任务3");
});
dispatch_group_async(group, globalQueue, ^{
NSLog(@"任务2");
});
dispatch_group_notify(group, globalQueue, ^{
NSLog(@"所有任务都已完成");
});
dispatch_group_async(group, globalQueue, ^{
NSLog(@"任务4");
});
GCD定时器
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, globalQueue);
// 3s 后定时器启动
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC));
// 每1秒执行一次回调
dispatch_source_set_timer(timer, start, 2.0 * NSEC_PER_SEC, 0);
// 计时
__block int count = 0;
dispatch_source_set_event_handler(timer, ^{
NSLog(@"%d", count);
if (count > 10) {
dispatch_cancel(timer);
}
count++;
});
dispatch_resume(timer);
NSOperation
NSOperation 是一个抽象类,并不具备封装操作的能力,我们必须使用它的子类。为此系统也提供了NSInvocationOperation 和 NSBlockOperation 两个子类供我们使用。当然我们也可以继承 NSOperation,创建我们的子类,实现内部相应的方法。
NSInvocationOperation
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImageWithURL:) object:url];
[invocationOperation start];
默认情况下,调用 start 方法后,并不会开启一个新的线程去执行 selector,而是在当前线程同步的执行操作,只有将 NSOperation 添加到 NSOperationQueue 中,才会执行异步操作。
NSBlockOperation
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
// 在主线程中执行
NSLog(@"操作1 --- %@", [NSThread currentThread]);
}];
[blockOperation addExecutionBlock:^{
// 在子线程中执行
NSLog(@"操作2 --- %@", [NSThread currentThread]);
}];
// 添加额外的任务数大于1才会异步执行
[blockOperation addExecutionBlock:^{
// 在子线程中执行
NSLog(@"操作3 --- %@", [NSThread currentThread]);
}];
[blockOperation addExecutionBlock:^{
// 在子线程中执行
NSLog(@"操作4 --- %@", [NSThread currentThread]);
}];
[blockOperation start];
Operation 的其它用法
执行操作
执行一个 Operation 有两种方式,一种是手动调用 start,这种方法调用会在当前线程进行同步执行,因此在主线程里面一定要小心调用,不然就会堵塞主线程。另一种是自动执行,只要将 operation 添加到 operationQueue 中就会尽快执行操作。
取消操作
NSOperation 开始执行操作后,会默认一直到操作完成,当然中途我们也可以调用 cancel 方法取消操作。
在调用 cancel 方法的时候,只是将 cancelled 设置为了 YES,因此在每个操作开始前,或者在每个有意义的实际操作完成后,都要先检测 isCancelled 是否被设置成了 YES, 如果已经取消了,那么后面的操作就不用在执行了。
操作完成后的操作
如果想在 NSOperation 执行完操作后做一些事情,可以调用 completionBlock 设置,在操作完成后就会回调block里面的内容。
自定义 Operation
如果系统提供的 NSInvocationOperation 和 NSBlockOperation 两个子类都不能满足自己的需求时,我们通过继承自定义子类,并添加需要执行的操作。
这里我们仍然以下载图片为例:
//
// MyOperation.h
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
typedef void(^MyDownloadFinishedBlock)(UIImage *image);
@interface MyOperation : NSOperation
@property (copy, nonatomic, readonly) NSString *imageURL;
- (instancetype)initWithURLString:(NSString *)urlString downloadFinishedBlock:(MyDownloadFinishedBlock)downloadFinishedBlock;
@end
//
// MyOperation.m
//
#import "MyOperation.h"
@interface MyOperation ()
@property (copy, nonatomic) MyDownloadFinishedBlock downloadFinishedBlock;
@end
@implementation MyOperation
- (instancetype)initWithURLString:(NSString *)urlString downloadFinishedBlock:(MyDownloadFinishedBlock)downloadFinishedBlock {
self = [super init];
if (self) {
_imageURL = urlString;
_downloadFinishedBlock = downloadFinishedBlock;
}
return self;
}
- (void)main {
// 如果时在异步线程中执行操作,即 main 方法在异步线程中调用,那么将无法访问主线程的自动释放池,因此创建一个属于当前线程的自动释放池
@autoreleasepool {
// 在 main 方法中定期的调用 isCancelled 方法检测操作是否已经被取消
// 在执行任何实际的工作之前,也就是 main 方法的开头,就要检测操作是否已经被取消
// 在执行一段耗时的操作后也需要检测操作是否已经被取消
if (self.isCancelled) {
return;
}
NSURL *url = [NSURL URLWithString:_imageURL];
// 此处就是通过网络获取图片 data,是比较耗时的操作,所以在后面就需要检测操作是否已经被取消
NSData *imageData = [NSData dataWithContentsOfURL:url];
if (self.isCancelled) {
url = nil;
_imageURL = nil;
return;
}
UIImage *image = [UIImage imageWithData:imageData];
if (self.isCancelled) {
image = nil;
return;
}
if (_downloadFinishedBlock) {
_downloadFinishedBlock(image);
}
}
}
@end
// 使用自定义的 MyOperation 下载一张图片
MyOperation *operation = [[MyOperation alloc] initWithURLString:@"http://f.hiphotos.baidu.com/image/pic/item/203fb80e7bec54e753da379aba389b504fc26a7b.jpg" downloadFinishedBlock:^(UIImage *image) {
NSLog(@"%@",[NSThread currentThread]);
[_imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
}];
[operation start];
说明:如果你要执行一个同步操作,那只需要重写 main 方法,在里面添加必要的操作。如果要执行一个异步的操作,那就需要重写 start 方法,因为在你把操作添加进去后系统会自动调用 start 方法,这时将不在调用mian里面的操作。
NSOperationQueue
一个 NSOperation 对象可以通过 start 方法来执行任务,默认是同步执行的。也可以将其添加到NSOperationQueue 操作队列中去执行,它是异步执行的。
添加 NSOperation 到 NSOperationQueue 中
不管通过哪种方式添加,只要将 operation 添加到 operationQueue 中,通常短时间内就会得到执行(异步)。但是如果存在依赖,或者整个 operationQueue 被暂停等原因,也可能需要等待。
添加一个operation
[operationQueue addOperation:invocationOperation];
添加一组operation
[operationQueue addOperations:@[invocationOperation, blockOperation] waitUntilFinished:YES];
添加一个block形式的operation
[operationQueue addOperationWithBlock:^{
NSLog(@"任务");
}];
添加依赖
概述
所谓依赖就是说,当某个 operation 对象需要依赖于其它 operation 对象才能完成时,就可通过 addDependency 方法添加一个或者多个依赖对象,只有所有依赖的对象都已经完成操作后,当前的 operation 对象才开始执行操作,当然也可以通过 removeDependency 方法来移除这种依赖关系。
依赖方式
既可以在同一个 operationQueue 中不同 operation 对象添加依赖,也可以在不同的 operationQueue 之间不同operation 对象之间添加依赖,operation 对象会管理自己的依赖关系。
限制依赖
虽然可以在一个 queue 中添加依赖,也可以在不同的 queue 中添加依赖,但是要特别注意的是,不能添加环形依赖,即 a 依赖 b,b 也依赖 a。
应用示例:
这里以修改用户头像为例,模拟从用户上传头像到显示头像的过程。
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
NSBlockOperation *blockOperation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1.上传头像");
}];
NSBlockOperation *blockOperation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2.从服务器请求上传头像的url");
}];
NSBlockOperation *blockOperation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"3.通过url下载头像");
}];
[operationQueue addOperation:blockOperation1];
[operationQueue addOperation:blockOperation2];
[operationQueue addOperation:blockOperation3];
打印顺序如下:2 - 1 - 3 或者 3 - 2 -1
这样的顺序显然不符合正常的逻辑顺序,这时候,我们就可以通过添加依赖,达到我们说期望的顺序逻辑。
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
NSBlockOperation *blockOperation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1.上传头像");
}];
NSBlockOperation *blockOperation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2.从服务器请求上传头像的url");
}];
NSBlockOperation *blockOperation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"3.通过url下载头像");
}];
// 3 -> 2 2 -> 1 (3 - 2 - 1)
[blockOperation3 addDependency:blockOperation2];
[blockOperation2 addDependency:blockOperation1];
[operationQueue addOperation:blockOperation1];
[operationQueue addOperation:blockOperation2];
[operationQueue addOperation:blockOperation3];
打印顺序如下:1 - 2 - 3
这样就符合我们期望的逻辑,即用户先上传图片并完成后,再从服务器获取该用户头像的 url 地址,最后通过 url 地址下载相应的头像并显示。