今天温故了一下有关 GCD 编程的相关知识,做了个小Demo,梳理一下。其中也是用到在github
上下载的别的大牛封装好的一个GCD源代码,使用很方便,另外里面也用到了SDWebImage
这个第三方库,设置网络图片
Demo的源代码放在 Coding.net 上,有需要可以下载查看。
主要内容
- GCD串行队列与并发队列
- GCD延时执行
- GCD线程组
- GCD定时器
- GCD信号量
- GCD综合使用示例
GCD串行队列与并发队列
- 串行队列:串行队列一次只执行一个线程,按照添加到队列的顺序依次执行
- 并发队列: 一次可以执行多个线程,线程之间没有先后顺序
- UI界面所在线程是串行队列
首先创建串行队列
// 创建串行队列
- (void)serailQueue {
// 创建队列
GCDQueue *queue = [[GCDQueue alloc] initSerial];
//执行队列中的线程1
[queue execute:^{
NSLog(@"线程1");
}];
//执行队列中的线程2
[queue execute:^{
NSLog(@"线程2");
}];
//执行队列中的线程3
[queue execute:^{
NSLog(@"线程3");
}];
//执行队列中的线程4
[queue execute:^{
NSLog(@"线程4");
}];
//执行队列中的线程5
[queue execute:^{
NSLog(@"线程5");
}];
}
在 viewDidLoad
里调用该方法
// 执行串行队列
[self serailQueue];
输出结果能看出,线程执行是按顺序依次执行的
再看并发队列
// 创建并发队列
- (void)initConcurrent {
// 创建队列
GCDQueue *queue = [[GCDQueue alloc] initConcurrent];
//执行队列中的线程1
[queue execute:^{
NSLog(@"线程1");
}];
//执行队列中的线程2
[queue execute:^{
NSLog(@"线程2");
}];
//执行队列中的线程3
[queue execute:^{
NSLog(@"线程3");
}];
//执行队列中的线程4
[queue execute:^{
NSLog(@"线程4");
}];
//执行队列中的线程5
[queue execute:^{
NSLog(@"线程5");
}];
}
在 viewDidLoad
里调用该方法
// 执行并发队列
[self initConcurrent];
此时的输出结果就不再遵循有序执行了,如下图:
UI界面所在的线程是串行队列
这个知识点,也通过写一段代码来加载一张网络图片来演示
首先,声明两个属性
@interface ViewController ()
// GCD 执行队列与UI界面所在线程队列
@property (nonatomic, strong) UIImageView *imageView;
@property (nonatomic, strong) UIImage *image;
@end
然后写一个 UI界面所在的线程是串行队列
的方法
- (void)UIQueue {
self.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
self.imageView.center = self.view.center;
[self.view addSubview:self.imageView];
[GCDQueue executeInGlobalQueue:^{
// 处理业务逻辑
// 网络测试图片url
NSURL *imgUrl = [NSURL URLWithString:@"http://o7mwf03sy.bkt.clouddn.com/1460334144112.jpg"];
// 处理义务逻辑
NSLog(@"处理业务逻辑");
[GCDQueue executeInMainQueue:^{
// 获取图片更新UI
[self.imageView sd_setImageWithURL:imgUrl];
// 更新UI
NSLog(@"更新UI");
}];
}];
}
控制台输出和模拟器效果如下:
从结果我们也不难看出,UI所在的线程队列是串行队列。 所以有个注意的地方就是,在处理业务逻辑的地方,如果耗费的资源和时间较多,就会阻塞主线程的执行。
GCD延时执行
在测试 GCD延时执行 执行前面,我们先来看一下 普通的 NSThread 延时执行 效果:
先写一个方法:
- (void)setNSThread {
NSLog(@"启动");
[self performSelector:@selector(threadEvent:)
withObject:self
afterDelay:2.f]; // 延时2秒执行
}
// NSThread 延时执行的事件
- (void)threadEvent:(id)sender {
NSLog(@"NSThread 延时执行事件");
}
NSThread
延时执行不仅要写方法,还要写一个执行事件的方法,调用方法运行结果如下:
正好延时了2秒, 再来看 GCD延时执行 方法,代码相对来说比较简单
- (void)setGCDEvent {
NSLog(@"启动");
[GCDQueue executeInMainQueue:^{
NSLog(@"GCD 延时执行事件");
} afterDelaySecs:2.f];
}
不需要额外的声明方法,调用方法后运行的结果是这样的:
也能达到我们要的延迟效果
这里之前有个误区, 简单看打印时间觉得
NSTimer
延时执行任务是比较精准的,但最近查了一些资料,其实NSTimer
是不精准的, 因为它不是一个实时系统, 如果此时程序是多线程的, 而NSTimer
又处在其中一个runloop
中的某一特定 Mode 中,由于多线程都是分时执行的, 所以当NSTimer
需要执行任务的时候,很有可能当前线程还在执行任务,此时NSTimer
则会等待其执行完毕才会执行延时任务,所以说,NSTimer
是不精准的.
除此之外, GCD延迟执行
还可以取消延时事件的执行,只需在执行延时执行前加一行代码
[NSObject cancelPreviousPerformRequestsWithTarget:self];
而NSTimer
没有这个方法。
GCD线程组
在开发中,我们可能会有这样的要求:就是在执行完毕线程1和线程2之后,才去执行线程3。有一种方法是设置一个标志,线程1执行完了标志为1,线程2执行完了再+1变成2,线程3执行的时候先判断当前标志的值是多少,再来决定是否执行,貌似这种方法是可行的,但总感觉是不科学的,我们完全可以用GCD线程组来完成这个需求;
首先声明方法
- (void)setGCDGroup {
// 初始化线程组
GCDGroup *group = [[GCDGroup alloc] init];
// 创建一个线程队列
GCDQueue *queue = [[GCDQueue alloc] initConcurrent];
// 让线程在group中执行 线程1
[queue execute:^{
sleep(1);// 休眠
NSLog(@"线程1执行完毕");
} inGroup:group];
// 让线程在group中执行 线程2
[queue execute:^{
sleep(3);
NSLog(@"线程2执行完毕");
} inGroup:group];
// 监听线程组是否执行完毕,然后执行线程3
[queue notify:^{
NSLog(@"线程3执行完毕");
} inGroup:group];
}
为了展示效果,这里让线程1休眠1秒执行,线程2休眠3秒执行(也就是在线程1之后2秒执行),看看是否是我们想要的结果。
可见,GCD 线程组 实现了我们想要的结果.线程组的作用一般就是实现监听响应的线程执行完毕后才执行。
GCDTimer 定时器
同样,为了展示效果,我们还是先来看看NSTimer 的定时执行效果
首先,声明两个私有属性
@property (nonatomic, strong) GCDTimer *gcdTimer;
@property (nonatomic, strong) NSTimer *normalTimer;
接着,创建 NSTimer
方法
- (void)runNSTimer {
// 初始化并激活NSTimer
self.normalTimer = [NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:@selector(timerEvent)
userInfo:nil
repeats:YES];
}
// NSTimer 执行时间
- (void)timerEvent {
NSLog(@"运行NSTimer");
}
调用该方法执行结果为
再来看GCDTimer
声明方法:
- (void)runGCDTimer {
// 初始化GCDTimer
self.gcdTimer = [[GCDTimer alloc] initInQueue:[GCDQueue mainQueue]];
// 指定间隔时间
[self.gcdTimer event:^{
NSLog(@"运行GCDTimer");
} timeInterval:NSEC_PER_SEC];// NSEC_PER_SEC 宏 代表1 秒
// 运行GCDTimer
[self.gcdTimer start];
}
调用该方法执行结果为
可以看到,
GCDTimer
虽然在打印时间上有那么一点点误差, 但其实 GCD 的延时操作才是最为精准的,因为NSTimer
是运行在Runloop
里面的,而RunLoop
是在 GCD 基础上实现的,所以说 GCD 可算是更加精准. 并且如果把NSTimer
运行在一些例如UITableView
里面,可能会出现一些奇怪的问题,而GCDTimer
不存在这个问题。
GCDSemaphore (GCD信号量)
先来看看我们正常调用一个异步线程方法的输出结果
- (void)setGCDSemaphore {
// 异步线程 1
[GCDQueue executeInGlobalQueue:^{
NSLog(@"异步线程 1");
}];
// 异步线程 2
[GCDQueue executeInGlobalQueue:^{
NSLog(@"异步线程 2");
}];
}
在异步线程执行里,我们是无法确定是哪个线程先执行完毕的,从输出结果就能看出,第一次是这样的:
第二次是这样的:
第三次是这样的:
、、、、、
以此可见,它的执行顺序是不固定的,这个时候,如果使用GCDSemaphore
,就能按着我们的意愿顺序执行线程了。
创建GCDSemaphore
并在响应位置加入消息指令:
- (void)setGCDSemaphore {
// 创建GCDSemaphore 信号量
GCDSemaphore *semaphore = [[GCDSemaphore alloc] init];
// 异步线程 1
[GCDQueue executeInGlobalQueue:^{
NSLog(@"异步线程 1");
// 发送执行完毕信号
[semaphore signal];
}];
// 异步线程 2
[GCDQueue executeInGlobalQueue:^{
// 等待接收执行完毕信号才开始执行进程
[semaphore wait];
NSLog(@"异步线程 2");
}];
// 作用: 必须线程1先执行完毕,然后在执行线程2
// 将异步线程转化成同步线程。
}
这样,每次输出顺序就是一样的
这样,当我们有这种按顺序执行的特殊需求时,
GCDSemaphore
就可以发挥很好的作用了。
GCD 综合使用介绍示例
这里我们通过并发下载三张网络图片的例子来延时 GCD 的强大作用。首先生命三个私有属性
@property (nonatomic, strong) UIImageView *view1;
@property (nonatomic, strong) UIImageView *view2;
@property (nonatomic, strong) UIImageView *view3;
为了等下代码更加方便操作,简化代码,我们声明一个设置view的方法,这里同样用到SDWebImage
获取图片。
// 创建 imageview ,需要frame 和 图片网络地址 的参数
- (UIImageView *)createImageViewWithFrame:(CGRect)frame imageUreStr:(NSString *)string {
UIImageView *imageView = [[UIImageView alloc] initWithFrame:frame];
imageView.alpha = 0.f; // 透明度设置为0
[imageView sd_setImageWithURL:[NSURL URLWithString:string]];
[self.view addSubview:imageView];
return imageView;
}
接着我们创建一个方法,就叫 setGCD
吧。
- (void)setGCD {
NSString *img1 = @"http://o7mwf03sy.bkt.clouddn.com/1460334156212.jpg";
NSString *img2 = @"http://o7mwf03sy.bkt.clouddn.com/1460334134611.jpg";
NSString *img3 = @"http://o7mwf03sy.bkt.clouddn.com/1460334144112.jpg";
self.view1 = [self createImageViewWithFrame:CGRectMake(50, 50, 200, 100) imageUreStr:img1];
self.view2 = [self createImageViewWithFrame:CGRectMake(50, 200, 200, 100) imageUreStr:img2];
self.view3 = [self createImageViewWithFrame:CGRectMake(50, 350, 200, 100) imageUreStr:img3];
// 在子线程中完成下载 图片1
[GCDQueue executeInGlobalQueue:^{
// 在主线程中更新UI
[GCDQueue executeInMainQueue:^{
NSLog(@"线程 1 开始执行");
// 2秒动画显示图片
[UIView animateWithDuration:2.f animations:^{
self.view1.alpha = 1.f;
} completion:^(BOOL finished) {
NSLog(@"线程 1 执行完毕");
}];
}];
}];
// 在子线程中完成下载 图片2
[GCDQueue executeInGlobalQueue:^{
NSLog(@"线程 2 开始执行");
// 在主线程中更新UI
[GCDQueue executeInMainQueue:^{
// 2秒动画显示图片
[UIView animateWithDuration:2.f animations:^{
self.view2.alpha = 1.f;
} completion:^(BOOL finished) {
NSLog(@"线程 2 执行完毕");
}];
}];
}];
// 在子线程中完成下载 图片3
[GCDQueue executeInGlobalQueue:^{
NSLog(@"线程 3 开始执行");
// 在主线程中更新UI
[GCDQueue executeInMainQueue:^{
// 2秒动画显示图片
[UIView animateWithDuration:2.f animations:^{
self.view3.alpha = 1.f;
} completion:^(BOOL finished) {
NSLog(@"线程 3 执行完毕");
}];
}];
}];
}
此时的输出结果是:
可见三张图片几乎是同时开始执行, 同时执行完毕。 但是假如我们有一种需求,要三张图片按顺序一张一张的出现,该怎么办呢?
对了,前面介绍过,使用
GCDSemaphore
信号量!创建
GCDSemaphore
并在响应位置插入信号代码
...
self.view3 = [self createImageViewWithFrame:CGRectMake(50, 350, 200, 100) imageUreStr:img3];
GCDSemaphore *semaphore = [[GCDSemaphore alloc] init];
// 在子线程中完成下载 图片1
[GCDQueue executeInGlobalQueue:^{
// 在主线程中更新UI
[GCDQueue executeInMainQueue:^{
NSLog(@"线程 1 开始执行");
// 2秒动画显示图片
[UIView animateWithDuration:2.f animations:^{
self.view1.alpha = 1.f;
} completion:^(BOOL finished) {
NSLog(@"线程 1 执行完毕");
// 发送执行完毕信号
[semaphore signal];
}];
}];
}];
// 在子线程中完成下载 图片2
[GCDQueue executeInGlobalQueue:^{
// 阻塞执行,等待消息
[semaphore wait];
NSLog(@"线程 2 开始执行");
// 在主线程中更新UI
[GCDQueue executeInMainQueue:^{
// 2秒动画显示图片
[UIView animateWithDuration:2.f animations:^{
self.view2.alpha = 1.f;
} completion:^(BOOL finished) {
NSLog(@"线程 2 执行完毕");
// 发送执行完毕信号
[semaphore signal];
}];
}];
}];
// 在子线程中完成下载 图片3
[GCDQueue executeInGlobalQueue:^{
// 阻塞执行,等待消息
[semaphore wait];
NSLog(@"线程 3 开始执行");
// 在主线程中更新UI
[GCDQueue executeInMainQueue:^{
// 2秒动画显示图片
[UIView animateWithDuration:2.f animations:^{
self.view3.alpha = 1.f;
} completion:^(BOOL finished) {
NSLog(@"线程 3 执行完毕");
}];
}];
}];
}
这样,在线程1执行完毕后发送信号,线程2接收线程1发送的信号再开始执行,完毕之后再给线程3发送信号,以此类推,就能实现按顺序执行线程。输出结果如下:
三个线程的开始执行和结束执行的时间有了明显差别,实现了我们想要的效果。
总结
到这里,我们基本能学到一下几点有关GCD的实用知识点
- GCD串行队列与并发队列
- GCD延时执行操作使用
- GCD线程组的操作使用
- GCD定时器的使用
- GCD信号量将异步操作转变成同步操作的使用