iOS - GCD编程

今天温故了一下有关 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];

输出结果能看出,线程执行是按顺序依次执行的


iOS - GCD编程_第1张图片
输出结果

再看并发队列

// 创建并发队列
- (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];

此时的输出结果就不再遵循有序执行了,如下图:


iOS - GCD编程_第2张图片
输出结果

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");  
        }];
    }];
}

控制台输出和模拟器效果如下:


iOS - GCD编程_第3张图片
结果

从结果我们也不难看出,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");
}

调用该方法执行结果为


iOS - GCD编程_第4张图片
结果

再来看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];
}

调用该方法执行结果为


iOS - GCD编程_第5张图片
结果

可以看到,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 执行完毕");
            }];
        }];
    }];
}

此时的输出结果是:

iOS - GCD编程_第6张图片
输出结果

可见三张图片几乎是同时开始执行, 同时执行完毕。 但是假如我们有一种需求,要三张图片按顺序一张一张的出现,该怎么办呢?
对了,前面介绍过,使用 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发送信号,以此类推,就能实现按顺序执行线程。输出结果如下:


iOS - GCD编程_第7张图片
结果

三个线程的开始执行和结束执行的时间有了明显差别,实现了我们想要的效果。

总结

到这里,我们基本能学到一下几点有关GCD的实用知识点

  • GCD串行队列与并发队列
  • GCD延时执行操作使用
  • GCD线程组的操作使用
  • GCD定时器的使用
  • GCD信号量将异步操作转变成同步操作的使用

你可能感兴趣的:(iOS - GCD编程)