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

一直以來兴猩,并發(fā)都被視為 iOS 開發(fā)中的「洪水猛獸」。許多開發(fā)者都將其視為危險(xiǎn)地帶早歇,唯恐避之而不及倾芝。更有謠傳認(rèn)為,多線程代碼應(yīng)該盡力避免箭跳。筆者同意蛀醉,如果你對(duì)并發(fā)的了解不夠深入,就容易造成危險(xiǎn)衅码。但是拯刁,危險(xiǎn)往往是因?yàn)闊o知。想想吧逝段,在人們的日常生活中垛玻,會(huì)經(jīng)歷多少危險(xiǎn)的行為或活動(dòng)?但是奶躯,一旦掌握其要領(lǐng)帚桩,也就是一碟小菜罷了。

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

ios-concurrency-featured

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

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

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

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

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

What do You Need to Know about Concurrency?

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

本文將會(huì)解釋理解并發(fā)所需的全部知識(shí)爆惧,徹底消除你對(duì)它的恐懼狸页。首先,我們建議你了解一下塊(blocks)(Swift 中的閉包),因?yàn)樗鼈冊(cè)诓l(fā) API 中廣泛使用芍耘。之后址遇,我們會(huì)探討調(diào)度隊(duì)列與 NSOperations。我們會(huì)詳細(xì)介紹這些并發(fā)概念斋竞,它們的區(qū)別以及實(shí)現(xiàn)方法倔约。

第一部分: GCD (Grand Central Dispatch)

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

什么是隊(duì)列绢要?

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

queue-line-2-1166050-1280x960

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

調(diào)度隊(duì)列

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

串行隊(duì)列

如果你選擇創(chuàng)建串行隊(duì)列,該隊(duì)列每次只能執(zhí)行一個(gè)任務(wù)圈匆。同一個(gè)串行隊(duì)列中的所有任務(wù)都會(huì)相互尊重漠另,依次執(zhí)行。然而跃赚,它們不會(huì)在意其他獨(dú)立隊(duì)列中的任務(wù)笆搓。這意味著,如果使用了多個(gè)串行隊(duì)列,仍有可能并發(fā)地執(zhí)行任務(wù)满败。例如肤频,你可以創(chuàng)建兩個(gè)串行隊(duì)列,每個(gè)隊(duì)列每次都只會(huì)執(zhí)行一個(gè)任務(wù)算墨,但是仍有可能出現(xiàn)兩個(gè)任務(wù)同時(shí)執(zhí)行的情況宵荒。

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

不過,需要重申的是鸿脓,這并不意味著戲院只能一次服務(wù)一名顧客抑钟。如果戲院開設(shè)兩個(gè)以上的售票窗口,就能同時(shí)服務(wù)三名顧客野哭。也即在塔,使用多個(gè)串行隊(duì)列,就能并行處理多項(xiàng)任務(wù)拨黔。

使用串行隊(duì)列的好處如下:

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

并發(fā)隊(duì)列

顧名思義狠持,并發(fā)隊(duì)列允許你并行執(zhí)行多個(gè)任務(wù)。任務(wù)開始執(zhí)行的次序遵照其加入隊(duì)列的次序瞻润。但是,任務(wù)執(zhí)行的過程都同步進(jìn)行,不需要等待绍撞。并發(fā)隊(duì)列保證任務(wù)開始執(zhí)行的次序是確定的正勒,但是你無法知道執(zhí)行的次序,執(zhí)行時(shí)長或在任意時(shí)間點(diǎn)同步執(zhí)行的任務(wù)個(gè)數(shù)傻铣。

比如章贞,你向某個(gè)并發(fā)隊(duì)列提交了三個(gè)任務(wù)(任務(wù)1、2非洲、3號(hào))鸭限。這些任務(wù)會(huì)并發(fā)執(zhí)行,開始執(zhí)行的次序依照他們加入隊(duì)列的次序两踏。然而败京,它們的執(zhí)行時(shí)長與完成時(shí)間并不一致。盡管任務(wù)2梦染、3開始執(zhí)行的時(shí)間比任務(wù)1晚赡麦,但它們?nèi)杂锌赡茉谌蝿?wù)1之前完成執(zhí)行。最終帕识,由系統(tǒng)決定任務(wù)執(zhí)行的情況泛粹。

使用隊(duì)列

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

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

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

