iOS 并发:从 NSOperation 和 Dispatch Queues 开始

做者:hossam ghareeb,原文连接,原文日期:2015-12-09
译者:ray16897188;校对:Channe;定稿:千叶知风html

并发(Concurrency)在 iOS 开发中老是被看做是洪水猛兽通常。人们觉得它是一个很危险的领域,不少开发者都尽可能避免与其接触。更有传闻说你必定要竭尽所能的避免写任何关于多线程的代码。假如你对并发不是很了解却还去使用它的话,那么我赞成:并发是很危险的。只是它的危险是由于你不了解它。试想一下常人一辈子中体验过的危险运动和行为有多少,不少对吧?可是当掌握了以后,就会通通变成小菜一碟。并发是把双刃剑,你应该掌握并学会如何去使用它。它能帮你写出效率高、执行快、反应灵敏的 App,而与此同时,对它的滥用会无情的毁掉你的 App。这就是为何在开始写任何关于并发的代码以前,先要想想你为何要用到并发、须要用到哪一个(与并发有关的)API 来解决这个问题。iOS 中咱们有不少能用到的 API。在此教程里我会讲到最经常使用的两个:NSOperationDispatch Queues(派发队列)。ios

咱们为何要用并发?

我知道你是一个有 iOS 背景的出色开发者。然而不管你要作一个什么样的 App,你都须要了解并发,让你的 App 更快,更灵敏。我总结了一下学习和使用并发所带来的优势:git

  • 能更有效的利用 iOS 设备的硬件:如今全部的 iOS 设备都有一个多核处理器,可让开发者并行执行多个任务。你应该好好利用这个特性,让硬件优点发挥出来。github

  • 更好的用户体验:你可能写过一些呼叫网络服务、处理 IO,或者是其余任何执行繁重任务的代码。你也知道在 UI 所在的线程中作这些操做会把你的 App 冻结住,使它没任何反应。当用户遇到这种状况时,他/她会绝不犹豫的直接杀掉或是关闭你的 App。而用并发的话,这些繁重的任务就会被安排到后台去执行,不会占满主线程、干扰你用户的操做。他们依旧可以点击按钮,来回拖动屏幕并在你的 App 中的每一个页面之间跳转,与此同时在后台还处理着繁重的装载任务。swift

  • NSOperation 和 Dispatch Queues 这样的 API 让使用并发变得容易:建立并管理线程并非简单的事情。这也是为何不少的开发者一听到并发,还有多线程代码这样的术语时会感到惧怕的缘由。在 iOS 中咱们有强大而易用的并发 API,让你的生活变得更来福。你无需再为建立线程或管理底层的东西操心,API 会为你搞定。这些 API 的另外一个优势是它们能轻松帮你实现同步(synchronization),避免了竞态条件(race condition)的产生,而竞态条件产生在当多个线程试图访问相同的资源的时候,这会引发没法预计的后果。有了同步,你就保护了资源不会被多个线程同时访问。网络

关于并发你都须要知道些什么?

在本篇教程中,我会解释为理解并发你所须要了解的一切,消除你对它的全部恐惧心理。首先我推荐你去看一下 blocks(即 Swift 中的 closures),它们在并发中会被大量使用。以后咱们会聊一下 Dispatch QueuesNSOperationQueues。我会带你了解每一个并发中的概念,概念之间的不一样,以及如何使用它们。数据结构

第一部分:GCD(Grand Central Dispatch)

GCD 是在系统的Unix层级中用于管理并发代码并异步执行操做时最经常使用的 API。GCD 提供而且管理任务的队列(queues of tasks)。先来看看什么是队列。多线程

什么是队列

队列是按照先进先出(FIFO)顺序管理对象的数据结构。队列相似于电影院的售票窗口前的长队。电影票是按先到先得的顺序卖出。长队前面的人先买到票,晚来的人后买到票。计算机科学中的队列概念和这个很像,由于第一个被加到队列中的对象也是第一个要从队列中被移除的。
Photo credit: FreeImages.com/Sigurd Decroos闭包

Dispatch Queues

