天天看点

深入学习中央调度(GCD)--第二部分

更新说明:查阅我们基于iOS8.0和Swift下中央调度( http://www.raywenderlich.com/79150/grand-central-dispatch-tutorial-swift-part-2 )教程这块的更新版本。

欢迎来到第二部分,也是深入学习GCD系列教程的最后部分。

第一部分中,你了解了很多自己之前不曾想象过的并发、线程以及GCD的工作原理。使用dispatch_once使单例线程安全及通过组合使用dispatch_barrier_async和dispatch_sync使读写图片数据线程安全。

除此之外,通过使用dispatch_after延时提示已增强用户体验以及从视图初始化中卸载部分工作然后异步地去执行CPU密集型任务。

如果你一直跟下来了,你可以很轻松地从第一部分例子工程处继续学习。如果你还没有完成或者不想再重复使用第一部分下载好的工程。

是时候去探索更多的GCD知识了。

纠正过早弹出的菜单

你可能已经注意到了,当你选择Le-Internet选项添加图片时,在图片下载好之前一个UIAlertView已经弹出了,像线面这样:

深入学习中央调度(GCD)--第二部分

用下面的代码去替换PhotoManager中的欺骗式downloadPhotoWithCompletionBlock: -  ( void )downloadPhotosWithCompletionBlock : (BatchPhotoDownloadingCompletionBlock )completionBlock

{

    __block  NSError  *error;

    for  (NSInteger i  =  0; i <  3; i ++ )  {

        NSURL  *url;

        switch  (i )  {

            case  0 :

                url  =  [ NSURL URLWithString :kOverlyAttachedGirlfriendURLString ];

                break;

            case  1 :

                url  =  [ NSURL URLWithString :kSuccessKidURLString ];

                break;

            case  2 :

                url  =  [ NSURL URLWithString :kLotsOfFacesURLString ];

                break;

            default :

                break;

        }

        Photo  *photo  =  [ [Photo alloc ] initwithURL :url

                              withCompletionBlock :^ (UIImage  *image,  NSError  *_error )  {

                                  if  (_error )  {

                                      error  = _error;

                                  }

                              } ];

        [ [PhotoManager sharedManager ] addPhoto :photo ];

    }

    if  (completionBlock )  {

        completionBlock (error );

    } } 这里你在方法的末尾调用完成回调了,你假定所有的图片都下载完成了。但不幸的是,截止到这个点无法保证所有的图片都下载完成。

照片类实例化方法开始从URL下载文件然后在下载完成之前立即返回了。换句话说,在结尾处downloadPhotoWithCompletionBlock调用自身完成回调,认为自身代码都是同步线性执行及每个方法调用都完成了自己的工作。

然而,[Photo initWIthURL:withCompletionBlock]是异步的,立即返回,因此这种方式不会(正常)工作。

相反的是,downloadPhotoCompletionBlock应该在所有的图片下载任务回调自身完成回调之后再去调用自己的完成回调。问题是这样的:你如何监听并发的异步事件?你不知道他们何时完成及可能按任何顺序完成。或许你可以写一些变态的代码(使用复杂BOOL值) 去跟踪下载状态,但是那样不利于扩展,看起来也很丑陋。

幸运的是,这类复杂的异步完成监听正好是dispatch groups的设计。

调度组

当整个一个任务组完成时,调度组发出通知。这些任务可以是异步的或同步的,甚至可以跟踪不同队列中的任务。当所有组中的事件完成的时候,调度组以同步或异步的方式发出通知。因为不同队列中元素可以被跟踪,所以一个dispatch_group_t实例是可以跟踪在这些队列中的不同任务的。

GCDapi提供了两种方式发出通知当组中的所有事件完成的时候。

第一种,dispatch_group_wait,是一种阻塞当前线程的方式知道所有在组中任务完成或者一个超时发生。这正是在这种情况下你想要的。(所有图片下载完成再回调)

打开PhotoManager.m然后替换下载完成回调用下面实现: -  ( void )downloadPhotosWithCompletionBlock : (BatchPhotoDownloadingCompletionBlock )completionBlock

{

    dispatch_async (dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_HIGH,  0 ),  ^ {  // 1

        __block  NSError  *error;

        dispatch_group_t downloadGroup  = dispatch_group_create ( );  // 2

        for  (NSInteger i  =  0; i <  3; i ++ )  {

            NSURL  *url;

            switch  (i )  {

                case  0 :

                    url  =  [ NSURL URLWithString :kOverlyAttachedGirlfriendURLString ];

                    break;

                case  1 :

                    url  =  [ NSURL URLWithString :kSuccessKidURLString ];

                    break;

                case  2 :

                    url  =  [ NSURL URLWithString :kLotsOfFacesURLString ];

                    break;

                default :

                    break;

            }

            dispatch_group_enter (downloadGroup );  // 3

            Photo  *photo  =  [ [Photo alloc ] initwithURL :url

                                  withCompletionBlock :^ (UIImage  *image,  NSError  *_error )  {

                                      if  (_error )  {

                                          error  = _error;

                                      }

                                      dispatch_group_leave (downloadGroup );  // 4

                                  } ];

            [ [PhotoManager sharedManager ] addPhoto :photo ];

        }

        dispatch_group_wait (downloadGroup, DISPATCH_TIME_FOREVER );  // 5

        dispatch_async (dispatch_get_main_queue ( ),  ^ {  // 6

            if  (completionBlock )  {  // 7

                completionBlock (error );

            }

        } );

    } ); } 按注释中的编号顺序看,你将得出以下结论: 1、因为你使用同步的dispatch_group_wait会阻塞当前线程,所以要使用异步调用方法去确保不阻塞主线程 2、这样创建了一个新的调度组,其行为有点像一个为完成的任务的计数器 3、dispatch_group_enter手动通知任务开始。你必须平衡dispatch_group_enter调用次数和dispatch_group_leave调用次数,否则会有怪异的崩溃。 4、这里手动发出通知待工作完成的时候。同时,平衡了group进入和group离开的数目。 5、dispatch_group_wait等待所有的工作完成或时间到期。如果在所有的事件完成之前时间到期,这个方法将会返回一个非0得结果。你可以把它放进一个条件块中去检查是否等待超时,不过,在这种情况下你要为它提供DISPATCH_TIME_FOREVER参数来指定永远等待。这意味着,不出意料,它将永远等待。这是不错的,因为图片的创建将总是能完成。 6、这是,你可以保证所有的图片任务完成或者超时。你可以生成在主线程设置一个完成回调去执行你的完成回调。这样在主线程添加的任务将在稍后执行。 7、最后,检查完成回调是否为空,如果不空,执行回调。 生成并运行app,尝试去下载多个图片,然后观察下在完成回调地方app的行为。 注:如果网络活动发生太快以至于无法分辨何时完成回调及你正在设备上运行app, 你可以切换一些关于开发人员选项的网络设置使之工作。跳到网络连接选项(开发者--NETWORK LINK CONDITIONER),开启它,选择一个描述选项,“Very Bad Network”是个不错的选择。如果你正在模拟器下运行,可以使用network link conditioner from GitHub 去改变网络速度。这是一个不错的要在你“武器库”中的工具,因为它可以强制使你注意到在连接速度不佳的时候aoo回发生什么。