最后胰坟,你可以創(chuàng)建任意數(shù)量的串行隊(duì)列或并發(fā)隊(duì)列因篇。當(dāng)用到并發(fā)隊(duì)列時(shí),筆者強(qiáng)烈建議你使用這四個(gè)全局隊(duì)列笔横。當(dāng)然竞滓,你也可以自己創(chuàng)建并發(fā)隊(duì)列。

GCD 備忘錄

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

gcd-cheatsheet

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

演示項(xiàng)目

我們的啟動(dòng)項(xiàng)目非常簡單涣易,主要展示四個(gè)圖片視圖,每個(gè)視圖都需要從一個(gè)遠(yuǎn)程站點(diǎn)獲取圖片冶伞。圖片請(qǐng)求會(huì)在主線程中完成新症。為了展示這個(gè)過程對(duì) UI 響應(yīng)性能的影響,筆者在圖片下面添加了一個(gè)簡單的滑動(dòng)條∠烨荩現(xiàn)在徒爹,下載并運(yùn)行該啟動(dòng)項(xiàng)目。點(diǎn)擊 Start 按鈕開始下載圖片芋类,在此過程中拖動(dòng)滑塊隆嗅。你會(huì)發(fā)現(xiàn),根本無法拖動(dòng)它侯繁。

concurrency-demo

一旦點(diǎn)擊了 Start 按鈕胖喳,圖片就會(huì)在主線程中開始下載。顯然贮竟,這種方法非常糟糕丽焊,會(huì)導(dǎo)致 UI 停止響應(yīng)较剃。不幸的是,直到今天粹懒,仍有許多應(yīng)用在主線程中執(zhí)行這類繁重的任務(wù)重付。下面,我們將使用調(diào)度隊(duì)列解決這一問題凫乖。

首先,我們會(huì)用并發(fā)隊(duì)列實(shí)現(xiàn)解決方案弓颈。之后帽芽,使用串行隊(duì)列再此實(shí)現(xiàn)解決方案。

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

現(xiàn)在翔冀,回到 Xcode 項(xiàng)目中的 ViewController.swift 文件导街。如果查看代碼,你會(huì)發(fā)現(xiàn)一名為 didClickOnStart 的動(dòng)作方法纤子。該方法會(huì)處理圖片的下載搬瑰,其實(shí)現(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
    
}

每個(gè) downloader 都會(huì)被視作一個(gè)任務(wù),所有的任務(wù)都在主隊(duì)列中執(zhí)行】嘏穑現(xiàn)在泽论,換一種實(shí)現(xiàn)方式。首先卡乾,獲取一個(gè)默認(rèn)優(yōu)先級(jí)的全局并發(fā)隊(duì)列的引用翼悴。

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ā)隊(duì)列的引用幔妨。之后鹦赎,在代碼塊內(nèi)部,提交下載第一張圖片的任務(wù)误堡。圖片下載完成之后古话,向主線程提交另一個(gè)任務(wù),用下載好的圖片更新圖片視圖锁施。換句話說陪踩,我們將圖片下載任務(wù)放到后臺(tái)線程中進(jìn)行,但是在主線程中執(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)隊(duì)列后膊毁,構(gòu)造并運(yùn)行應(yīng)用,運(yùn)行速度應(yīng)該會(huì)明顯改善(如果報(bào)出代碼錯(cuò)誤基跑,請(qǐng)仔細(xì)對(duì)照你的代碼與上面的代碼)婚温。此外,在下載圖片的同時(shí)媳否,滑動(dòng)條應(yīng)該也可以順利拖動(dòng)栅螟,沒有任何延遲荆秦。

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

解決延遲問題的另一種辦法就是使用串行隊(duì)列。現(xiàn)在力图,回到 ViewController.swift 文件的 didClickOnStart() 方法步绸。這一次,我們會(huì)使用串行隊(duì)列下載圖片吃媒。不過瓤介,在使用串行隊(duì)列時(shí),你必須加倍注意自己引用的是哪一個(gè)串行隊(duì)列赘那。每個(gè)應(yīng)用都有一個(gè)默認(rèn)的串行隊(duì)列刑桑,該隊(duì)列其實(shí)是用于 UI 加載的主隊(duì)列。因此募舟,在使用串行隊(duì)列時(shí)祠斧,你必須創(chuàng)建一個(gè)新隊(duì)列,否則拱礁,在執(zhí)行自身任務(wù)的同時(shí)琢锋,應(yīng)用也會(huì)試圖執(zhí)行更新 UI 的任務(wù)。這會(huì)導(dǎo)致錯(cuò)誤與延遲呢灶,進(jìn)而損害用戶體驗(yàn)吴超。你可以使用 dispatch_queue_create 方法創(chuàng)建一個(gè)新的隊(duì)列,并將所有任務(wù)提交給這個(gè)隊(duì)列烛芬,方法與之前介紹的相同宏蛉。完成這些改動(dò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ā)隊(duì)列案例的唯一不同是串行隊(duì)列的創(chuàng)建峰尝。當(dāng)你再次創(chuàng)建并運(yùn)行應(yīng)用時(shí)劳淆,會(huì)發(fā)現(xiàn)圖片下載過程還是在后臺(tái)運(yùn)行缆八,因此 UI 交互不受影響奖恰。

