首页 > 学院 > 开发设计 > 正文

iOS开发之GCD基础

2019-11-14 20:23:06
字体:
来源:转载
供稿:网友

重新回顾、学习GCD、Block。先贴出一篇不错的讲解GCD基础使用的文章

原文地址:http://blog.csdn.net/aolan1108/article/details/17283415

 

 

做了2年的ios开发,很想静下心来想想,做一些总结,但是苦于生活和工作方面的种种原因,一直没能如愿。今天终于下定决心,把自己所学所想记录下来,方便以后查看,同时可供大家分享。

 
就像之前说的,做了两年的ios开发,只知道不断的copy代码,如果你要真的问我GCD是神马东东,我一时半会也说不清楚。这里我要为自己生存在互联网时代感觉到庆幸,我在网上翻阅了很多资料,大都不太完整,在这些资料中,我不断筛选,终于把GCD给整明白了。我不喜欢说官方的话,说一个东西,从他的定义开始,这个比较俗套,如果大家真的不明白GCD是啥意思,可以自己找去。
 
队列种类:
一、主线程的Main queue,通过dispatch_get_main_queue获取。
二、并行队列global dispatch queue,通过dispatch_get_global_queue获取,由系统创建(不需要开发人员去创建)三个不同优先级的dispatch queue。并行队列的执行顺序与其加入队列的顺序相同。
三、串行队列serial queues一般用于按顺序同步访问,可创建任意数量的串行队列,各个串行队列之间是并发的。一般用dispatch_queue_create来进行创建,非arc的情况下需要用户手动来释放队列,可能会有人说,既然队列也是一种对象,可以创建和释放,那一定会有引用计数器了,是的,可以使用函数dispatch_retain和dispatch_release来增加或者减少引用计数器。
 
提交job:
在举例子给大家看之前,我还想说说两种提交job的方式:dispatch_async和dispatch_sync,分别是异步执行和同步执行,两者之前的区别在于,前者在把任务提交到队列执行不会造成阻塞,而后者后面的代码块需要等到队列中的任务执行完成后才可以执行。
 
下面是例子
 
例子一,我们可以看到首先执行的是第二条打印语句,这说明这条语句没有被阻塞。
 
[objc] view plaincopy
 
  1. //主线程异步执行  -(void)action1{                    dispatch_async(dispatch_get_main_queue(), ^{                    for (int i = 0; i< 5; i++) NSLog(@"主线程异步执行===========%d",i);                            });                    NSLog(@"=================主线程异步执行");      }  

     



打印结果:
 

2013-12-09 14:36:20.863 TestGCD[872:a0b] =================主线程异步执行

2013-12-09 14:36:20.864 TestGCD[872:a0b] 主线程异步执行===========0

2013-12-09 14:36:20.865 TestGCD[872:a0b] 主线程异步执行===========1

2013-12-09 14:36:20.865 TestGCD[872:a0b] 主线程异步执行===========2

2013-12-09 14:36:20.865 TestGCD[872:a0b] 主线程异步执行===========3

2013-12-09 14:36:20.866 TestGCD[872:a0b] 主线程异步执行===========4

 
 
 
 
 
例子二,会有人问,既然主线程可以异步执行,那么主线程也可以同步执行喽。的确是可以的,但是不能直接在action1里面将dispatch_async改为dispatch_sync,这样修改是没有意义的,大家可以试试,里面的任务根本不会执行。
 
[objc] view plaincopy
 
  1. //主线程同步执行  -(void)action2{            dispatch_async(dispatch_get_global_queue(0, 0), ^{                    for (int i = 0; i< 3; i++) NSLog(@"并发线程异步执行===========%d",i);                    dispatch_sync(dispatch_get_main_queue(), ^{                            for (int i = 0; i< 3; i++) NSLog(@"主线程同步执行===========%d",i);                        });                     NSLog(@"===========主线程执行完毕");      });             NSLog(@"===========并发线程可能正在执行");  }  

     

打印结果:

 

2013-12-09 15:22:22.352 TestGCD[1269:a0b] ===========并发线程可能正在执行