Dispatch Queues 是一种可以轻松执行异步和并发任务的方式。它们是队列,其中的任务是由你的 App 以 blocks(代码块)的形式提交。Dispatch Queues 有两种:(1)串行队列(serial queues),和(2)并发队列(concurrent queues)。在讲述二者的不一样以前,你须要知道派给这两种队列的任务是在另外的线程中被执行,而不是在建立它们的那个线程中被执行。换句话说,你是在主线程中建立block并将其提交到 Dispatch Queues 中去。但全部这些任务(block)会在其余的线程中运行,并不是主线程。并发

串行队列

当你选择建立一个串行队列时,该队列在某一时刻只能执行一个任务。该队列中的全部任务都会彼此尊重,按序执行。然而你无需担忧其余队列中的任务,意思是你依然能够经过使用多个串行队列来以并发的形式执行任务。例如你能够建立两个串行队列,每个队列某一时刻只能执行一个任务,可是仍是有最多两个任务被并发执行。

用串行队列来管理一个共享资源(shared resource)再合适不过。它提供的对共享资源的访问确保是串行化的,从而防止竞态条件的发生。想象一下有个售票小摊,还有一大堆人想买电影票,那小摊的售票员就是一个共享资源。若是这个售票员必须同时为这堆人服务时就会特别混乱。为避免这个状况,买票的人会被要求去排队(串行队列),这样售票员同一时刻就能够只对一人服务。

再说一遍,这里没有说电影院每时刻只能为一个顾客服务。若是电影院再开两个售票点,就能够同时服务三位客户了。这也是为何我说过即便你使用串行队列还依然能并行执行多个任务的缘由。

使用串行队列的优势:

  1. 能确保对一个共享资源进行串行化的访问,避免了竞态条件;

  2. 任务的执行顺序是可预知的;你向一个串行队列提交任务时,它们被执行的顺序与它们被提交的顺序相同;

  3. 你能够建立任意数量的串行队列;

并发队列

正如其名,并发队列可让你并行的执行多个任务。任务(block)按照它们被加入到队列中的顺序依次开始,可是它们都是并发的被执行,并不须要彼此等待才开始。并发队列能保证任务按同一顺序开始,但你不能知道执行的顺序、执行的时间以及在某一时刻正在被执行任务的数量。

