OS X 和iOS 中的多线程技术(下)
上篇文章中介绍了 pthread 和 NSThread 两种多线程的方式,本文将继续介绍 GCD 和 NSOperation 这两种方式。。
1.GCD
1.1 什么是GCD
- GCD 全称 Grand Central Dispatch,可译为“牛逼的中枢调度器”
- GCD 基于纯 C 语言,内部封装了强大的函数库
1.2 使用 GCD 有什么优势
- GCD 是苹果公司为多核的并行运算提出的解决方案
- GCD 会自动利用更多的CPU内核 (如 二核 ,四核)
- GCD 会自动管理线程的生命周期(创建 、 调度 、 销毁线程)
- 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码
1.3 GCD 的使用
-
GCD 有两个核心的概念
- 任务 : 需要执行的操作
- 队列 : 用来存放任务
-
GCD 的使用步骤
- 制定任务
- 将任务放入到队列中,GCD会自动将队列中的任务取出,放到对应的线程中执行,队列中的任务取出遵循 FIFO原则。(FIFO:先进先出,队列原则)
-
GCD 中有两个用来执行任务的常用函数
-
同步方法执行任务
dispatch_sync(dispatch_queue_t _Nonnull queue, ^(void)block) queue : 队列 Block : 任务
-
异步方法执行任务
dispatch_async(dispatch_queue_t _Nonnull queue, ^(void)block)
-
-
同步和异步的区别
- 同步 : 只能在当前的线程中执行任务,不具备开启新线程的能力
- 异步 : 可以在新的线程中执行任务,具备开启新线程的能力
1.4 队列的类型
GCD 的队列可以分为 2 大类
-
并发队列 ( Concurrent Dispatch Queue )
- 可以让多任务并发执行,自动开启多个线程同时执行任务
- 并发功能只有在异步(dispatch_async)函数下才有效
-
串行队列 ( Serial Dispatch Queue )
- 让任务一个接一个地有序执行(一个任务执行完毕后才开始执行下一个)
注意:同步 、 异步、并发、串行的区分
-
同步
和异步
主要影响: 能不能开启新的线程- 同步 : 只是在当前线程中执行任务 ,不具备开启新线程的能力
- 异步 : 可以在新的线程中执行任务,具备开启新县城的能力
-
并发
和串行
主要影响: 任务的执行方式- 并发 : 多个任务并发执行
- 串行 : 多个任务一次顺序执行
1.5 GCD 的各种队列的组合
- 异步函数 + 并发队列:可以同时开启多条线程
// 1.获得全局的并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 2.将任务加入队列
dispatch_async(queue, ^{
for (NSInteger i = 0; i<10; i++) {
NSLog(@"1-----%@", [NSThread currentThread]);
}
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i<10; i++) {
NSLog(@"2-----%@", [NSThread currentThread]);
}
});
- 同步函数 + 并发队列:不会开启新的线程
// 1.获得全局的并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 2.将任务加入队列
dispatch_sync(queue, ^{
NSLog(@"1-----%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"2-----%@", [NSThread currentThread]);
});
- 异步函数 + 串行队列:会开启新的线程,但是任务是串行的,执行完一个任务,再执行下一个任务
// 1.创建串行队列
dispatch_queue_t queue = dispatch_queue_create("com.coder.queue", DISPATCH_QUEUE_SERIAL);
// dispatch_queue_t queue = dispatch_queue_create("com.coder.queue", NULL);
// 2.将任务加入队列
dispatch_async(queue, ^{
NSLog(@"1-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"2-----%@", [NSThread currentThread]);
});
- 异步函数 + 主队列:只在主线程中执行任务
// 1.创建串行队列
dispatch_queue_t queue = dispatch_queue_create("com.coder.queue", DISPATCH_QUEUE_SERIAL);
// 2.将任务加入队列
dispatch_sync(queue, ^{
NSLog(@"1-----%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"2-----%@", [NSThread currentThread]);
});
- 同步函数 + 主队列:
// 1.获得主队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 2.将任务加入队列
dispatch_sync(queue, ^{
NSLog(@"1-----%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"2-----%@", [NSThread currentThread]);
});
各种队列的执行效果 :
注意:
使用 sync 函数往当前串行队列中添加任务,会卡住当前的串行队列
1.6 GCD 个线程之间通信
通常开辟子线程是为了执行耗时操作。如下载图片的等,使用 GCD 进行线程间通信非常方便,示例代码如下:
// 子线程中下载网络图片 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 图片的网络路径
NSURL *url = [NSURL URLWithString:@"http://img.pconline.com.cn/images/photoblog/9/9/8/1/9981681/200910/11/1255259355826.jpg"];
// 加载图片
NSData *data = [NSData dataWithContentsOfURL:url];
// 生成图片
UIImage *image = [UIImage imageWithData:data];
// 回到主线程
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
});
1.7 GCD 其他常用函数
- 阻隔执行任务的函数
dispatch_barrier_sync(dispatch_queue_t _Nonnull queue, ^(void)block)
// 此函数起一个阻隔任务执行的作用, 它前面的任务执行完之后它才执行,等它执行完后面的任务才能执行
- 延迟执行
// GCD 延迟执行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"run-----");
});
// iOS 中其他方式的延迟执行还有
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
和定时器
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:NO];
---------------- run 方法 -----------------
- (void)run
{
NSLog(@"run-----");
}
- 一次性函数
一次性函数在整个程序运行中只会执行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"------run-----");
// 内部代码默认是线程安全的
});
- 快速迭代函数(遍历)
快速迭代行数,实际上在全局队列中遍历子线程执行任务,用于显著提高执行效率。
案例:【文件假拷贝】,【App Store 所有App同时更新】让每个任务都开子线程去并发执行会充分利用CPU,提高效率。
// 本示例代码是将 From 文件夹下的内容拷贝到 TO 文件夹下
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSString *from = @"/Users/xiaoyou/Desktop/From";
NSString *to = @"/Users/xiaoyou/Desktop/To";
NSFileManager *mgr = [NSFileManager defaultManager];
NSArray *subpaths = [mgr subpathsAtPath:from];
dispatch_apply(subpaths.count, queue, ^(size_t index) {
NSString *subpath = subpaths[index];
NSString *fromFullpath = [from stringByAppendingPathComponent:subpath];
NSString *toFullpath = [to stringByAppendingPathComponent:subpath];
// 剪切
[mgr moveItemAtPath:fromFullpath toPath:toFullpath error:nil];
NSLog(@"%@---%@", [NSThread currentThread], subpath);
});
- GCD 队列组
队列组中的任务执行完,组会受到一个通知,然后执行最终的操作
// 1. 创建全局队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 2. 创建一个队列组
dispatch_group_t group = dispatch_group_create();
// 任务 1.下载图片1
dispatch_group_async(group, queue, ^{
// 图片的网络路径
NSURL *url = [NSURL URLWithString:@"http://img.pconline.com.cn/images/photoblog/9/9/8/1/9981681/200910/11/1255259355826.jpg"];
// 加载图片
NSData *data = [NSData dataWithContentsOfURL:url];
// 生成图片
self.image1 = [UIImage imageWithData:data];
});
// 任务 2.下载图片2
dispatch_group_async(group, queue, ^{
// 图片的网络路径
NSURL *url = [NSURL URLWithString:@"http://pic38.nipic.com/20140228/5571398_215900721128_2.jpg"];
// 加载图片
NSData *data = [NSData dataWithContentsOfURL:url];
// 生成图片
self.image2 = [UIImage imageWithData:data];
});
// 任务 3.将图片1、图片2合成一张新的图片
dispatch_group_notify(group, queue, ^{
// 开启新的图形上下文
UIGraphicsBeginImageContext(CGSizeMake(100, 100));
// 绘制图片
[self.image1 drawInRect:CGRectMake(0, 0, 50, 100)];
[self.image2 drawInRect:CGRectMake(50, 0, 50, 100)];
// 取得上下文中的图片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// 结束上下文
UIGraphicsEndImageContext();
// 回到主线程显示图片
dispatch_async(dispatch_get_main_queue(), ^{
// 4.将新图片显示出来
self.imageView.image = image;
});
});
2. 使用 GCD 实现单例
2.1 单例模式
单例模式是开发过程中长期积累的一种编程习惯。
单例模式作用如下:
- 可以保证在程序运行过程中,一个类只有一个实例,而且该实例易于供外界访问
- 方便控制实例的个数,节约系统资源
单例模式使用场合:
- 在整个应用中,共享一份资源(该资源只需要创建初始化1次,如Application,NSUserDefault 等)
2.2 单例模式的实现(纯代码)
- 在 .m 中保留一个全局的 static 实例
static id _instance;
- 重写 allocWithZone: 方法,创建唯一实例
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
// 使用GCD一次性函数,保证线程安全
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [self allocWithZone:zone];
});
return _instance;
}
- 提供类方法,供外界使用
+ (instancetype)shareInstance{
// 使用GCD一次性函数,保证线程安全
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[self alloc] init];
});
return _instance;
}
- 实现 copyWithZone: 方法
+ (id)copyWithZone:(struct _NSZone *)zone
{
return _instance;
}
2.3 单例模式的实现(宏)
从上面的实现中可以看到,单例的实现方式是一样的,我们可以把它抽取成一个宏来实现,这样更加方便使用.
如下是单例的宏实现,只需在对应的单例类中添加两个对应的宏,就可轻松实现单例。
// .h文件
#define XMGSingletonH(name) + (instancetype)shared##name;
// .m文件
#define XMGSingletonM(name) \
static id _instance; \
\
+ (instancetype)allocWithZone:(struct _NSZone *)zone \
{ \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_instance = [super allocWithZone:zone]; \
}); \
return _instance; \
} \
\
+ (instancetype)shared##name \
{ \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_instance = [[self alloc] init]; \
}); \
return _instance; \
} \
\
- (id)copyWithZone:(NSZone *)zone \
{ \
return _instance; \
}
思考:为什么不使用继承?
继承:看似可行,实际会有问题,程序中的GCD一次性代码只会执行一次,当第一次有子类 A 调用之后,再有子类 B 调用返回的直接是第一次调用 A 的实例,无法返回正确类型 B 单例
也就是说如果有 static 这样的内部类对象不能用继承。
3. NSOperation
3.1 NSOperation 简介
NSOperation 是 OS X 和 iOS 开发中最后一种多线程实现方式,它是基于 GCD 的 OC 封装,使用更加面向对象。
- NSOperation 的作用
- 配合使用NSOperation 和 NSOperationQueue 实现多线程
- NSOperation 和 NSOperationQueue 实现多线程的具体步骤
- 先将需要执行的操作封装到一个 NSOperation 对象中
- 然后将 NSOperation 对象添加到 NSOperationQueue 中
- 系统会自动将 NSOperationQueue 中的 NSOperation 取出来,并将封装的操作放到一条新线程中执行
3.2 NSOperation 的子类
NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类
-
使用NSOperation子类的方式有3种
- NSInvocationOperation
- NSBlockOperation
- 自定义子类继承NSOperation,实现内部相应的方法
NSInvocationOperation
- 创建NSInvocationOperation对象
- (id)initWithTarget:(id)target selector:(SEL)sel object:(id)arg;
- 调用start方法开始执行操作
- (void)start;
一旦执行操作,就会调用target的sel方法
注意
- 默认情况下,调用了start方法后并不会开一条新线程去执行操作,而是在当前线程同步执行操作
- 只有将NSOperation放到一个NSOperationQueue中,才会异步执行操作
NSBlockOperation
- 创建NSBlockOperation对象
+ (id)blockOperationWithBlock:(void (^)(void))block;
- 通过addExecutionBlock:方法添加更多的操作
- (void)addExecutionBlock:(void (^)(void))block;
注意:
只要NSBlockOperation封装的操作数 > 1,就会异步执行操作
3.3 NSOperationQueue
-
NSOperationQueue的作用
- NSOperation可以调用start方法来执行任务,但默认是同步执行的
- 如果将NSOperation添加到NSOperationQueue(操作队列)中,系统会自动异步执行NSOperation中的操作
添加操作到NSOperationQueue中
- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;
3.4 最大并发数
-
什么是并发数?
- 同时执行的任务数
- 比如,同时开3个线程执行3个任务,并发数就是3
最大并发数的相关方法
- (NSInteger)maxConcurrentOperationCount;
- (void)setMaxConcurrentOperationCount:(NSInteger)cnt;
3.5 队列的取消、暂停、恢复
- 取消队列的所有操作
- (void)cancelAllOperations;
提示:也可以调用NSOperation的- (void)cancel方法取消单个操作
- 暂停和恢复队列
- (void)setSuspended:(BOOL)b; // YES代表暂停队列,NO代表恢复队列
- (BOOL)isSuspended;
3.6 操作依赖
- NSOperation之间可以设置依赖来保证执行顺序
- 比如一定要让操作A执行完后,才能执行操作B,可以这么写
[operationB addDependency:operationA]; // 操作B依赖于操作A
- 可以在不同queue的NSOperation之间创建依赖关系(如图)
注意:
不能相互依赖,比如A依赖B,B依赖A
3.7 操作的监听
可以监听一个操作的执行完毕
- (void (^)(void))completionBlock;
- (void)setCompletionBlock:(void (^)(void))block;
3.8 自定义NSOperation
自定义NSOperation的步骤很简单
- 重写
- (void)main
方法,在里面实现想执行的任务 - 重写
- (void)main
方法的注意点- 自己创建自动释放池(因为如果是异步操作,无法访问主线程的自动释放池)
- 经常通过
- (BOOL)isCancelled
方法检测操作是否被取消,对取消做出响应
苹果建议:应该对自定义的 Operation 中的执行完一个耗时操作,应该手动调用一下 isCancelled 方法查看是不是已经取消并做对应的操作
/**
* 需要执行的任务
*/
- (void)main
{
for (NSInteger i = 0; i<1000; i++) {
NSLog(@"download1 -%zd-- %@", i, [NSThread currentThread]);
}
if (self.isCancelled) return;
for (NSInteger i = 0; i<1000; i++) {
NSLog(@"download2 -%zd-- %@", i, [NSThread currentThread]);
}
if (self.isCancelled) return;
for (NSInteger i = 0; i<1000; i++) {
NSLog(@"download3 -%zd-- %@", i, [NSThread currentThread]);
}
if (self.isCancelled) return;
}
3.9 NSOperation 线程间通信
此处依旧以下载并合成一张图片为例,只需开启两个子线程分别下载image,第三个线程为合并操作, 然后添加线程依赖。并放到队列中
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
__block UIImage *image1 = nil;
// 下载图片1
NSBlockOperation *download1 = [NSBlockOperation blockOperationWithBlock:^{
// 图片的网络路径
NSURL *url = [NSURL URLWithString:@"http://img.pconline.com.cn/images/photoblog/9/9/8/1/9981681/200910/11/1255259355826.jpg"];
// 加载图片
NSData *data = [NSData dataWithContentsOfURL:url];
// 生成图片
image1 = [UIImage imageWithData:data];
}];
__block UIImage *image2 = nil;
// 下载图片2
NSBlockOperation *download2 = [NSBlockOperation blockOperationWithBlock:^{
// 图片的网络路径
NSURL *url = [NSURL URLWithString:@"http://pic38.nipic.com/20140228/5571398_215900721128_2.jpg"];
// 加载图片
NSData *data = [NSData dataWithContentsOfURL:url];
// 生成图片
image2 = [UIImage imageWithData:data];
}];
// 合成图片
NSBlockOperation *combine = [NSBlockOperation blockOperationWithBlock:^{
// 开启新的图形上下文
UIGraphicsBeginImageContext(CGSizeMake(100, 100));
// 绘制图片
[image1 drawInRect:CGRectMake(0, 0, 50, 100)];
image1 = nil;
[image2 drawInRect:CGRectMake(50, 0, 50, 100)];
image2 = nil;
// 取得上下文中的图片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// 结束上下文
UIGraphicsEndImageContext();
// 回到主线程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imageView.image = image;
}];
}];
[combine addDependency:download1];
[combine addDependency:download2];
[queue addOperation:download1];
[queue addOperation:download2];
[queue addOperation:combine];
简单的,只有下载图片然后放到主线程展示的线程通信如下:
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
// 图片的网络路径
NSURL *url = [NSURL URLWithString:@"http://img.pconline.com.cn/images/photoblog/9/9/8/1/9981681/200910/11/1255259355826.jpg"];
// 加载图片
NSData *data = [NSData dataWithContentsOfURL:url];
// 生成图片
UIImage *image = [UIImage imageWithData:data];
// 回到主线程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imageView.image = image;
}];
}];
4 小结
本文主要讲解了 GCD 和 NSOperation 两种多线程的创建和使用方式。加上上篇文章 共有 pthread 、 NSThread 、 GCD 和 NSOperation 四种多线程方案,实际使用中需要根据项目需求灵活使用。