玩转iOS开发:实战开发中的GCD Tips小技巧 (一)

文章分享至我的个人技术博客: https://cainluo.github.io/15061581251764.html


这篇文章主要是给之前的GCD文章作为一个补充, 顺便讲讲一些在实际开发中遇到的问题和一些解决的办法, 如果没有看过之前文章的朋友可以去看看:

转载声明:如需要转载该文章, 请联系作者, 并且注明出处, 以及不能擅自修改本文.


线程死锁与解决办法

不恰当的使用线程造成的死锁

在我们实际开发中, 什么时候会遇到线程死锁呢? 估计有很多小伙伴就可以想到了, 就是在主队列里同步执行不同线程的时候:

- (void)mineQueueLockThread {
    
    dispatch_queue_t queue = dispatch_queue_create("mineQueue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_sync(queue, ^{
        
        NSLog(@"第一次执行, %@", [NSThread currentThread]);
        
        dispatch_sync(queue, ^{
            
            NSLog(@"第二次执行, %@", [NSThread currentThread]);
        });
    });
}
复制代码

打印结果:

2017-09-23 17:40:05.147 GCD-Tips[56843:3169265] 开始执行
2017-09-23 17:40:05.147 GCD-Tips[56843:3169265] 第一次执行, <NSThread: 0x60000007c8c0>{number = 1, name = main}
(lldb) 
复制代码

1

看到上面的图, 我们就可以看到任务执行到这里就死掉了, 也就是我们所说的卡线程, 那我们改改吧, 有的朋友会想到, 我把里面的那个任务改成异步不就好了么? 那我们来看看:

- (void)mineQueueLockThread {
    
    dispatch_queue_t queue = dispatch_queue_create("mineQueue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_sync(queue, ^{
        
        NSLog(@"第一次执行, %@", [NSThread currentThread]);
        
        dispatch_async(queue, ^{
            
            NSLog(@"第二次执行, %@", [NSThread currentThread]);
        });
    });
}
复制代码

打印结果:

2017-09-23 17:44:12.488 GCD-Tips[56903:3176227] 开始执行
2017-09-23 17:44:12.488 GCD-Tips[56903:3176227] 第一次执行, <NSThread: 0x60000006ac40>{number = 1, name = main}
2017-09-23 17:44:12.489 GCD-Tips[56903:3176345] 第二次执行, <NSThread: 0x600000075ec0>{number = 3, name = (null)}
复制代码

事实告诉我们这是正常的, 那还有另一种呢? 直接改外部的任务为异步执行怎么样?

- (void)mineQueueLockThread {
    
    dispatch_queue_t queue = dispatch_queue_create("mineQueue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(queue, ^{
        
        NSLog(@"第一次执行, %@", [NSThread currentThread]);
        
        dispatch_sync(queue, ^{
            
            NSLog(@"第二次执行, %@", [NSThread currentThread]);
        });
    });
}
复制代码
2017-09-23 17:45:52.740 GCD-Tips[56936:3178609] 开始执行
2017-09-23 17:45:52.741 GCD-Tips[56936:3178739] 第一次执行, <NSThread: 0x600000070640>{number = 3, name = (null)}
(lldb) 
复制代码

2

看到结果, 挂了, 为什么呢? 按道理来说, 外部是异步, 而里面是同步是不会卡死的, 其实在之前的文章里我们就提到过.

首先, 我们知道第一个是异步执行的, 这是没有问题的, 问题就是在第二个任务, 我们都知道这里是在主线程中执行, 但是别忘了, 主线程正在执行着任务, 所以这时候我们再去执行, 那问题就出现了, 所以就卡死了啦~

解决的办法, 有两个:

  • 1.要么把第二个任务变成异步执行.
  • 2.要么把两个任务都变成异步执行.

PS: 这里不要在同步执行嵌套串行队列, 哪怕你是分开小方法里也是一样的.

使用dispatch_apply造成的死锁

当我们不恰当的时候dispatch_apply的时候也会造成死锁的情况:

- (void)applyLockThread {
    
    dispatch_queue_t queue = dispatch_queue_create("applyQueue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_apply(3, queue, ^(size_t i) {
        
        NSLog(@"延迟添加一");
        
        dispatch_apply(3, queue, ^(size_t j) {
            
            NSLog(@"延迟添加二");
        });
    });
}
复制代码

打印的结果:

2017-09-24 00:13:10.760 GCD-Tips[57745:3330022] 开始执行
2017-09-24 00:13:10.761 GCD-Tips[57745:3330022] 延迟添加一
(lldb)
复制代码

3

这里我们是串行队列, 所以大致道理和上面的差不多, 那怎么解决呢? 我们可以把串行队列, 改成并行队列:

- (void)applyLockThread {
    
    dispatch_queue_t queue = dispatch_queue_create("applyQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_apply(3, queue, ^(size_t i) {
        
        NSLog(@"延迟添加一");
        
        dispatch_apply(3, queue, ^(size_t j) {
            
            NSLog(@"延迟添加二");
        });
    });
}
复制代码
2017-09-24 00:13:58.238 GCD-Tips[57777:3331061] 开始执行
2017-09-24 00:13:58.239 GCD-Tips[57777:3331061] 延迟添加一
2017-09-24 00:13:58.239 GCD-Tips[57777:3331183] 延迟添加一
2017-09-24 00:13:58.239 GCD-Tips[57777:3331061] 延迟添加二
2017-09-24 00:13:58.239 GCD-Tips[57777:3331198] 延迟添加一
2017-09-24 00:13:58.239 GCD-Tips[57777:3331061] 延迟添加二
2017-09-24 00:13:58.239 GCD-Tips[57777:3331183] 延迟添加二
2017-09-24 00:13:58.240 GCD-Tips[57777:3331061] 延迟添加二
2017-09-24 00:13:58.240 GCD-Tips[57777:3331198] 延迟添加二
2017-09-24 00:13:58.240 GCD-Tips[57777:3331183] 延迟添加二
2017-09-24 00:13:58.240 GCD-Tips[57777:3331198] 延迟添加二
2017-09-24 00:13:58.240 GCD-Tips[57777:3331183] 延迟添加二
2017-09-24 00:13:58.240 GCD-Tips[57777:3331198] 延迟添加二
复制代码

队列组的等待

之前我们在文章里有提过使用dispatch_apply, 这是一个延迟提交任务到线程中执行的方法, 除了这个任务延迟提交之外, 在队列组当中也有一个对应的方法, 叫做dispatch_after.

- (void)groupQueueAfter {
    
    dispatch_time_t timeOne = dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC);
    dispatch_time_t timeTwo = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
    
    dispatch_queue_t mainQueue = dispatch_get_main_queue();

    dispatch_after(timeOne, mainQueue, ^{
        
        NSLog(@"延迟添加一");
    });
    
    dispatch_after(timeTwo, mainQueue, ^{

        NSLog(@"延迟添加二");
    });
}
复制代码

打印的结果:

2017-09-24 00:28:35.205 GCD-Tips[57882:3349738] 开始执行
2017-09-24 00:28:37.405 GCD-Tips[57882:3349738] 延迟添加一
2017-09-24 00:28:38.504 GCD-Tips[57882:3349738] 延迟添加二
复制代码

从打印中, 我们可以看得出的确是延迟添加了, 但这里需要注意一点, dispatch_after只是延迟提交Block而已, 并不是延后立即执行, 不能做到精准控制的.

这里解释一下dispatch_time的第二个参数:

#define NSEC_PER_SEC 1000000000ull
#define NSEC_PER_MSEC 1000000ull
#define USEC_PER_SEC 1000000ull
#define NSEC_PER_USEC 1000ull
复制代码
  • NSEC_PER_SEC,每秒有多少纳秒。
  • USEC_PER_SEC,每秒有多少毫秒。(注意是指在纳秒的基础上)
  • NSEC_PER_USEC,每毫秒有多少纳秒。

如果我们要延迟1秒的话, 可以有几种写法:

dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC);

dispatch_time(DISPATCH_TIME_NOW, 1000 * USEC_PER_SEC);

dispatch_time(DISPATCH_TIME_NOW, USEC_PER_SEC * NSEC_PER_USEC);
复制代码