这种解决方案到目前为止是好的,但一般情况下要尽量避免阻塞线程,如果可能的话。下一步你的任务就是重写相同的方法保证在所有的下载完成之后异步的发送通知。

在我们转向另一种调度组的使用前,先简要介绍下何时使用调度组在各种队列类型下: 1、自定义串行队列:这是不错的选择,在一组任务完成的时候发送通知。 2、主队列(串行):这种情况下也是不错的选择。在主队列中你应该谨慎的使用,如果你正在同步等待所有任务完成的时候,因为你不能阻塞主线程。但,一旦长时间运行的任务完成如网络调用,异步模式是一种很有吸引力的方式去更新UI。 3、并发队列:调度组同样是不错的选择去发送完成通知。

调度组,第二部分

这也是很好的,但有点笨拙的是在其他队列上异步调度,然后使用dispatch_group_wait。有另外一种方式。。。

找到PhotoManager中的下载回调,用下面的实现替换: -  ( void )downloadPhotosWithCompletionBlock : (BatchPhotoDownloadingCompletionBlock )completionBlock

{

    // 1

    __block  NSError  *error;

    dispatch_group_t downloadGroup  = dispatch_group_create ( ); 

    for  (NSInteger i  =  0; i <  3; i ++ )  {

        NSURL  *url;

        switch  (i )  {

            case  0 :

                url  =  [ NSURL URLWithString :kOverlyAttachedGirlfriendURLString ];

                break;

            case  1 :

                url  =  [ NSURL URLWithString :kSuccessKidURLString ];

                break;

            case  2 :

                url  =  [ NSURL URLWithString :kLotsOfFacesURLString ];

                break;

            default :

                break;

        }

        dispatch_group_enter (downloadGroup );  // 2

        Photo  *photo  =  [ [Photo alloc ] initwithURL :url

                              withCompletionBlock :^ (UIImage  *image,  NSError  *_error )  {

                                  if  (_error )  {

                                      error  = _error;

                                  }

                                  dispatch_group_leave (downloadGroup );  // 3

                              } ];

        [ [PhotoManager sharedManager ] addPhoto :photo ];

    }

    dispatch_group_notify (downloadGroup, dispatch_get_main_queue ( ),  ^ {  // 4

        if  (completionBlock )  {

            completionBlock (error );

        }

    } ); } 下面是新的异步方式工作原理: 1、新的实现中,无需嵌套异步调用因为没有阻塞主线程 2、相同的enter方法,没有任何改变 3、相同的leave方法,同样没有任何改变 4、dispatch_group_notify作为异步完成块。这里代码在调度组中没有元素剩余时执行,然后完成回调执行。你仍然指定了那个队列执行完成回调代码,这里主队列就是。 这种方法以更干净的方式处理特定的工作,没有阻塞任何线程。

