iOS 并發(fā):NSOperation 與調(diào)度隊列入門(1)

一直以來,并發(fā)都被視為 iOS 開發(fā)中的「洪水猛獸」。許多開發(fā)者都將其視為危險地帶,唯恐避之而不及。更有謠傳認為,多線程代碼應該盡力避免。筆者同意,如果你對并發(fā)的了解不夠深入,就容易造成危險。但是,危險往往是因為無知。想想吧,在人們的日常生活中,會經(jīng)歷多少危險的行為或活動?但是,一旦掌握其要領,也就是一碟小菜罷了。

并發(fā)就是一柄值得你學習使用并熟練掌握的雙刃劍。它能幫助你打造高效、迅捷、響應及時的應用。于此同時,一旦誤用,也會毫不留情地毀掉應用。因此,在開始編寫并發(fā)代碼之前,好好想想你為什么需要并發(fā),你需要哪個 API 來解決問題?在 iOS 開發(fā)中,可用的 API 有很多。在本教程中,我們將探討最常用的兩個 API——NSOperation 以及調(diào)度隊列。

ios-concurrency-featured

為什么需要并發(fā)?

假設你是有經(jīng)驗的 iOS 開發(fā)老手,不論你要創(chuàng)建什么樣的應用,你都需要并發(fā)來提高應用的響應度與速度。以下是筆者總結(jié)的學習或使用并發(fā)能夠帶來的好處:

  • 利用 iOS 設備的硬件:現(xiàn)在,所有的 iOS 設備配備多核處理器,允許開發(fā)者并行執(zhí)行多個任務。你應該通過此功能好好利用這些硬件。

  • 更好的用戶體驗:你很可能編寫了調(diào)用 Web 服務,處理 IO,或執(zhí)行一些繁重任務的代碼。你也知道,在 UI 線程執(zhí)行這些操作會凍結(jié)應用,使其無法響應用戶的行為。一旦用戶遭遇這類情況,他們的第一反應往往是結(jié)束應用。有了并發(fā)機制,這些任務都可以在背景線程中執(zhí)行,而無需暫停主線程或煩擾到用戶。用戶可以點擊應用中的按鈕,滾動瀏覽或跳轉(zhuǎn)目錄,與此同時,那些繁重的加載任務則放到后臺處理。

  • NSOperation 與調(diào)度隊列這類 API 簡化了并發(fā)的使用:創(chuàng)建并管理線程并非易事。這也是大多數(shù)開發(fā)者一聽到并發(fā)、多線程代碼這類術(shù)語就大驚失色的原因。iOS 其實提供了許多易于使用的并發(fā) API,能大大簡化開發(fā)者的工作。你不必擔心創(chuàng)建線程或管理底層的部件,這些 API 會幫你搞定一切。使用這些 API 的另一個好處在于:它們能幫你輕易實現(xiàn)同步化,從而避免了競爭狀態(tài)。當多個線程視圖讀取共享資源時,就會形成競爭狀態(tài),導致意想不到的結(jié)果。使用同步機制,就能防止資源在多個線程間的共享。

What do You Need to Know about Concurrency?

關于并發(fā),你需要了解哪些內(nèi)容?

本文將會解釋理解并發(fā)所需的全部知識,徹底消除你對它的恐懼。首先,我們建議你了解一下塊(blocks)(Swift 中的閉包),因為它們在并發(fā) API 中廣泛使用。之后,我們會探討調(diào)度隊列與 NSOperations。我們會詳細介紹這些并發(fā)概念,它們的區(qū)別以及實現(xiàn)方法。

第一部分: GCD (Grand Central Dispatch)

GCD 是用于在系統(tǒng) Unix 層管理并發(fā)代碼、異步執(zhí)行操作最為常用的 API。GCD 提供并管理任務隊列。首先,了解一下隊列是什么。

什么是隊列?

隊列是以先進先出(FIFO)原則管理對象的數(shù)據(jù)結(jié)構(gòu)。隊列與戲院售票窗口外的隊伍很相似。戲票是以先到先得的次序售賣的。排在隊伍前面的人會在隊伍后面的人之前得到戲票。計算機科學中的隊列也遵循似的原理:第一個添加到隊列中的對象會第一個從隊列中移除。

queue-line-2-1166050-1280x960

Photo credit: FreeImages.com/Sigurd Decroos
圖片來源:FreeImages.com/Sigurd Decroos

調(diào)度隊列