不過蛹屿,你會(huì)注意到兩點(diǎn):

  1. 與并發(fā)隊(duì)列的案例相比勇边,圖片下載時(shí)間有所延長怀浆。原因是每次只下載一張圖片函筋。每個(gè)任務(wù)只有在前一個(gè)任務(wù)完成之后谨敛,才開始執(zhí)行藐俺。
  2. 圖片依次加載颈娜,分別為圖片1浙宜,圖片2官辽,圖片3,圖片4梆奈。原因是串行隊(duì)列每次只執(zhí)行一個(gè)任務(wù)野崇。

第二部分:操作隊(duì)列

我們知道,GCD 是允許開發(fā)者并發(fā)地執(zhí)行任務(wù)的底級(jí)別 C API亩钟。然而乓梨,操作隊(duì)列是隊(duì)列模型的高級(jí)抽象,基于 GCD 建立清酥。這意味著扶镀,你可以像 GCD 那樣并發(fā)地執(zhí)行任務(wù),卻是以面向?qū)ο蟮姆绞窖媲帷:喍灾艟酰僮麝?duì)列進(jìn)一步簡化了開發(fā)者的工作。

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

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

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

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

NSOperation

NSOperation

如前所述弃舒,任務(wù)以 NSOperation 實(shí)例的形式提交給操作隊(duì)列癞埠。而在 GCD 的討論中,我們說過任務(wù)以塊為單位進(jìn)行提交聋呢。此處也一樣燕差,不過任務(wù)必須捆綁為 NSOperation 實(shí)例。你可以簡單地將 NSOperation 視為一個(gè)工作單元坝冕。

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

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

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

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

NSOperation Illustration

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

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)先級(jí)高的操作會(huì)首先執(zhí)行证逻。

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

  • 若操作已經(jīng)結(jié)束豪治,cancel 方法就無法起效洞拨。
  • 若操作正在被執(zhí)行,系統(tǒng)不會(huì)強(qiáng)制停止操作代碼负拟。但是烦衣,cancelled(已取消)屬性會(huì)設(shè)置為真。
  • 若操作還在隊(duì)列中等待執(zhí)行掩浙,該操作就不會(huì)被執(zhí)行花吟。

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

5.一旦任務(wù)完成造虎,任何 NSOperation 都可以將完成塊設(shè)置為 called(已經(jīng)調(diào)用)傅蹂。一旦 NSOperation 中的 finished 屬性設(shè)置為真,塊就會(huì)變?yōu)?called算凿。

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

var queue = NSOperationQueue()

之后搞乏,用下面的代碼替代 didClickOnStart 方法。請(qǐng)查看我們是如何在 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)建新的操作赃阀。其實(shí)非常簡單析恢,不是么?在主隊(duì)列中執(zhí)行任務(wù),我們可以用 NSOperationQueue (NSOperationQueue.mainQueue())提交想在主隊(duì)列中執(zhí)行的任務(wù)仇箱,而不是像使用 GCD 時(shí)那樣調(diào)用 dispatch_async 方法捕儒。

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

在前面的例子里佳镜,我們借助 addOperationWithBlock 方法往隊(duì)列中添加操作×鸥澹現(xiàn)在,讓我們使用 NSBlockOperation 進(jìn)行同樣的操作蟀伸,與此同時(shí)蚀同,提供更多的功能與選擇,比如設(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)
}

針對(duì)每一個(gè)操作,我們都創(chuàng)建一個(gè)新的 NSBlockOperation 實(shí)例用于將任務(wù)封裝為塊迟蜜。借助 NSBlockOperation刹孔,你還可以設(shè)置完成處理程序。現(xiàn)在娜睛,操作執(zhí)行完成之后芦疏,特定的完成處理程序就會(huì)被調(diào)用。此處微姊,為了簡便起見,我們只是在日志中記錄一則簡單的消息分预,提示操作已經(jīng)完成兢交。如果你運(yùn)行演示項(xiàng)目,會(huì)在控制臺(tái)看到如下信息:

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