太多并发的危险

所有这些新工具任由你使用,你可能认为线程可以处理一切,对吗?

深入学习中央调度(GCD)--第二部分

再看下downloadPhotoWithCompltionBlcok。你或许注意到有一个for循环处理了3个独立的图片。你的工作就是看看如何执行并发地执行这个循环并加速它。

这是dispatch_apply的工作。

dispatch_apply像一个for循环并发地执行不同的迭代。这个方法是同步的,因此看起来像一个正常的for循环,dispatch_apply仅在所有的工作都完成的时候返回。

必须小心弄明白在块内一个指定的工作量的最佳迭代量,因为每个迭代工作量很小在迭代量大的时候依然可以产生很大的开销,这对我们并发调用的收益是不利的。

何时适合使用dispatch_apply? 1、自定义串行队列:串行队列将完全忽略dispatch_apply的使用,工作起来就像普通的for循环 2、主队列(串行):跟上面一样,在串行队列中使用时很糟糕的主意。仅是一个普通的循环 3、并发队列:并发循环是个不错的选择,特别是需要跟踪任务执行进度的时候。

回到downloadPhotoWithCompletionBlock使用下面的代码替换: -  ( void )downloadPhotosWithCompletionBlock : (BatchPhotoDownloadingCompletionBlock )completionBlock

{

    __block  NSError  *error;

    dispatch_group_t downloadGroup  = dispatch_group_create ( );

    dispatch_apply ( 3, dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_HIGH,  0 ),  ^ ( size_t i )  {

        NSURL  *url;

        switch  (i )  {

            case  0 :

                url  =  [ NSURL URLWithString :kOverlyAttachedGirlfriendURLString ];

                break;

            case  1 :

                url  =  [ NSURL URLWithString :kSuccessKidURLString ];

                break;

            case  2 :

                url  =  [ NSURL URLWithString :kLotsOfFacesURLString ];

                break;

            default :

                break;

        }

        dispatch_group_enter (downloadGroup );

        Photo  *photo  =  [ [Photo alloc ] initwithURL :url

                              withCompletionBlock :^ (UIImage  *image,  NSError  *_error )  {

                                  if  (_error )  {

                                      error  = _error;

                                  }

                                  dispatch_group_leave (downloadGroup );

                              } ];

        [ [PhotoManager sharedManager ] addPhoto :photo ];

    } );

    dispatch_group_notify (downloadGroup, dispatch_get_main_queue ( ),  ^ {

        if  (completionBlock )  {

            completionBlock (error );

        }

    } ); } 你的循环现在并发的执行,在上文中,dispatch_apply的调用中你提供的第一个迭代参数是枚举的次数,第二个是任务执行的队列,第三个block行为。