調(diào)度隊列是在應用中實現(xiàn)異步、并發(fā)地執(zhí)行任務的簡單方法。在調(diào)度隊列中,應用產(chǎn)生的任務會以塊(代碼塊)的形式提交。目前,有兩種調(diào)度隊列:1、串行隊列(Serial Queues),2、并發(fā)隊列(Concurrent Queues)。在進一步了解兩種隊列的區(qū)別之前,你需要知道:分配給這兩種隊列的任務在執(zhí)行時所處的線程與創(chuàng)建任務的線程相獨立。換句話說,你創(chuàng)建了一些代碼塊,并將其提交給主線程中的調(diào)度隊列。但是,所有的任務(也即代碼塊)會在單獨的線程(而非主線程)中執(zhí)行。

串行隊列

如果你選擇創(chuàng)建串行隊列,該隊列每次只能執(zhí)行一個任務。同一個串行隊列中的所有任務都會相互尊重,依次執(zhí)行。然而,它們不會在意其他獨立隊列中的任務。這意味著,如果使用了多個串行隊列,仍有可能并發(fā)地執(zhí)行任務。例如,你可以創(chuàng)建兩個串行隊列,每個隊列每次都只會執(zhí)行一個任務,但是仍有可能出現(xiàn)兩個任務同時執(zhí)行的情況。

在管理共享資源時,串行隊列的用處極大。它能保證對共享資源的訪問是依次進行的,從而防止出現(xiàn)競爭狀態(tài)。設想,只有一個售票窗口,但是有一群人想買戲票的場景。此處,售票窗口的職員就是共享資源。如果該職員不得不同時服務所有購票者,場面一定非常混亂。為了應對這種場景,人們被要求排成一列(串行隊列),職員才能依次服務每位購票者。

不過,需要重申的是,這并不意味著戲院只能一次服務一名顧客。如果戲院開設兩個以上的售票窗口,就能同時服務三名顧客。也即,使用多個串行隊列,就能并行處理多項任務。

使用串行隊列的好處如下:

  1. 保證依次訪問共享資源,防止出現(xiàn)競爭狀態(tài)。
  2. 任務以可預測的次序執(zhí)行。當你向串行調(diào)度隊列提交多個任務時,任務的執(zhí)行次序與其插入次序一致。
  3. 你可以創(chuàng)建任意數(shù)量的串行隊列。

并發(fā)隊列

顧名思義,并發(fā)隊列允許你并行執(zhí)行多個任務。任務開始執(zhí)行的次序遵照其加入隊列的次序。但是,任務執(zhí)行的過程都同步進行,不需要等待。并發(fā)隊列保證任務開始執(zhí)行的次序是確定的,但是你無法知道執(zhí)行的次序,執(zhí)行時長或在任意時間點同步執(zhí)行的任務個數(shù)。

比如,你向某個并發(fā)隊列提交了三個任務(任務1、2、3號)。這些任務會并發(fā)執(zhí)行,開始執(zhí)行的次序依照他們加入隊列的次序。然而,它們的執(zhí)行時長與完成時間并不一致。盡管任務2、3開始執(zhí)行的時間比任務1晚,但它們?nèi)杂锌赡茉谌蝿?之前完成執(zhí)行。最終,由系統(tǒng)決定任務執(zhí)行的情況。

使用隊列

了解了串行隊列與并發(fā)隊列的基本知識之后,現(xiàn)在來看看如何使用它們。默認情況下,系統(tǒng)為每個應用提供了一個串行隊列與四個并發(fā)隊列。主調(diào)度隊列是全局可用的串行隊列,在應用的主線程上執(zhí)行任務。該隊列用于更新應用的 UI,執(zhí)行與 UIViews 更新相關的所有任務。因此每次只能執(zhí)行一個任務,所以當你在主隊列運行繁重的任務時,UI 就會停止響應。

除了主隊列,系統(tǒng)還提供了四個并發(fā)隊列。我們稱之為 Global Dispatch(全局調(diào)度)隊列。這些隊列對應用而言是全局的,差別只在于優(yōu)先級的不同。為了使用這些隊列,你必須用 dispatch_get_global_queue 方法取得你偏好隊列的引用。該 dispatch_get_global_queue 方法的首個參數(shù)必須為下面四個值中的一個:

這些隊列類型代表了執(zhí)行的優(yōu)先次序。HIGH 隊列的優(yōu)先級最高,而 BACKGROUND 隊列的優(yōu)先級最低。你可以根據(jù)任務的優(yōu)先級決定使用何種優(yōu)先級的隊列。此外,這些隊列也會為蘋果的 API 所用,因此,你的任務并不是隊列中的所有任務。