2013-12-09 15:22:22.352 TestGCD[1269:1403] 并发线程异步执行===========0

2013-12-09 15:22:22.355 TestGCD[1269:1403] 并发线程异步执行===========1

2013-12-09 15:22:22.356 TestGCD[1269:1403] 并发线程异步执行===========2

2013-12-09 15:22:22.357 TestGCD[1269:a0b] 主线程同步执行===========0

2013-12-09 15:22:22.358 TestGCD[1269:a0b] 主线程同步执行===========1

2013-12-09 15:22:22.358 TestGCD[1269:a0b] 主线程同步执行===========2

2013-12-09 15:22:22.359 TestGCD[1269:1403] ===========主线程执行完毕

 

从结果中我们仔细理解,就会比较深刻,首先我们看到打印语句4最先执行(有可能不是最先执行,这个是由系统决定的),说明我们提交到并发队列dispatch_get_global_queue的执行方式是异步的;其次打印语句2是在打印语句1执行完才开始执行,这说明dispatch_get_global_queue虽然是并发队列,但是其内部的任务执行顺序是串行的;最后,我们看到打印语句3是在打印语句2执行完成后再执行,说明主线程同步执行是阻塞的,我们通常会将UI的刷新用同步方式放到主线程中去操作,当然这种操作的时间一般都比较短,以至于用户几乎无法察觉。

 

例子三,其实GCD的操作真正的意义当然不在于在主线程中做一些操作,在很多时候,我们都会用到后台线程,如果你的iphone是多核的cpu,那么恭喜你,你可以有流畅的操作体验,即使不是多核,你的操作体验也相对流畅,因为你可以将需要大量时间才能执行完成的任务放到后台线程中去执行,用户就不会有死机感,譬如说图片加载、网络请求、复杂逻辑的数据解析等等。

 

//后台异步执行  
-(void)action3{            dispatch_async(dispatch_get_global_queue(0, 0), ^{                    for (int i = 0; i< 10000; i++) NSLog(@"=================%d",i);                });            NSLog(@"=================后台异步执行");  } 

 

 

你可以迅速点击两次按钮,即连续执行两次action3方法,在这里,我们截取部分打印结果:

 

 

2013-12-09 15:51:30.864 TestGCD[1350:3007] =================0

2013-12-09 15:51:30.864 TestGCD[1350:a0b] =================后台异步执行

2013-12-09 15:51:30.864 TestGCD[1350:3007] =================1

2013-12-09 15:51:30.865 TestGCD[1350:3007] =================2

2013-12-09 15:51:30.865 TestGCD[1350:3007] =================3

......

 

2013-12-09 15:51:31.007 TestGCD[1350:3007] =================600

2013-12-09 15:51:31.007 TestGCD[1350:3007] =================601

2013-12-09 15:51:31.007 TestGCD[1350:a0b] =================后台异步执行

2013-12-09 15:51:31.007 TestGCD[1350:3007] =================602

2013-12-09 15:51:31.008 TestGCD[1350:3007] =================603

2013-12-09 15:51:31.007 TestGCD[1350:473] =================0

2013-12-09 15:51:31.008 TestGCD[1350:473] =================1

2013-12-09 15:51:31.008 TestGCD[1350:473] =================2

2013-12-09 15:51:31.008 TestGCD[1350:3007] =================604

2013-12-09 15:51:31.008 TestGCD[1350:473] =================3

2013-12-09 15:51:31.008 TestGCD[1350:3007] =================605

......

 

我们看到由于action3被执行了两次,在点击第一次后,屏幕仍然接受点击时间,说明主线程没有被阻塞,用户体验仍然很流畅。两次点击实际上是提交了两个任务到dispatch_get_global_queue队列上,第一次任务并没有执行完成,第二次任务就开始执行,说明dispatch_get_global_queue是并行队列,即任务块与任务块之间是并发执行的。

 

 

 

例子四,肯定会有小伙伴们说后台既然可以异步执行,那么应该也可以同步执行喽,是的,你是对的,但是在实际的开发过程中没有什么意义,我们会发现改方法会产生阻塞,但是却不能用来刷新UI,也没有体现多核处理器的优势。

 

  1. //后台同步执行  -(void)action4{            dispatch_sync(dispatch_get_global_queue(0, 0), ^{                    for (int i = 0; i< 10000; i++) NSLog(@"=================%d",i);                });            NSLog(@"=================后台同步执行");  }  

     