要明白,虽然你已经以线程安全的方式地添加图片了,但图片完成的顺序可能不同,依赖哪个线程先完成。

生成运行,从Le Internet添加一些图片。注意有什么不同?

事实上,这是不值得在这种情况下。下面是为什么? 1、你可能已经在线程并发上占用了很大的系统开销,而不仅仅是在第一次运行循环的地方。你应该用非常大的合时的步长来使用idspatch_apply迭代。 2、创建一个app的时间是有限制的,不要浪费时间去优化你不知道好坏的代码。如果你要去优化一些东西,待优化的东西对你来说是值得关注的及时间是值得。通过使用Instruments去找出执行时间最长的方法优化app。查看更多的关于 How to Use Instruments in Xcode。 3、通常情况下,优化代码会使你的代码更复杂对自己来说,其他开发者可能会追在你后面(打)。确信添加的复杂的东西在效益上是值得的。 记住,不要疯狂优化代码。这样可能会使它更难懂(对自己来说)和其他开发者(不得不挨着遍历你的代码)。

复杂GCD的乐趣

等等,还有更多。下面是一些不常用的额外的方法。虽然不会频繁的使用,但在特定的情况下会很有帮助。

阻塞-合适的方式

这听起来也许是一个疯狂的想法,但是你知道Xcode有测试功能吗?我知道,有时装作它不在那儿,但当构建复杂关系的代码是,编写测试并测试它是很重要的。

在Xcode中测试就是子类化XCTestCase并运行测试点开始测试。测试在主线程测定,因此你可以假定每个测试点以串行的方式执行。

一旦给定的测试方法完成,XCTest会认为一个测试点完成然后继续下一个测试。这就意味着任何来自前面测试点的代码都将继续执行当下一个测试运行的时候。

网络代码通常是异步的,因此当执行网络读取时不要阻塞主线程。再加上测试方法完成的时候测试结束的事实,使网络代码更难测试。那样的话,除非你阻塞主线程知道网络代码完成。

注:有些人会说这类测试不是集成测试优先考虑的测试类型。有些人同意,有些人不同意。如果它适合你,就这样做。

深入学习中央调度(GCD)--第二部分

跳到GooglyPuffTest.m,替换downloadImageURLWithString:

-  ( void )downloadImageURLWithString : ( NSString  * )URLString

{

    NSURL  *url  =  [ NSURL URLWithString :URLString ];

    __block  BOOL isFinishedDownloading  =  NO;

    __unused Photo  *photo  =  [ [Photo alloc ]

                             initwithURL :url

                             withCompletionBlock :^ (UIImage  *image,  NSError  *error )  {

                                  if  (error )  {

                                     XCTFail ( @ "%@ failed. %@", URLString, error );

                                  }

                                 isFinishedDownloading  =  YES;

                              } ];

    while  ( !isFinishedDownloading )  { } }

这是一个朴素的测试异步网络代码的方式。这个在方法末尾的循环等待直到这个isFinishedDownloading变量为真(在block完成回调中发生)。我们来看下这有什么影响。

运行你的测试通过点Pruduct/Test或者通过快捷键command+u。

当测试运行的时候,注意下CPU的使用情况通过Xcode的debug。这个不佳的设计就是基本的“自旋锁”。这里它不实用,因为浪费了CPU循环等待,扩展性也不好。

也许需要使用网络连接调节器(手机设置中或者github上开源项目),就像之前解释的一样,为了更好的看到问题。如果网络太快自旋发生在一瞬间。

你需要一个更优雅、可扩展的解决方案去阻塞线程知道资源可用。输入信号量。

信号量

信号量是一个古老的线程概念,被谦卑的程序员Edsger W. Dijkstra引入。信号量是一个复杂的主题因为他们基于错综复杂的操作系统函数。

如果你像了解更多关于信号量的只是查阅   link which discusses semaphore theory in more detail . 如果你是学术类型的,一个经典的软件系统就是哲学家就餐的问题。

信号量让你可以控制多个使用者访问有限的资源。例如:如果你创建一个信号量被两个资源池使用,顶多只有两个线程可以同时访问临界区。其他想要使用资源的必须等待。。。你猜到了?FIFO队列。