好比你向一个并发队列提交了三个任务(任务#1,#2和#3)。任务被并发执行,按照加入队列的顺序依次开始。然而任务的执行时间和结束时间都不相同。即便任务#2和#3可能会迟一些开始,它们可能都会先于任务#1结束。对任务的执行是由系统自己决定。

使用队列

已经解释了串行队列和并发队列,如今来看看如何使用它们。系统会缺省为每一个应用提供一个串行队列和四个并发队列。其中 main dispatch queue(主派发队列)是全局可用的串行队列,在应用的主线程中执行任务。这个队列被用来更新 App 的 UI,执行全部与更新 UIViews 相关的任务。该队列中同一时刻只执行一个任务,这就是为何当你在主队列中运行一个繁重的任务时UI会被阻塞的缘由。

除主队列以外,系统还提供了4个并发队列。咱们管它们叫 Global Dispatch queues(全局派发队列)。这些队列对整个应用来讲是全局可用的,彼此只有优先级高低的区别。要使用其中一个全局并发队列的话,你得使用 dispatch_get_global_queue 函数得到一个你想要的队列的引用,该函数的第一个参数取以下值:

  • DISPATCH_QUEUE_PRIORITY_HIGH

  • DISPATCH_QUEUE_PRIORITY_DEFAULT

  • DISPATCH_QUEUE_PRIORITY_LOW

  • DISPATCH_QUEUE_PRIORITY_BACKGROUND

这些队列类型表明着执行优先级。带有 HIGH 的队列有最高优先级,BACKGROUND 则是最低的优先级。这样你就能基于任务的优先级来决定要用哪个队列。还要注意这些队列也被 Apple 的 API 所使用,因此这些队列中并不仅有你本身的任务。

最后,你能够建立任何数量的串行或并发队列。使用并发队列的状况下,即便你能够本身建立,我仍是强烈建议你使用上面那四个全局队列。

GCD 小抄

如今你应该有了一个对 Dispatch Queues 的基本了解。我会给你一个简单的小抄作参考。里面很简单,包含了对 GCD 你须要了解的全部信息。

还不错吧?如今咱们来研究一个简单的示范,看看如何使用 Dispatch Queues。我会告诉你如何使用 Dispatch Queues 来优化 App 的性能,让它有更快的响应速度。

示例项目

咱们的初始项目很简单,它展现4个 image views,每一个 image view 显示一张来自远端站点的图片。图片的请求是在主线程中完成。为了给你展现这么作对UI响应会有何影响,我还在图片下面加了一个简单的 slider。下载并运行这个初始项目。点击 Start 按钮开始图片的下载,而后在图片下载的过程当中拖动 slider,你会发现根本就拖不动。


你点了 Start 按钮以后,图片就会在主线程中开始下载。显然这种方式糟糕至极,让 UI 没法响应。不幸的是时至今日还有很对的 App 依旧在主线程中执行繁重的装载任务。如今咱们使用 Dispatch Queues 来解决这个问题。

首先咱们使用并发队列的解决方案,随后再使用串行队列的解决方案。

使用 Concurrent Dispatch Queues

如今回到 Xcode 项目的 ViewController.swift 文件中。若是你细看一下代码,就会看到点击事件的方法 didClickOnStart。这个方法负责处理图片的下载。咱们如今是这样来完成该任务的:

@IBAction func didClickOnStart(sender: AnyObject) {
    let img1 = Downloader.downloadImageWithURL(imageURLs[0])
    self.imageView1.image = img1
    
    let img2 = Downloader.downloadImageWithURL(imageURLs[1])
    self.imageView2.image = img2
    
    let img3 = Downloader.downloadImageWithURL(imageURLs[2])
    self.imageView3.image = img3
    
    let img4 = Downloader.downloadImageWithURL(imageURLs[3])
    self.imageView4.image = img4
    
}

每个 downloader 都被看做是一个任务,而全部的任务都在主队列中被执行。如今咱们来得到一个全局并发队列的引用,该队列是默认优先级的那个。

let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
        dispatch_async(queue) { () -> Void in
            
            let img1 = Downloader.downloadImageWithURL(imageURLs[0])
            dispatch_async(dispatch_get_main_queue(), {
                
                self.imageView1.image = img1
            })
            
        }

先用 dispatch_get_global_queue 得到到默认并发队列的引用,而后在 block 中提交一个任务,下载第一张图片。当图片下载完成后,咱们再向主队列提交另一个任务,这个任务用拿下载好了的图片去更新 image view。换句话说,咱们就是将图片下载任务放到了后台线程中执行,而 UI 相关的任务则是在主线程中执行。

对剩下的图片作一样改动,代码以下:

@IBAction func didClickOnStart(sender: AnyObject) {
    
    let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
    dispatch_async(queue) { () -> Void in
        
        let img1 = Downloader.downloadImageWithURL(imageURLs[0])
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView1.image = img1
        })
        
    }
    dispatch_async(queue) { () -> Void in
        
        let img2 = Downloader.downloadImageWithURL(imageURLs[1])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView2.image = img2
        })
        
    }
    dispatch_async(queue) { () -> Void in
        
        let img3 = Downloader.downloadImageWithURL(imageURLs[2])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView3.image = img3
        })
        
    }
    dispatch_async(queue) { () -> Void in
        
        let img4 = Downloader.downloadImageWithURL(imageURLs[3])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView4.image = img4
        })
    }
    
}

你向默认队列以并发任务的形式提交了四个图片的下载任务。如今建立项目而后运行 App ,运行起来应该更快了(若是你收到任何错误告警,在把你的代码和上面的比较一下)。注意到下载图片的过程当中你应该能够拖动那个 slider,没有任何延迟。

使用 Serial Dispatch Queues