最后一个USEC_PER_SEC * NSEC_PER_USEC,翻译过来就是每秒的毫秒数乘以每毫秒的纳秒数,也就是每秒的纳秒数,所以,延时500毫秒之类的,也就不难了吧~

dispatch_after的嵌套

在这里, dispatch_after是可以嵌套使用的, 但需要注意的是, 虽然并不会造成锁线程, 但会导致延迟提交Block提前:

- (void)groupQueueAfterNested {
    
    dispatch_time_t timeOne = dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC);
    dispatch_time_t timeTwo = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
    
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    dispatch_after(timeOne, mainQueue, ^{
        
        NSLog(@"延迟添加一");
        
        
        dispatch_after(timeTwo, mainQueue, ^{
            
            NSLog(@"延迟添加二");
        });
    });
}
复制代码
2017-09-24 00:31:16.810 GCD-Tips[57923:3354999] 开始执行
2017-09-24 00:31:19.010 GCD-Tips[57923:3354999] 延迟添加一
2017-09-24 00:31:19.885 GCD-Tips[57923:3354999] 延迟添加二
复制代码

所以我们在这里使用dispatch_after的时候, 要注意延迟添加的时机了.


dispatch_apply的注意

在之前的文章里, 我们有提过dispatch_apply, 这里补充一点就是它在使用的时候, 无论是串行还是并行队列都一样, 它将会阻塞主线程, 我们来看代码:

- (void)queueApply {
    
    NSLog(@"开始执行, 当前线程为:%@", [NSThread currentThread]);
    
    dispatch_queue_t queueOne = dispatch_queue_create("applyQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queueTwo = dispatch_queue_create("applyQueue", DISPATCH_QUEUE_CONCURRENT);

    dispatch_apply(3, queueOne, ^(size_t i) {
        
        NSLog(@"延迟执行一: %zu, 当前线程为:%@", i, [NSThread currentThread]);
    });
    
    dispatch_apply(3, queueTwo, ^(size_t i) {
        
        NSLog(@"延迟执行二: %zu, 当前线程为:%@", i, [NSThread currentThread]);
    });
    
    NSLog(@"结束执行, 当前线程为:%@", [NSThread currentThread]);
}
复制代码
2017-09-24 00:44:04.704 GCD-Tips[58065:3373668] 开始执行
2017-09-24 00:44:04.705 GCD-Tips[58065:3373668] 开始执行, 当前线程为:<NSThread: 0x600000067880>{number = 1, name = main}
2017-09-24 00:44:04.705 GCD-Tips[58065:3373668] 延迟执行一: 0, 当前线程为:<NSThread: 0x600000067880>{number = 1, name = main}
2017-09-24 00:44:04.706 GCD-Tips[58065:3373668] 延迟执行一: 1, 当前线程为:<NSThread: 0x600000067880>{number = 1, name = main}
2017-09-24 00:44:04.706 GCD-Tips[58065:3373668] 延迟执行一: 2, 当前线程为:<NSThread: 0x600000067880>{number = 1, name = main}
2017-09-24 00:44:04.706 GCD-Tips[58065:3373668] 延迟执行二: 0, 当前线程为:<NSThread: 0x600000067880>{number = 1, name = main}
2017-09-24 00:44:04.706 GCD-Tips[58065:3373784] 延迟执行二: 1, 当前线程为:<NSThread: 0x60800006f440>{number = 3, name = (null)}
2017-09-24 00:44:04.706 GCD-Tips[58065:3373786] 延迟执行二: 2, 当前线程为:<NSThread: 0x60800006f380>{number = 4, name = (null)}
2017-09-24 00:44:04.707 GCD-Tips[58065:3373668] 结束执行, 当前线程为:<NSThread: 0x600000067880>{number = 1, name = main}
复制代码

总结

好了, 这次就讲到这里, 一下子讲太多也不好消化, 剩下的留着之后再讲吧.


工程地址

项目地址: https://github.com/CainRun/iOS-Project-Example/tree/master/GCD-Tips/GCD-Tips-One


最后

码字很费脑, 看官赏点饭钱可好

微信

支付宝