我们也是连续点击两次按钮,执行两次action4方法,我们看到的打印结果让我们很惊讶,可能会有人说,dispatch_get_global_queue队列不是并发队列吗,怎么执行的结果是这样的。在这里我需要解释下,由于提交给全局队列的执行方式是同步的,这里实际上是产生了阻塞,即必须是该任务完成后才能执行下面的任务,我们看到打印语句2执行的位置就知道了。所以同步一般在刷新UI界面或者处理共享数据的时候使用,而且任务的处理时间不能太长,会影响用户体验。

 

 

2013-12-09 16:04:35.967 TestGCD[1385:a0b] =================0

2013-12-09 16:04:35.969 TestGCD[1385:a0b] =================1

2013-12-09 16:04:35.969 TestGCD[1385:a0b] =================2

......

2013-12-09 16:04:40.645 TestGCD[1385:a0b] =================9997

2013-12-09 16:04:40.645 TestGCD[1385:a0b] =================9998

2013-12-09 16:04:40.646 TestGCD[1385:a0b] =================9999

2013-12-09 16:04:40.646 TestGCD[1385:a0b] =================后台同步执行

2013-12-09 16:04:40.647 TestGCD[1385:a0b] =================0

2013-12-09 16:04:40.647 TestGCD[1385:a0b] =================1

2013-12-09 16:04:40.648 TestGCD[1385:a0b] =================2

2013-12-09 16:04:40.648 TestGCD[1385:a0b] =================3

......

 

2013-12-09 16:04:45.218 TestGCD[1385:a0b] =================9998

2013-12-09 16:04:45.218 TestGCD[1385:a0b] =================9999

2013-12-09 16:04:45.218 TestGCD[1385:a0b] =================后台同步执行

 

 

 

例子五,开发人员可以自己定义一个队列用于执行后台线程,同全局队列使用方法基本类似,在非arc的情况下,需要开发人员手动释放。

 

 
  1. //自定义dispatch_queue_t  -(void)action5{            dispatch_queue_t urls_queue = dispatch_queue_create("myQueue", NULL);            dispatch_async(urls_queue, ^{                    for (int i = 0; i< 5; i++) NSLog(@"自定义dispatch_queue_t===========%d",i);                    dispatch_sync(dispatch_get_main_queue(), ^{                           NSLog(@"同主线程交互===========");          });        });            dispatch_release(urls_queue);   //arc不要  }  

     

打印结果:

 

 

2013-12-09 16:43:52.416 TestGCD[1464:1417] 自定义dispatch_queue_t===========0

2013-12-09 16:43:52.417 TestGCD[1464:1417] 自定义dispatch_queue_t===========1

2013-12-09 16:43:52.418 TestGCD[1464:1417] 自定义dispatch_queue_t===========2

2013-12-09 16:43:52.419 TestGCD[1464:1417] 自定义dispatch_queue_t===========3

2013-12-09 16:43:52.420 TestGCD[1464:1417] 自定义dispatch_queue_t===========4

2013-12-09 16:43:52.420 TestGCD[1464:a0b] 同主线程交互===========

 

 

例子六,除此之外,调度方式还有延迟执行。

 

 

  1. //延迟3秒执行  -(void)action6{            double delayInSeconds = 3.0;      dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));      dispatch_after(popTime, dispatch_get_main_queue(), ^(void){                    NSLog(@"延迟3秒执行===========");        });  }  

     

打印结果:

 

 

2013-12-09 16:48:58.473 TestGCD[1484:a0b] 延迟3秒执行===========

 
 

例子七,一次性执行,主要用于创建单例对象。在ios4.0以前,我们创建单例会用到互斥锁@synchronized来确保其他线程没有对self对象进行修改,一般在共用变量的时候使用。

 
  1. + (NetworkManager *)getNetworkInstance{         @synchronized(self){                    if (nil == network) network = [[NetworkManager alloc] init];      }          return network;  }  

     