Canceling Operations

取消操作

如前所述笼痹,NSBlockOperation 允許你管理操作∨湓現(xiàn)在,讓我們來學(xué)習(xí)如何取消一個(gè)操作凳干。為此晴裹,首先要在導(dǎo)航欄添加一個(gè)名為 Cancel(取消)的按鈕。為了演示取消操作救赐,我們將在操作2與操作1涧团,以及操作3與操作2之間分別添加一個(gè)依賴關(guān)系。也即,操作2會(huì)在操作1完成之后開始執(zhí)行泌绣,而操作3會(huì)在操作2完成之后開始執(zhí)行钮追。操作4不存在依賴關(guān)系,會(huì)并發(fā)執(zhí)行阿迈。要想取消這些操作元媚,你只需調(diào)用 NSOperationQueue 的 cancelAllOperations() 方法即可。下面苗沧,在 ViewController 類中插入下面的方法:

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

請(qǐng)記住刊棕,你需要把添加到導(dǎo)航欄的 Cancel 按鈕與 didClickOnCancel 方法相連接。為此待逞,你可以回到 Main.storyboard 文件甥角,打開連接檢查器(Connections Inspector)。之后飒焦,你會(huì)看到 Received Actions 一節(jié)下的分開 didSelectCancel() 方法蜈膨。點(diǎn)擊并從空?qǐng)A拖拽到 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的日志記錄語句,從而更深入地理解此過程¤厩現(xiàn)在驰凛,建造并允許應(yīng)用。點(diǎn)擊Start 按鈕之后担扑,點(diǎn)擊 Cancel 按鈕恰响。這樣,操作1完成后所有操作都會(huì)被取消涌献,以下是運(yùn)行結(jié)果:

  • 由于操作1已經(jīng)被執(zhí)行了胚宦,取消無法起效。因此燕垃,cancelled 的值在日志中記為假枢劝,應(yīng)用仍會(huì)展示圖片1。
  • 如果點(diǎn)擊 Cancel 按鈕的速度足夠快卜壕,操作2會(huì)被取消您旁。cancelAllOperations() 的調(diào)用會(huì)中斷操作2的執(zhí)行,因此圖片2下載失敗轴捎。
  • 操作3已經(jīng)在隊(duì)列中鹤盒,等待操作2執(zhí)行完成蚕脏。因?yàn)樗蕾囉诓僮?的完成才能繼續(xù)執(zhí)行。但由于操作2被取消了昨悼,操作3也不會(huì)得到執(zhí)行蝗锥,而是立即從隊(duì)列中移除。
  • 操作4并未設(shè)置任何依賴關(guān)系率触。因此终议,它會(huì)并發(fā)執(zhí)行,成功下載圖片4葱蝗。
ios-concurrency-cancel-demo

Where to go from here?

下一步該做什么穴张?

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

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

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市帚屉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌漾峡,老刑警劉巖攻旦,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異生逸,居然都是意外死亡牢屋,警方通過查閱死者的電腦和手機(jī)且预,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來烙无,“玉大人锋谐,你說我怎么就攤上這事〗乜幔” “怎么了涮拗?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長迂苛。 經(jīng)常有香客問我三热,道長,這世上最難降的妖魔是什么三幻? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任就漾,我火速辦了婚禮,結(jié)果婚禮上念搬,老公的妹妹穿的比我還像新娘抑堡。我一直安慰自己,他們只是感情好朗徊,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布首妖。 她就那樣靜靜地躺著,像睡著了一般荣倾。 火紅的嫁衣襯著肌膚如雪悯搔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天舌仍,我揣著相機(jī)與錄音妒貌,去河邊找鬼。 笑死铸豁,一個(gè)胖子當(dāng)著我的面吹牛灌曙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播节芥,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼在刺,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了头镊?” 一聲冷哼從身側(cè)響起蚣驼,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎相艇,沒想到半個(gè)月后颖杏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡坛芽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年留储,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了翼抠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡获讳,死狀恐怖阴颖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情丐膝,我是刑警寧澤量愧,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站尤误,受9級(jí)特大地震影響侠畔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜损晤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一软棺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧尤勋,春花似錦喘落、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至暖哨,卻和暖如春赌朋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背篇裁。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國打工沛慢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人达布。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓团甲,卻偏偏與公主長得像,于是被迫代替她去往敵國和親黍聂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子躺苦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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