另外一种解决延迟问题的方法是使用串行队列。如今仍是回到 ViewController.swift 中的 didClickOnStart() 方法。这回咱们用一个串行队列来下载图片。使用串行队列时你必定要留意你所引用的究竟是哪个串行队列。每个 App 都有一个默认的串行队列,实际上它也是UI任务相关的主队列。因此切记用串行队列的时候,你必定要建立一个新的,不然就会在 App 尝试执行更新UI相关任务的同时又执行你的任务。这就会产生错误,引发延时,毁掉用户体验。你可使用 dispatch_queue_create 函数建立一个新的队列,而后将全部任务按相同方式提交给它,和咱们以前作的同样。更改以后,代码以下:

@IBAction func didClickOnStart(sender: AnyObject) {
    
    let serialQueue = dispatch_queue_create("com.appcoda.imagesQueue", DISPATCH_QUEUE_SERIAL)
    
    
    dispatch_async(serialQueue) { () -> Void in
        
        let img1 = Downloader .downloadImageWithURL(imageURLs[0])
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView1.image = img1
        })
        
    }
    dispatch_async(serialQueue) { () -> Void in
        
        let img2 = Downloader.downloadImageWithURL(imageURLs[1])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView2.image = img2
        })
        
    }
    dispatch_async(serialQueue) { () -> Void in
        
        let img3 = Downloader.downloadImageWithURL(imageURLs[2])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView3.image = img3
        })
        
    }
    dispatch_async(serialQueue) { () -> Void in
        
        let img4 = Downloader.downloadImageWithURL(imageURLs[3])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView4.image = img4
        })
    }
    
}

正如咱们所见,与并行队列解决方案惟一的不一样就是须要建立一个串行队列。再次点击 build 而后运行 App ,你又会看见图片在后台进行下载,因此能够和UI进行交互。

可是你会注意到两点:

  1. 与使用并发队列的状况相比,下载图片的时间有些长。缘由是咱们在同一时刻只下载一张图片。每一个任务必须等到前一个任务执行完成后才会被执行。

  2. 图片的下载是按照 image1,image2,image3,和 image4 的顺序。由于使用的是每次只能执行一个任务的串行队列。

第二部分:Operation Queues

GCD 是一个底层的 C API,能让开发者并行执行任务。与之相对比,Operation queues 是对队列模型的高层级抽象,并且是基于GCD建立的。这意味着你能够像GCD那样执行并发任务,只不过是以一种面性对象的风格。简而言之,Operation Queues 让开发者的“来福”更进一步。

与 GCD 不一样的是,Operation Queues 不遵循先进先出的顺序。如下是 Operation Queues 和 Dispatch Queues 的不一样:

  1. 不遵循 FIFO(先进先出):在 Operation Queues 中,你能够设置 operation(操做)的执行优先级,而且能够在 operation 之间添加依赖,这意味着你能够定义某些 operation,使得它们能够在另一些 operation 执行完毕以后再被执行。这就是为何它们不遵循先进先出的顺序。

  2. 默认状况下 Operation Queues 是并发执行:虽然你不能将其改为串行队列,但仍是有一种方法,经过在 operation 之间添加相依性来让 Operation Queues 中的任务按序执行。

  3. Operation Queues 是 NSOperationQueue 类的实例,任务被封装在 NSOperation 的实例中。

NSOperation

任务是以 NSOperation 实例的形式被提交到 Operation Queues 中去的。以前说过 GCD 中任务是以 block 的形式被提交。在这里也是相似,只不过是须要被绑到 NSOperation 实例中。你能够简单的将 NSOperation 看做是一套工做任务的总体。

NSOperation 是一个抽象的类,不能够被直接拿来用,因此你只能使用 NSOperation 的子类。在 iOS 的 SDK 中有两个 NSOperation 的具体子类。这两个类能够直接用,可是你也能够用 NSOperation 的子类,建立你本身的类来完成特定 operation。这两个能够直接使用的类是:

  1. NSBlockOperation - 用这个类来初始化包含一个或多个 blocks 的 operation。该 operation 自己可包含的 block 超过一个,当全部的block 执行完毕后这个 operation 就被视为已完成。

  2. NSInvocationOperation  - 用这个类来初始化一个 operation,能用来调用某指定对象的选择器(selector)。