让我们看下信号量。

打开GooglyPuffTest.m然后替换downloadImageURLWithString用下面的实现: -  ( void )downloadImageURLWithString : ( NSString  * )URLString

{

    // 1

    dispatch_semaphore_t semaphore  = dispatch_semaphore_create ( 0 );

    NSURL  *url  =  [ NSURL URLWithString :URLString ];

    __unused Photo  *photo  =  [ [Photo alloc ]

                             initwithURL :url

                             withCompletionBlock :^ (UIImage  *image,  NSError  *error )  {

                                  if  (error )  {

                                     XCTFail ( @ "%@ failed. %@", URLString, error );

                                  }

                                  // 2

                                 dispatch_semaphore_signal (semaphore );

                              } ];

    // 3

    dispatch_time_t timeoutTime  = dispatch_time (DISPATCH_TIME_NOW, kDefaultTimeoutLengthInNanoSeconds );

    if  (dispatch_semaphore_wait (semaphore, timeoutTime ) )  {

        XCTFail ( @ "%@ timed out", URLString );

    }

} 下面是上面的信号量是如何工作的? 1、创建信号量。参数的值代表信号量的初始值。这个数字是第一次不执行增加就能访问信号量的数目。(注意:增加信号量被称为信号它) 2、在完成回调中你告诉信号量你不在需要资源。增加信号量数目然后就可以被其他资源访问。 3、在具有给定超时的信号量上等待。当前线程的回调直到信号量被signal。一个非0得返回意味着超时了。这种情况下,测试是失败的因为它认为网络响应不应超过10S。

重新运行测试。只要网络连接正常,测试应当及时返回成功。特别注意下CPU的使用,比较之前的自旋锁实现。

禁用网络连接在测试,如果在真机运行的话,打开飞行模式。如果运行在模拟器关闭你的连接。测试返回一个失败的结果,在10S之后。太好了,成功了。

这是相当琐碎的测试,但如果你是服务器开发人员,这些基本的测试可以防止互相指责(谁来承担最近的这个网络问题)。

使用调度源

一个特别有趣的功能是调度来源,它是基本的底层抓包功能可以帮助响应或者监听Unix信号,文件描述符,Mach端口,VFS节点和其他晦涩的东西。所有这些都超出了本教程的范围,但可以小试一下实现一个调度源对象及以一个独特的方式使用它。

第一次使用调度源的用户可能会迷失在如何使用一个源上,因此你需要做的第一件事情就是理解dispatch_source_create如何工作。这是创建一个源的方法: dispatch_source_t dispatch_source_create (

   dispatch_source_type_t type,

   uintptr_t handle,

    unsigned  long mask,    dispatch_queue_t queue ); 第一个参数是dispatch_source_type_t。这是一个很重要的参数,它表明了句柄和任务参数是什么。你需要去查阅Xcode文档 Xcode documentation 看看什么选项是可用的对该参数来说。

这里你会检测DISPATCH_SOURCE_TYPE_SIGNAL。下面是文档   documentation shows : 说明: 调度源就是监听当前进程的信号,句柄是一个信号数(整型)。mask未使用,暂时传零。在signal.h中可以找到一个Unix信号列表,开始有一群#define。从信号列表中 ,你将监测一个SIGSTOP信号。这个信号被发送当一个进程接受到不可避免的暂停指令时。当你使用LLDB调试器调试你的应用时,相同的信号被发送。