最后,你可以創(chuàng)建任意數(shù)量的串行隊列或并發(fā)隊列。當用到并發(fā)隊列時,筆者強烈建議你使用這四個全局隊列。當然,你也可以自己創(chuàng)建并發(fā)隊列。

GCD 備忘錄

現(xiàn)在,你應該對調(diào)度隊列有了基本的理解。接下來,筆者將提供你一份簡單的 GCD 備忘錄以供參考。該備忘錄非常簡單,但是包含了有關 GCD 的林林總總,都是你用得上的知識。

gcd-cheatsheet

很贊,對吧?接下來,我們會通過一個簡單的演示程序展示如何使用調(diào)度隊列。筆者會教你如果使用調(diào)度隊列優(yōu)化應用性能,提高應用響應度。

演示項目

我們的啟動項目非常簡單,主要展示四個圖片視圖,每個視圖都需要從一個遠程站點獲取圖片。圖片請求會在主線程中完成。為了展示這個過程對 UI 響應性能的影響,筆者在圖片下面添加了一個簡單的滑動條。現(xiàn)在,下載并運行該啟動項目。點擊 Start 按鈕開始下載圖片,在此過程中拖動滑塊。你會發(fā)現(xiàn),根本無法拖動它。

concurrency-demo

一旦點擊了 Start 按鈕,圖片就會在主線程中開始下載。顯然,這種方法非常糟糕,會導致 UI 停止響應。不幸的是,直到今天,仍有許多應用在主線程中執(zhí)行這類繁重的任務。下面,我們將使用調(diào)度隊列解決這一問題。

首先,我們會用并發(fā)隊列實現(xiàn)解決方案。之后,使用串行隊列再此實現(xiàn)解決方案。

使用并發(fā)調(diào)度隊列

現(xiàn)在,回到 Xcode 項目中的 ViewController.swift 文件。如果查看代碼,你會發(fā)現(xiàn)一名為 didClickOnStart 的動作方法。該方法會處理圖片的下載,其實現(xiàn)方式如下:

@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 都會被視作一個任務,所有的任務都在主隊列中執(zhí)行。現(xiàn)在,換一種實現(xiàn)方式。首先,獲取一個默認優(yōu)先級的全局并發(fā)隊列的引用。

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 方法獲得默認并發(fā)隊列的引用。之后,在代碼塊內(nèi)部,提交下載第一張圖片的任務。圖片下載完成之后,向主線程提交另一個任務,用下載好的圖片更新圖片視圖。換句話說,我們將圖片下載任務放到后臺線程中進行,但是在主線程中執(zhí)行與 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
        })
    }
    
}

將四張圖片的下載作為并發(fā)任務提交給默認隊列后,構(gòu)造并運行應用,運行速度應該會明顯改善(如果報出代碼錯誤,請仔細對照你的代碼與上面的代碼)。此外,在下載圖片的同時,滑動條應該也可以順利拖動,沒有任何延遲。

使用串行調(diào)度隊列

解決延遲問題的另一種辦法就是使用串行隊列。現(xiàn)在,回到 ViewController.swift 文件的 didClickOnStart() 方法。這一次,我們會使用串行隊列下載圖片。不過,在使用串行隊列時,你必須加倍注意自己引用的是哪一個串行隊列。每個應用都有一個默認的串行隊列,該隊列其實是用于 UI 加載的主隊列。因此,在使用串行隊列時,你必須創(chuàng)建一個新隊列,否則,在執(zhí)行自身任務的同時,應用也會試圖執(zhí)行更新 UI 的任務。這會導致錯誤與延遲,進而損害用戶體驗。你可以使用 dispatch_queue_create 方法創(chuàng)建一個新的隊列,并將所有任務提交給這個隊列,方法與之前介紹的相同。完成這些改動之后,代碼如下:

@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
        })
    }
    
}

如你所見,此方法與并發(fā)隊列案例的唯一不同是串行隊列的創(chuàng)建。當你再次創(chuàng)建并運行應用時,會發(fā)現(xiàn)圖片下載過程還是在后臺運行,因此 UI 交互不受影響。

不過,你會注意到兩點:

  1. 與并發(fā)隊列的案例相比,圖片下載時間有所延長。原因是每次只下載一張圖片。每個任務只有在前一個任務完成之后,才開始執(zhí)行。
  2. 圖片依次加載,分別為圖片1,圖片2,圖片3,圖片4。原因是串行隊列每次只執(zhí)行一個任務。

第二部分:操作隊列