ios4.0之后,我们可以用GCD创建单例,下面的GCD语法只会执行一次,代码如下:

 

 
  1. +(UserManager *)sharedManager{            static UserManager *_manager = nil;            static dispatch_once_t onceToken;            dispatch_once(&onceToken, ^{                    _manager = [[UserManager alloc]init];        });            return _manager;  }  

     

该方法的好处有:一、线程安全;二、很好满足静态分析器的要求;三、兼容ARC;四、仅需要少量代码。

 

 

 

例子八,合并汇总结果,dispatch_group_t队列非常好用,当我们有多个异步执行的队列在执行,我们还有一个任务需要用到这多个异步执行队列执行的结果时,我们就会用到dispatch_group_t,废话不多说,直接上代码:

 

 

 
  1. //合并汇总结果  -(void)action8{         dispatch_group_t group = dispatch_group_create();            dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{                    for (int i = 0; i < 5; i++) NSLog(@"并行执行的线程一=============%d",i);      });            dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{                    for (int i = 0; i < 5; i++) NSLog(@"并行执行的线程二=============%d",i);        });            dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{                   NSLog(@"汇总结果===========");        });            dispatch_release(group);  }  

     

 

打印结果:
 

2013-12-10 11:48:39.139 TestGCD[699:1403] 并行执行的线程一=============0

2013-12-10 11:48:39.139 TestGCD[699:3807] 并行执行的线程二=============0

2013-12-10 11:48:39.143 TestGCD[699:1403] 并行执行的线程一=============1

2013-12-10 11:48:39.143 TestGCD[699:3807] 并行执行的线程二=============1

2013-12-10 11:48:39.144 TestGCD[699:1403] 并行执行的线程一=============2

2013-12-10 11:48:39.144 TestGCD[699:3807] 并行执行的线程二=============2

2013-12-10 11:48:39.145 TestGCD[699:1403] 并行执行的线程一=============3

2013-12-10 11:48:39.145 TestGCD[699:3807] 并行执行的线程二=============3

2013-12-10 11:48:39.146 TestGCD[699:3807] 并行执行的线程二=============4

2013-12-10 11:48:39.146 TestGCD[699:1403] 并行执行的线程一=============4

2013-12-10 11:48:39.147 TestGCD[699:3807] 汇总结果===========

 

我们看到第三条打印语句是在前两条打印语句执行之后才执行。

 

这也是我综合了官方文档和网上的很多资料总结出来的,并且用实际的代码调试出来的结果,希望能给大家一些帮助,对于其中的不足,欢迎大家给出不同意见。

 

 

参考:http://www.VEVb.com/pure/archive/2013/03/31/2977420.html

 

 

 

 

2014_07_16今天再次翻看GCD。再浏览过程中,发现了另外一篇不错的讲解队列的帖子,现转发之,希望能对你有所帮助和启发,里面提到死锁的问题,可以思考和交流。

原文地址:http://www.VEVb.com/sunfrog/p/3305614.html

GCD编程的核心就是dispatch队列,dispatch block的执行最终都会放进某个队列中去进行,它类似NSOperationQueue但更复杂也更强大,并且可以嵌套使用。所以说,结合block实现的GCD,把函数闭包(Closure)的特性发挥得淋漓尽致。

 

dispatch队列的生成可以有这几种方式:

1. dispatch_queue_t queue = dispatch_queue_create("com.dispatch.serial"DISPATCH_QUEUE_SERIAL); //生成一个串行队列,队列中的block按照先进先出(FIFO)的顺序去执行,实际上为单线程执行。第一个参数是队列的名称,在调试程序时会非常有用,所有尽量不要重名了。

2. dispatch_queue_t queue = dispatch_queue_create("com.dispatch.concurrent"DISPATCH_QUEUE_CONCURRENT); //生成一个并发执行队列,block被分发到多个线程去执行

3. dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //获得程序进程缺省产生的并发队列,可设定优先级来选择高、中、低三个优先级队列。由于是系统默认生成的,所以无法调用dispatch_resume()和dispatch_suspend()来控制执行继续或中断。需要注意的是,三个队列不代表三个线程,可能会有更多的线程。并发队列可以根据实际情况来自动产生合理的线程数,也可理解为dispatch队列实现了一个线程池的管理,对于程序逻辑是透明的。

官网文档解释说共有三个并发队列,但实际还有一个更低优先级的队列,设置优先级为DISPATCH_QUEUE_PRIORITY_BACKGROUND。Xcode调试时可以观察到正在使用的各个dispatch队列。

4. dispatch_queue_t queue = dispatch_get_main_queue(); //获得主线程的dispatch队列,实际是一个串行队列。同样无法控制主线程dispatch队列的执行继续或中断。

接下来我们可以使用dispatch_async或dispatch_sync函数来加载需要运行的block。

dispatch_async(queue, ^{

  //block具体代码

}); //异步执行block,函数立即返回

dispatch_sync(queue, ^{

  //block具体代码

}); //同步执行block,函数不返回,一直等到block执行完毕。编译器会根据实际情况优化代码,所以有时候你会发现block其实还在当前线程上执行,并没用产生新线程。

实际编程经验告诉我们,尽可能避免使用dispatch_sync,嵌套使用时还容易引起程序死锁。

如果queue1是一个串行队列的话,这段代码立即产生死锁:

   dispatch_sync(queue1, ^{

      dispatch_sync(queue1, ^{

    ......

  });

  ......

 });

不妨思考下,为什么下面代码在主线程中执行会死锁:

dispatch_sync(dispatch_get_main_queue(), ^{

  ......

}); 

 

那实际运用中,一般可以用dispatch这样来写,常见的网络请求数据多线程执行模型:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

  //子线程中开始网络请求数据

  //更新数据模型

  dispatch_sync(dispatch_get_main_queue(), ^{

    //在主线程中更新UI代码

  });

});

程序的后台运行和UI更新代码紧凑,代码逻辑一目了然。

 

dispatch队列是线程安全的,可以利用串行队列实现锁的功能。比如多线程写同一数据库,需要保持写入的顺序和每次写入的完整性,简单地利用串行队列即可实现:

dispatch_queue_t queue1 = dispatch_queue_create("com.dispatch.writedb"DISPATCH_QUEUE_SERIAL);

- (void)writeDB:(NSData *)data

{

  dispatch_async(queue1, ^{

    //write database

  });

} 

下一次调用writeDB:必须等到上次调用完成后才能进行,保证writeDB:方法是线程安全的。 

 

dispatch队列还实现其它一些常用函数,包括:

void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t)); //重复执行block,需要注意的是这个方法是同步返回,也就是说等到所有block执行完毕才返回,如需异步返回则嵌套在dispatch_async中来使用。多个block的运行是否并发或串行执行也依赖queue的是否并发或串行。

void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block); //这个函数可以设置同步执行的block,它会等到在它加入队列之前的block执行完毕后,才开始执行。在它之后加入队列的block,则等到这个block执行完毕后才开始执行。

void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block); //同上,除了它是同步返回函数

void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block); //延迟执行block

最后再来看看dispatch队列的一个很有特色的函数:

void dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t queue);

它会把需要执行的任务对象指定到不同的队列中去处理,这个任务对象可以是dispatch队列,也可以是dispatch源(以后博文会介绍)。而且这个过程可以是动态的,可以实现队列的动态调度管理等等。比如说有两个队列dispatchA和dispatchB,这时把dispatchA指派到dispatchB:

dispatch_set_target_queue(dispatchA, dispatchB);

那么dispatchA上还未运行的block会在dispatchB上运行。这时如果暂停dispatchA运行:

dispatch_suspend(dispatchA);

则只会暂停dispatchA上原来的block的执行,dispatchB的block则不受影响。而如果暂停dispatchB的运行,则会暂停dispatchA的运行。

这里只简单举个例子,说明dispatch队列运行的灵活性,在实际应用中你会逐步发掘出它的潜力。

dispatch队列不支持cancel(取消),没有实现dispatch_cancel()函数,不像NSOperationQueue,不得不说这是个小小的缺憾。 


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表