那么 NSOperation 的优点在哪里?

  1. 首先它能够经过 NSOperation 类的 addDependency(op: NSOperation)方法得到对相依性的支持。若是你有这样的需求:即某 operation 的启动需取决于另外一个 operation 的执行,那么就得用 NSOperation

  2. 其次,你可将 queuePriority 属性设为如下值来改变执行优先级:

    public enum NSOperationQueuePriority : Int {

    case VeryLow
       case Low
       case Normal
       case High
       case VeryHigh

    }
    拥有最高优先级的 operation 会被第一个执行。

  3. 你能够取消掉某特定队列中的某个 operation,或者是取消队列中全部的 operation。
    经过调用 NSOperation 类的 cancel() 方法来实现对 operation 的取消。你取消任何 operation 的时候,会是下面三种场景之一:

    • 你的 operation 已经完成了,这种状况下 cancel 方法没有任何效果。

    • 你的 operation 正在被执行的过程当中,这种状况下系统不会强制中止你的 operation 代码,而是将 cancelled 属性置为 true。

    • 你的 operation 还在队列中等待被执行,这种状况下你的 operation 就不会被执行。

  4. NSOperation 有3个有用的布尔型属性:finishedcancelledreadyfinished 在 operation 执行完毕后被置为 true。cancelled 在 operation 被取消后被置为 true。ready 在 operation 即将被执行时被置为 true。

  5. 全部的 NSOperation 在任务被完成后均可以选择去设置一段 completion block。NSOperationfinished 属性变为 true 后这段 block 就会被执行。

如今来重写一下咱们的示例项目,此次使用 NSOperationQueues。首先在 ViewController 类中声明以下变量:

var queue = NSOperationQueue()

而后将 didClickOnStart 方法中的代码替换成下面的,再看看在 NSOperationQueue 中怎样去执行 operation:

@IBAction func didClickOnStart(sender: AnyObject) {
    queue = NSOperationQueue()
 
    queue.addOperationWithBlock { () -> Void in
        
        let img1 = Downloader.downloadImageWithURL(imageURLs[0])
 
        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView1.image = img1
        })
    }
    
    queue.addOperationWithBlock { () -> Void in
        let img2 = Downloader.downloadImageWithURL(imageURLs[1])
        
        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView2.image = img2
        })
 
    }
    
    queue.addOperationWithBlock { () -> Void in
        let img3 = Downloader.downloadImageWithURL(imageURLs[2])
        
        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView3.image = img3
        })
 
    }
    
    queue.addOperationWithBlock { () -> Void in
        let img4 = Downloader.downloadImageWithURL(imageURLs[3])
        
        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView4.image = img4
        })
 
    }
}

如上所见,你使用了 addOperationWithBlock 方法来建立一个新的、带有某给定 block(或者它在 Swift 中的名字:闭包)的 operation。很简单,对吧?为在主队列中完成某个任务,与使用 GCD 时调用 dispatch_async() 不一样,咱们用 NSOperationQueue(NSOperationQueue.mainQueue())也能够达到相同结果,将你想要在主队列中执行的 operation 提交过去。

能够运行一下这个 App 作个快速测试。若是代码输入正确的话, App 应该可以在后台下载图片,不会阻塞UI。

以前的例子中咱们使用了 addOperationWithBlock 方法把 operation 添加到队列中。再让咱们来看看如何使用 NSBlockOperation:在达到相同的效果的同时,还会给咱们提供更多的功能和选项,好比设置 completion handler。改后的 didClickOnStart 方法以下:

@IBAction func didClickOnStart(sender: AnyObject) {
    
    queue = NSOperationQueue()
    let operation1 = NSBlockOperation(block: {
        let img1 = Downloader.downloadImageWithURL(imageURLs[0])
        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView1.image = img1
        })
    })
    
    operation1.completionBlock = {
        print("Operation 1 completed")
    }
    queue.addOperation(operation1)
    
    let operation2 = NSBlockOperation(block: {
        let img2 = Downloader.downloadImageWithURL(imageURLs[1])
        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView2.image = img2
        })
    })
    
    operation2.completionBlock = {
        print("Operation 2 completed")
    }
    queue.addOperation(operation2)
    
    
    let operation3 = NSBlockOperation(block: {
        let img3 = Downloader.downloadImageWithURL(imageURLs[2])
        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView3.image = img3
        })
    })
    
    operation3.completionBlock = {
        print("Operation 3 completed")
    }
    queue.addOperation(operation3)
    
    let operation4 = NSBlockOperation(block: {
        let img4 = Downloader.downloadImageWithURL(imageURLs[3])
        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView4.image = img4
        })
    })
    
    operation4.completionBlock = {
        print("Operation 4 completed")
    }
    queue.addOperation(operation4)
}