到PhotoCollectionViewController.m,在viewDidLoad的开始添加下面的代码: // 1

  #if DEBUG

      // 2

      dispatch_queue_t queue  = dispatch_get_main_queue ( );

      // 3

      static dispatch_source_t source  =  nil;

      // 4

      __typeof (self ) __weak weakSelf  = self;

      // 5

      static dispatch_once_t onceToken;

      dispatch_once ( &onceToken,  ^ {

          // 6

          source  = dispatch_source_create (DISPATCH_SOURCE_TYPE_SIGNAL, SIGSTOP,  0, queue );

          // 7

          if  (source )

          {

              // 8

              dispatch_source_set_event_handler (source,  ^ {

                  // 9

                  NSLog ( @ "Hi, I am: %@", weakSelf );

              } );

              dispatch_resume (source );  // 10

          }

      } );   #endif 代码有点复杂,因此跟着注释一步一步来看看会发生什么: 1、最好在DEBUG模式下编译你的代码,因为这样可以更深入你的应用程序有趣的部分 2、仅仅为了方便而已,创建一个diaptch_queue_t的实例变量而不是直接用方法作为参数。当代码变长时有时拆分可以更好提高可读性。 3、源需要超出函数作用于的生存周期,所以用了一个静态变量 4、通过使用weakSelf来确保不会产生循环引用。这不是特别必须的,因为爱视图控制器跟app生命周期一样。然后,如果有其他类消失了,这将保证他们u循环引用。 5、这里使用久经考验的dispatch_once来对调度源进行一次设置 6、这里是你实例化的调度源。表明你对信号监听感兴趣并提供SIGSTOP作为第二参数。此外,你使用主队列去处理接受事件,你将发现很短暂。 7、如果提供的参数格式不正确,调度源对象将不会被创建。因此,在继续工作前,需要确保你有一个有效的调度源对象。 8、dispatch_source_set_event_handler将会调用当你接收到你监听的信号时。你可以设置相应的处理逻辑处理在block块内 9、这是一个基本的NSLog调用,将打印对象到控制台 10、所有的调度源默认开始状态都是挂起。你必须告诉源对象去唤醒执行当你想要开始监听事件的时候 生成运行app,暂停调试然后立即唤醒app。查看控制台,你将看到深色部分的函数确实工作了,你应该看到的像调试器中一样。 (类似下面的输出:

2014-03-29 17:41:30.610 GooglyPuff[8181:60b] Hi, I am:       

You app is now debugging-aware! That’s pretty awesome, but how would you use this in real life?

) 你可以用它调试一个对象,然后显示数据当你唤醒app的时候。你也可以给app一些自定义的安全逻辑去保护用户数据当恶意的攻击者依附调试器攻击你的应用时。

一个有趣的注意是用这个合适的方法去作为一个堆栈跟踪工具去找出在调试器中你想操作的对象。

深入学习中央调度(GCD)--第二部分

考虑下另外的解决方案。当你暂停调试器,你总是得不到想要的堆栈结构。现在你可以在任何时候停止调试器,然后让代码执行到你想要的位置。这是非常有用的,如果你想执行到app中的某个代码点上,不过访问调试器是冗长乏味的,尝试一下吧。

深入学习中央调度(GCD)--第二部分

在NSLog处加一个断点,暂停调试,然后再开始。app将停在断点处。你现在在深入地访问该视图控制器的方法,现在你可以访问视图控制器实例的核心内容。很帅,有么有。 注:如果你注意到在调试器中的一些线程,多看一下他们。主线程将总是libdisapatch第一个线程,GCD协调器是第二个线程。除此外,线程数和保留的线程取决于app断点时硬件的运行情况。

在调试器中输入: po  [[weakSelf navigationItem] setPrompt:@"WOOT!"] 然后继续运行app,你将会看到下面的:

深入学习中央调度(GCD)--第二部分
深入学习中央调度(GCD)--第二部分

在这方法中你可以更新UI,请求类属性,甚至执行方法-所有的都将不得不重启app进入特定的工作状态。

下一步学习什么?

你可以download the final project here.

我不想再强调主题的重要性了,但你确实用该去查阅下How to Use Instruments 。你肯定会需要这个如果你计划去优化你的app的话。关注下Instruments,确实是一个关联执行的不错的工具,哪些区域的代码执行起来比其他区域更耗时间。如果你想查看具体某个方法的执行时间的话,就需要自己想办法解决了。

查看下How to Use NSOperations and NSOperationQueues,一个构建于GCD之上的并发管理技术。一般情况下,如果你要创建一个启动就自动导引(创建就不管)的任务,其最佳做法是使用GCD。NSOperations提供了更好的控制,最大并发数和以速度为代价的面相对象范例。

记住,除非你有特定的理由要使用低级(API),否则总是坚持使用高级的API。如果你想学习更多或者想做一些有趣的事情可以冒险尝试下苹果的“黑科技”。

祝君好运!有问题和反馈可以在下面的讨论区发表。

***<第一次做翻译工作,太难了。做的不好,欢迎各位大大们不吝批评指正。万分感谢!>***