我們知道,GCD 是允許開發(fā)者并發(fā)地執(zhí)行任務的底級別 C API。然而,操作隊列是隊列模型的高級抽象,基于 GCD 建立。這意味著,你可以像 GCD 那樣并發(fā)地執(zhí)行任務,卻是以面向?qū)ο蟮姆绞健:喍灾僮麝犃羞M一步簡化了開發(fā)者的工作。

與 GCD 不同,操作隊列不循序先進先出的次序。以下是操作隊列與調(diào)度隊列的不同之處:

  1. 不遵循 FIFO 次序:在操作隊列中,你可以為操作設定執(zhí)行優(yōu)先級,并添加操作間的依賴關系。也就是說,你可以定義一些操作只在另一些操作完成之后才能被執(zhí)行。這也是他們不遵循先進先出原則的原因。

  2. 默認情況下,操作隊列并發(fā)運行:盡管不能將其類型改為串行隊列,你仍能使用操作間的依賴關系指定任務的執(zhí)行順序。

  3. 操作隊列是 NSOperationQueue 類的實例,其任務則封裝在 NSOperation 的實例中。

NSOperation

NSOperation

如前所述,任務以 NSOperation 實例的形式提交給操作隊列。而在 GCD 的討論中,我們說過任務以塊為單位進行提交。此處也一樣,不過任務必須捆綁為 NSOperation 實例。你可以簡單地將 NSOperation 視為一個工作單元。

NSOperation 是抽象類,因此無法直接使用。所以,你只能使用 NSOperation 的子類。在 iOS SDK 中,提供了兩個 NSOperation 的具體子類。這些類可以直接使用,不過,你也可以自行創(chuàng)建 NSOperation 的子類來執(zhí)行操作。我們可以直接使用的兩個類為:

  1. NSBlockOperation —— 使用此類可創(chuàng)建帶有一個或多個塊的操作。操作本身可包含多個塊,而且只有當所有塊都執(zhí)行完畢時,該操作才算完成。
  2. NSInvocationOperation —— 使用此類創(chuàng)建的操作能夠針對特定對象喚起選擇器。

So what’s the advantages of NSOperation?
那么,NSOperation 有什么好處呢?

1.首先,借由 NSOperation 類中的 addDependency(op: NSOperation) 方法,他們支持依賴關系。當你想創(chuàng)建的操作依賴于另一個操作的執(zhí)行情況時,NSOperation 就能派上用場了。

NSOperation Illustration

2.其次,將 queuePriority 屬性的值設置為下列值中的某一個,你可以改變操作執(zhí)行的優(yōu)先級。

public enum NSOperationQueuePriority : Int {
    case VeryLow
    case Low
    case Normal
    case High
    case VeryHigh
}

The operations with high priority will be executed first.

優(yōu)先級高的操作會首先執(zhí)行。

3.你可以取消任意隊列中的某個操作或所有操作。操作在添加到隊列之后仍可以取消,調(diào)用 NSOperation 類中的 cancel() 方法即可。當你選擇取消操作時,可能發(fā)生的場景如下:

  • 若操作已經(jīng)結(jié)束,cancel 方法就無法起效。
  • 若操作正在被執(zhí)行,系統(tǒng)不會強制停止操作代碼。但是,cancelled(已取消)屬性會設置為真。
  • 若操作還在隊列中等待執(zhí)行,該操作就不會被執(zhí)行。

4.NSOperation 有三個很有用的布爾值屬性,非別為finished(已完成),cancelled(已取消),和 ready(準備就緒)。一旦操作執(zhí)行完畢,finished 會設置為真。而一旦操作取消,cancelled 會設置為真。若是操作即將被執(zhí)行,則 ready 會設置為真。

5.一旦任務完成,任何 NSOperation 都可以將完成塊設置為 called(已經(jīng)調(diào)用)。一旦 NSOperation 中的 finished 屬性設置為真,塊就會變?yōu)?called。

現(xiàn)在,讓我們用 NSOperationQueues 重寫演示項目。首先,在 ViewController 類中聲明此變量:

var queue = NSOperationQueue()

之后,用下面的代碼替代 didClickOnStart 方法。請查看我們是如何在 NSOperationQueue 中執(zhí)行操作的:

@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 方法用給定的塊(或者如 Swift 中所說,閉包)創(chuàng)建新的操作。其實非常簡單,不是么?在主隊列中執(zhí)行任務,我們可以用 NSOperationQueue (NSOperationQueue.mainQueue())提交想在主隊列中執(zhí)行的任務,而不是像使用 GCD 時那樣調(diào)用 dispatch_async 方法。

