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

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

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

ios-concurrency-featured

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

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

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

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

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

What do You Need to Know about Concurrency?

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

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

第一部分: GCD (Grand Central Dispatch)

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

什么是隊列?

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

queue-line-2-1166050-1280x960

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

調(diào)度隊列

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

串行隊列

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

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

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

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

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

并發(fā)隊列

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

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

使用隊列

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

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

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

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

GCD 備忘錄

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

gcd-cheatsheet

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

演示項目

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

concurrency-demo

一旦點擊了 Start 按鈕,圖片就會在主線程中開始下載。顯然,這種方法非常糟糕,會導(dǎo)致 UI 停止響應(yīng)。不幸的是,直到今天,仍有許多應(yīng)用在主線程中執(zhí)行這類繁重的任務(wù)。下面,我們將使用調(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 都會被視作一個任務(wù),所有的任務(wù)都在主隊列中執(zhí)行?,F(xiàn)在,換一種實現(xiàn)方式。首先,獲取一個默認(rè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 方法獲得默認(rèn)并發(fā)隊列的引用。之后,在代碼塊內(nèi)部,提交下載第一張圖片的任務(wù)。圖片下載完成之后,向主線程提交另一個任務(wù),用下載好的圖片更新圖片視圖。換句話說,我們將圖片下載任務(wù)放到后臺線程中進行,但是在主線程中執(zhí)行與 UI 相關(guān)的任務(wù)。

@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ā)任務(wù)提交給默認(rèn)隊列后,構(gòu)造并運行應(yīng)用,運行速度應(yīng)該會明顯改善(如果報出代碼錯誤,請仔細(xì)對照你的代碼與上面的代碼)。此外,在下載圖片的同時,滑動條應(yīng)該也可以順利拖動,沒有任何延遲。

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

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

@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)建。當(dāng)你再次創(chuàng)建并運行應(yīng)用時,會發(fā)現(xiàn)圖片下載過程還是在后臺運行,因此 UI 交互不受影響。

不過,你會注意到兩點:

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

第二部分:操作隊列

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

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

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

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

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

NSOperation

NSOperation

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

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

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

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

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

NSOperation Illustration

2.其次,將 queuePriority 屬性的值設(shè)置為下列值中的某一個,你可以改變操作執(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() 方法即可。當(dāng)你選擇取消操作時,可能發(fā)生的場景如下:

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

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

5.一旦任務(wù)完成,任何 NSOperation 都可以將完成塊設(shè)置為 called(已經(jīng)調(diào)用)。一旦 NSOperation 中的 finished 屬性設(shè)置為真,塊就會變?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í)行任務(wù),我們可以用 NSOperationQueue (NSOperationQueue.mainQueue())提交想在主隊列中執(zhí)行的任務(wù),而不是像使用 GCD 時那樣調(diào)用 dispatch_async 方法。

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

在前面的例子里,我們借助 addOperationWithBlock 方法往隊列中添加操作?,F(xiàn)在,讓我們使用 NSBlockOperation 進行同樣的操作,與此同時,提供更多的功能與選擇,比如設(shè)置完成處理程序。這一次,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 實例用于將任務(wù)封裝為塊。借助 NSBlockOperation,你還可以設(shè)置完成處理程序?,F(xiàn)在,操作執(zhí)行完成之后,特定的完成處理程序就會被調(diào)用。此處,為了簡便起見,我們只是在日志中記錄一則簡單的消息,提示操作已經(jīng)完成。如果你運行演示項目,會在控制臺看到如下信息:

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

Canceling Operations

取消操作

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

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

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

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

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

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

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

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

Where to go from here?

下一步該做什么?

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

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

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容