对每一个 operation,咱们都为其建立了一个新的 NSBlockOperation 实例并将任务封装在一个 block 中。而使用了 NSBlockOperation,你还能够设置 completion handler。当 operation 完成后,completion handler 就会被调用。将示例运行一下,就会在控制台看见这样的输出:

Operation 1 completed
Operation 3 completed
Operation 2 completed
Operation 4 completed

取消 operation

以前提过,NSBlockOperation 可以让你管理 operation。那么如今来看看如何取消一个 operation。首先给 navigation bar 加一个 bar button item,将其命名为 Cancel。为展现取消 operation,咱们在 Operation #2 和 Operation #1 之间添加一个相依性,Operation #3 和 Operation #2 之间添加另外一个相依性。也就是说 Operation #2 会在 Operation #1 完成后开始执行,而 Operation #3 会在 Operation #2 完成后执行。Operation #4 并无相依性,它会被并发执行。要取消 operation 的话,你只需调用 NSOperationQueue 的 cancelAllOperations() 方法。在ViewController 类中插入下面的方法:

@IBAction func didClickOnCancel(sender: AnyObject) {
        self.queue.cancelAllOperations()
}

记住须要把你在 navigation bar 上添加的 Cancel 按钮与 didClickOnCancel 方法关联起来。你能够这么作:返回 Main.storyboard 文件,打开Connections Inspector,这里你会在Received Actions区域中看见unlink didSelectCancel()。点击 + 并将其从空圆圈拖拽到 Cancel bar button 上。而后在 didClickOnStart 方法中添加相依性:

operation2.addDependency(operation1)
operation3.addDependency(operation2)

接下来把 operation #1 的 completion block 改一下,让它在控制台打印出 cancel 的状态:

operation1.completionBlock = {
    print("Operation 1 completed, cancelled:\(operation1.cancelled) ")
}

你能够本身改一下 operation #2,#3 和 #4 的打印语句,这样能够更好的理解这一过程。而后建立并运行项目。你点击了 Start 按钮以后,再按 Cancel 按钮,这就会在 operation #1 执行完毕后取消全部的 operation。下面告诉了咱们都发生了些什么:

  • 因为 operation #1 已经开始执行,取消对它没有任何效果。这就是为何 cancelled 会被记录成 false,而且 App 仍是会显示第一张图片。

  • 若是你点击 Cancel 按钮足够快的话,operation #2 会被取消。对 cancelAllOperations() 的调用会中止对该 operation 的执行,因此第二张图片没有被下载。

  • operation #3 已经排在队列中,等待 operation #2 的完成。由于 operation #3 是否开始取决于 operation #2 的完成与否,而 operation #2 已经被取消,operation #3 就不会被执行,从队列中被当即踢出了。

  • 没有对 operation #4 作任何相依性的设置,因此它被并发的执行了,下载了第四张图片。

接下来看什么?

本篇教程中我为你讲解了 iOS 中并发的概念,以及你在 iOS 中该如何去使用它。我给了你一个还不错的并发入门简介,解释了 GCD,并示范了怎样去建立串行和并发队列。除此以外,咱们还看了一下 NSOperationQueues。你如今应该对 GCD 和 NSOperationQueues 之间的不一样有所了解了。

若是想进一步了解 iOS 的并发,建议你去看一下 Apple 的并发指南

你能够从 iOS Concurrency repository on Github 这里找到此教程提到的全套源代码以做参考。

随便问任何问题,我真心喜欢你的评论。

本文由 SwiftGG 翻译组翻译,已经得到做者翻译受权,最新文章请访问 http://swift.gg

相关文章
相关标签/搜索