現(xiàn)在,你可以運行應用,簡單測試一下。如果代碼輸入正確,應用應該在后臺下載圖片,不影響用戶交互界面。

在前面的例子里,我們借助 addOperationWithBlock 方法往隊列中添加操作。現(xiàn)在,讓我們使用 NSBlockOperation 進行同樣的操作,與此同時,提供更多的功能與選擇,比如設置完成處理程序。這一次,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)
}

針對每一個操作,我們都創(chuàng)建一個新的 NSBlockOperation 實例用于將任務封裝為塊。借助 NSBlockOperation,你還可以設置完成處理程序。現(xiàn)在,操作執(zhí)行完成之后,特定的完成處理程序就會被調(diào)用。此處,為了簡便起見,我們只是在日志中記錄一則簡單的消息,提示操作已經(jīng)完成。如果你運行演示項目,會在控制臺看到如下信息:

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

Canceling Operations

取消操作

如前所述,NSBlockOperation 允許你管理操作。現(xiàn)在,讓我們來學習如何取消一個操作。為此,首先要在導航欄添加一個名為 Cancel(取消)的按鈕。為了演示取消操作,我們將在操作2與操作1,以及操作3與操作2之間分別添加一個依賴關系。也即,操作2會在操作1完成之后開始執(zhí)行,而操作3會在操作2完成之后開始執(zhí)行。操作4不存在依賴關系,會并發(fā)執(zhí)行。要想取消這些操作,你只需調(diào)用 NSOperationQueue 的 cancelAllOperations() 方法即可。下面,在 ViewController 類中插入下面的方法:

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

請記住,你需要把添加到導航欄的 Cancel 按鈕與 didClickOnCancel 方法相連接。為此,你可以回到 Main.storyboard 文件,打開連接檢查器(Connections Inspector)。之后,你會看到 Received Actions 一節(jié)下的分開 didSelectCancel() 方法。點擊并從空圓拖拽到 Cancel 欄按鈕。之后,參照如下代碼創(chuàng)建 didClickOnStart 方法中的依賴關系:

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

之后,修改操作1的完成塊,在日志中記錄取消的狀態(tài):

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

你也可以修改操作2、3、4的日志記錄語句,從而更深入地理解此過程。現(xiàn)在,建造并允許應用。點擊Start 按鈕之后,點擊 Cancel 按鈕。這樣,操作1完成后所有操作都會被取消,以下是運行結(jié)果:

  • 由于操作1已經(jīng)被執(zhí)行了,取消無法起效。因此,cancelled 的值在日志中記為假,應用仍會展示圖片1。
  • 如果點擊 Cancel 按鈕的速度足夠快,操作2會被取消。cancelAllOperations() 的調(diào)用會中斷操作2的執(zhí)行,因此圖片2下載失敗。
  • 操作3已經(jīng)在隊列中,等待操作2執(zhí)行完成。因為它依賴于操作2的完成才能繼續(xù)執(zhí)行。但由于操作2被取消了,操作3也不會得到執(zhí)行,而是立即從隊列中移除。
  • 操作4并未設置任何依賴關系。因此,它會并發(fā)執(zhí)行,成功下載圖片4。
ios-concurrency-cancel-demo

Where to go from here?

下一步該做什么?

在本文中,筆者詳細介紹了 iOS 并發(fā)的概念以及實現(xiàn)方式。首先,筆者簡單介紹了并發(fā)的概念,闡釋了 GCD,以及創(chuàng)建串行與并發(fā)隊列的方式。進一步地,我們學習了NSOperationQueues。現(xiàn)在,你應該對 GCD 與 NSOperationQueues 的區(qū)別有清晰的了解。

若想了解有關 iOS 并發(fā)的更多知識,筆者推薦你學習蘋果的并發(fā)指南

作為參考,你可以在此處下載前文提到的完整源碼。

Please feel free to ask any questions. I love to read your comment.
歡迎提問或留下意見及建議。
OneAPM Mobile Insight 以真實用戶體驗為度量標準進行 Crash 分析,監(jiān)控網(wǎng)絡請求及網(wǎng)絡錯誤,提升用戶留存。訪問 OneAPM 官方網(wǎng)站感受更多應用性能優(yōu)化體驗,想閱讀更多技術(shù)文章,請訪問 OneAPM 官方技術(shù)博客

本文轉(zhuǎn)自 OneAPM 官方博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377

推薦閱讀更多精彩內(nèi)容