在計算機的早期,中央處理器(Central Processing Unit棠笑,簡稱CPU)的主頻(即時鐘頻率辉词, clock speed )決定單位時間可執(zhí)行任務(wù)數(shù)量。隨著技術(shù)的進步闸度,處理器的設(shè)計變得更加緊湊竭贩,散熱和其他物理因素開始限制CPU的時鐘頻率。因此莺禁,芯片(chip)制造商尋求其他途徑來提高芯片的整體性能留量,最終解決方案就是增加芯片上處理器的內(nèi)核數(shù)。通過增加內(nèi)核數(shù)量哟冬,單個芯片每秒可執(zhí)行更多指令(instruction)楼熄,而不需要增加CPU主頻、改變芯片大小柒傻。唯一的問題是如何利用額外的核心孝赫。
對于像macOS、iOS這樣的多任務(wù)操作系統(tǒng)红符,任一時刻都會有上百個程序在運行青柄。因此,需要安排不同程序在不同核心上预侯。然而致开,大部分正在運行的程序要么是系統(tǒng)守護進程( system daemon)要么是后臺應(yīng)用,只需要占用非常短的處理器時間萎馅。相反双戳,用戶當前使用的app,占用CPU主要資源糜芳。
應(yīng)用程序使用多核的傳統(tǒng)方式是創(chuàng)建多個線程(multiple thread)飒货,但線程代碼最大問題是不能很好擴展到任意數(shù)量的內(nèi)核。你不能創(chuàng)建與內(nèi)核一樣多的線程峭竣,并期望程序運行良好塘辅。系統(tǒng)當前負載和硬件共同決定當前最佳線程數(shù)量,即當前最佳線程數(shù)量會動態(tài)變化皆撩。因此扣墩,計算出當前最佳線程數(shù)量將非常具有挑戰(zhàn)性。即使你的計算是正確的,使這些線程高效運行呻惕、互不干擾也是非常具有挑戰(zhàn)性的补鼻。
macOS和iOS不依賴線程浅侨,而是采用了異步(asynchronous)設(shè)計方案來解決并發(fā)(concurrency)問題早直。異步函數(shù)已在操作系統(tǒng)中使用了很多年嘹裂,通常用于處理耗時的任務(wù),如讀取磁盤數(shù)據(jù)型酥。異步函數(shù)調(diào)用任務(wù)后立即返回山憨,不需要等待任務(wù)執(zhí)行查乒。通常弥喉,這項工作包括以下三部分:
- 獲取后臺線程。
- 在獲取到的后臺線程開始任務(wù)玛迄。
- 當任務(wù)執(zhí)行完畢時通過回調(diào)通知調(diào)用者由境。
在過去,如果沒有滿足需求的后臺線程蓖议,就需要自己編寫異步函數(shù)虏杰,創(chuàng)建、管理后臺線程勒虾。這類多線程方案如下:
- pthread:是一套通用的多線程API纺阔,由C語言編寫。適用于Unix修然、Linux笛钝、Windows等系統(tǒng),具有跨平臺愕宋、可移植優(yōu)點玻靡,但使用難度大,需開發(fā)者管理線程生命周期中贝,平常幾乎不使用囤捻。
- NSThread:由 Objective-C 語言編寫。使用簡單邻寿,更加面向?qū)ο笮粒芍苯硬僮骶€程對象。生命周期由開發(fā)者控制绣否,偶爾使用
NSThread
誊涯。
現(xiàn)在,macOS和iOS提供以下技術(shù)支持異步執(zhí)行任何任務(wù)枝秤,而不需要自己管理線程醋拧。
- Grand Central Dispatch簡稱GCD ,GCD是基于C語言開發(fā)的,其結(jié)合了語言特點丹壕、運行時庫(runtime library)庆械,可以為macOS、iOS菌赖、watchOS和tvOS系統(tǒng)上的多核設(shè)備提供系統(tǒng)級的并發(fā)支持缭乘,可以更好協(xié)調(diào)所有正在運行app的需求,并以均衡的方式分配可用資源琉用。使用GCD時應(yīng)用程序只需要定義需要執(zhí)行的任務(wù)堕绩,然后交由系統(tǒng)執(zhí)行,而不再需要創(chuàng)建線程邑时。通過讓系統(tǒng)管理線程奴紧,app獲得了原線程編程方式無法實現(xiàn)的伸縮性,開發(fā)人員也可以實現(xiàn)更簡單晶丘、更高效的編程模型黍氮。
- 操作隊列(Operation Queue)是Objective-C API,是在GCD之上實現(xiàn)了一些方便的功能浅浮,即
NSOperation
API是GCD的高級抽象沫浆。如果你在使用NSOperation
,那么你在隱式使用GCD滚秩。使用NSOperation
時只需要定義要執(zhí)行的任務(wù)专执,將其添加到NSOperationQueue
隊列即可,NSOperationQueue
負責這些任務(wù)的調(diào)度和執(zhí)行郁油。與GCD一樣本股,NSOperationQueue
負責所有的線程管理,以確保系統(tǒng)已艰、app盡可能高效的運行痊末。NSOperation
是基類,不能直接使用哩掺,需繼承后使用其子類凿叠,或使用系統(tǒng)提供的子類來執(zhí)行任務(wù)。例如NSInvocationOperation
和NSBlockOperation
嚼吞。如需了解更多盒件,可以查看Operation、OperationQueue的使用舱禽。
這一篇文章只涉及GCD的使用炒刁。
1. GCD術(shù)語
在學習GCD之前,需要學習幾個與此相關(guān)的概念誊稚。
1.1 串行Serial
任務(wù)串行執(zhí)行時翔始,一次只能執(zhí)行一個任務(wù)罗心。
在應(yīng)用程序中,術(shù)語任務(wù)(task)有廣泛的應(yīng)用城瞎,但在這篇文章中渤闷,可以把一個任務(wù)理解為一個塊(Block)。在函數(shù)中也可以使用GCD脖镀,但因塊更為簡單飒箭、直觀,所以蜒灰,更常使用塊弦蹂。
1.2 并發(fā)Concurrent
并發(fā)指task同時(simultaneously)發(fā)生,可以被同時執(zhí)行强窖,但具體是否同時執(zhí)行由GCD根據(jù)當前負載決定凸椿。
1.3 并行Parallelism
并行指多個任務(wù)同時執(zhí)行。
通過并行技術(shù)毕骡,多核設(shè)備可以同時執(zhí)行多個線程削饵;單核設(shè)備如果想要并行執(zhí)行任務(wù)岩瘦,就需要運行一個線程未巫,執(zhí)行環(huán)境切換(也稱上下文切換,context switch)启昧,運行另外一個線程或進程(process)叙凡,這通常很快發(fā)生,足以形成并行執(zhí)行的錯覺密末。如下圖所示:
比如很多人同時在候車室排隊檢票握爷,可以理解為并發(fā)。如果候車室開通多個檢票口同時檢票严里,這時一次可為多位旅客檢票新啼,可理解為并行。
并行的反義詞是串行刹碾,并發(fā)實際上是關(guān)于結(jié)構(gòu)燥撞。當使用GCD編寫代碼時,需要構(gòu)建可以同時運行的任務(wù)迷帜,不能同時運行的任務(wù)物舒。可以同時運行的任務(wù)是否并行戏锹,及并行時同時執(zhí)行幾項任務(wù)由GCD來決定冠胯。并行的任務(wù)必須并發(fā),但并發(fā)的任務(wù)并不保證得到并行锦针。
1.4 同步Synchronous
同步執(zhí)行會等待提交的task執(zhí)行完畢后荠察,再繼續(xù)執(zhí)行后面任務(wù)置蜀。我們平常調(diào)用的方法都是同步方法,如第一行調(diào)用doSomething:
方法悉盆,程序執(zhí)行第二行時盾碗,doSomething:
方法肯定執(zhí)行完畢了。同步執(zhí)行一些耗時操作時會堵塞當前線程舀瓢。調(diào)用dispatch_sync
函數(shù)并將任務(wù)提交到當前隊列(即dispatch_sync
函數(shù)所在隊列)會導(dǎo)致死鎖廷雅。
因為是同步調(diào)用,目標隊列不會復(fù)制塊(Block_copy)京髓,而只引用塊航缀。為提高性能,同步函數(shù)一般盡可能在當前線程調(diào)用塊堰怨。
1.5 異步Asynchronous
異步指調(diào)用任務(wù)后立即返回芥玉,不需要等待任務(wù)執(zhí)行。異步不會堵塞當前線程备图。
1.6 臨界區(qū)塊Critical Section
Critical section的代碼不能被同時執(zhí)行灿巧,即不同線程、進程同時訪問揽涮。通常是因為代碼操縱共享資源抠藕,如果被多線程同時訪問,會損壞數(shù)據(jù)蒋困。如對NSMutableArray
進行寫操作時盾似,不能同時進行讀取操作。
1.7 竟態(tài)條件Race Condition
在race condition條件下雪标,軟件系統(tǒng)的行為取決于任務(wù)的執(zhí)行順序零院,而任務(wù)執(zhí)行順序不受控。Race condition可能產(chǎn)生不可預(yù)知行為村刨,且通過代碼檢查時不易顯現(xiàn)出來告抄。
1.8 死鎖Dead Lock
兩個或多個任務(wù)、線程都在等待對方完成操作嵌牺。第一個任務(wù)無法完成打洼,因為它在等待第二個任務(wù)完成。第二個任務(wù)無法完成髓梅,因為它在等待第一個任務(wù)完成拟蜻。兩個任務(wù)相互等待,這就形成了死鎖枯饿。
1.9 線程安全Thread Safe
線程安全的代碼可以在多個線程或并發(fā)任務(wù)中同時執(zhí)行酝锅。非線程安全的代碼一次只能在一個環(huán)境中運行。例如奢方,NSArray
是線程安全的搔扁,可以同時在多個線程或并發(fā)任務(wù)中執(zhí)行爸舒。而NSMutableArray
不是線程安全的,一次只能在一個環(huán)境中運行稿蹲。
1.10 環(huán)境切換Context Switch
Context switch指當在同一個處理器處理不同線程時扭勉,存儲和恢復(fù)執(zhí)行狀態(tài)的過程。編寫多任務(wù)應(yīng)用程序時苛聘,進行上下文切換非常常見涂炎,但會產(chǎn)生額外的開銷。
2. GCD隊列
GCD提供的調(diào)度隊列( dispatch queue)是一個類似于對象的結(jié)構(gòu)设哗,用于管理向其提交的任務(wù)唱捣。所有的dispatch queue都是先進先出(first in, first out. 簡稱FIFO)的數(shù)據(jù)結(jié)構(gòu)。因此网梢,隊列中任務(wù)運行順序與添加順序一致震缭,即第一個進入隊列的任務(wù),第一個被執(zhí)行战虏;第二個進入隊列的任務(wù)拣宰,第二個被執(zhí)行;以此類推烦感。
所有dispatch queue自身都是線程安全的巡社,因此,你可以從多個線程訪問它們啸盏。當你了解了調(diào)度隊列如何為你編寫的代碼提供線程安全時重贺,GCD的好處就顯而易見了。使用GCD的關(guān)鍵在于把要執(zhí)行的任務(wù)提交到正確的dispatch queue或調(diào)度函數(shù)回懦。
GCD的隊列分為串行隊列(Serial Queues)和并發(fā)隊列(Concurrent Queues)兩種。
2.1 串行隊列Serial Queues
在Serial Queues中次企,一次執(zhí)行一個任務(wù)怯晕,一個任務(wù)完成后另一個任務(wù)才會開始。兩個任務(wù)間隔時間無法確定缸棵。
串行隊列中任務(wù)執(zhí)行時間由GCD控制舟茶,唯一可以保證的是任務(wù)執(zhí)行順序和添加順序一致。
因為serial queues中任務(wù)一次只能運行一個堵第,所以不會產(chǎn)生同時訪問critical section吧凉,產(chǎn)生race condition的風險。
2.2 并發(fā)隊列Concurrent Queues
GCD只能保證并發(fā)隊列中的任務(wù)按照添加的順序開始執(zhí)行踏志。至于task結(jié)束順序阀捅,兩個task間時間間隔,任一時間正在運行task數(shù)量都無法保證针余。
上圖中的Block 0開始一段時間后Block 1才開始執(zhí)行饲鄙,Block 1凄诞、Block 2、Block 3開始時間相差很短忍级。雖然Block 3比Block 2開始的晚帆谍,但結(jié)束的早。
GCD決定什么時間開始執(zhí)行任務(wù)轴咱。如果兩項任務(wù)執(zhí)行時間重合汛蝙,由GCD決定是否在另一個內(nèi)核上(如果目前有空閑內(nèi)核)運行任務(wù),還是執(zhí)行context switch來執(zhí)行另一任務(wù)朴肺。
3. 創(chuàng)建和管理Dispatch Queues
在提交任務(wù)到dispatch queue之前患雇,必須確定要使用的隊列類型以及如何使用。如果有特殊用途宇挫,可以自定義配置隊列苛吱。
為了方便使用,GCD默認提供了主隊列dispatch_get_main_queue
和全局隊列dispatch_get_global_queue
器瘪。
3.1 獲取主隊列dispatch_get_main_queue
主隊列是一個全局的串行隊列翠储,運行在應(yīng)用程序的主線程上。主隊列與run loop協(xié)同工作橡疼,交錯執(zhí)行主隊列中任務(wù)和run loop中其他響應(yīng)事件援所。
// 獲取主隊列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
3.2 獲取全局隊列dispatch_get_global_queue
全局隊列是并發(fā)隊列。
系統(tǒng)為每個應(yīng)用程序提供了四個不同優(yōu)先級的全局隊列欣除。因為這些隊列是全局的住拭,可以直接使用dispatch_get_global_queue
函數(shù)請求隊列,不需要顯式創(chuàng)建历帚。
// 獲取優(yōu)先級為QOS_CLASS_USER_INITIATED的全局隊列
dispatch_queue_t aQueue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
dispatch_get_global_queue
函數(shù)的第一個參數(shù)指定Quality of Service(簡稱 QoS)滔岳,第二個參數(shù)是為未來擴展保留的。目前挽牢,總是設(shè)置為0
谱煤。
指定優(yōu)先級的四個參數(shù)如下:
QoS Class | 用途 | 任務(wù)所需時間 |
---|---|---|
QOS_CLASS_USER_INTERACTIVE | 優(yōu)先級與主線程任務(wù)相同,用于處理用戶正在等待禽拔、需要立即反饋的任務(wù)刘离。追求性能和響應(yīng)速度。 | 接近瞬間完成睹栖。 |
QOS_CLASS_USER_INITIATED | 用于用戶發(fā)起的硫惕,需要立即獲得結(jié)果的任務(wù)。例如野来,打開磁盤上的文檔恼除,用戶點擊界面時執(zhí)行相應(yīng)操作,即用戶交互的下一步需要這一步的執(zhí)行結(jié)果梁只。對響應(yīng)和性能有較高要求的缚柳。 | 幾乎瞬間完成埃脏,如幾秒鐘或更少。 |
QOS_CLASS_UTILITY | 需要一些時間來完成秋忙,不需要立即返回結(jié)果彩掐。例如,下載或?qū)霐?shù)據(jù)灰追。一般有提示進度的進度條堵幽。追求響應(yīng)和能源效率的平衡。 | 幾秒鐘至幾分鐘弹澎。 |
QOS_CLASS_BACKGROUND | 在后臺運行朴下,不需要用戶看到。例如:索引苦蒿、同步殴胧、備份。關(guān)注能源效率佩迟。 | 幾分鐘到幾個小時团滥。 |
QOS_CLASS_USER_INTERACTIVE
的優(yōu)先級與主線程相同,但QOS_CLASS_USER_INTERACTIVE
仍然是在全局隊列报强,更新UI只能在主線程中灸姊。沒有用戶交互時,app中任務(wù)應(yīng)當至少90%時間在utility及以下級別運行秉溉。
3.3 自定義隊列
除了使用系統(tǒng)提供的隊列力惯,還可以手動創(chuàng)建隊列。
// 創(chuàng)建串行隊列
dispatch_queue_t serialQueue = dispatch_queue_create("com.GCD.serialQueue", DISPATCH_QUEUE_SERIAL);
// 創(chuàng)建并發(fā)隊列
dispatch_queue_t conQueue = dispatch_queue_create("com.GCD.conQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_create
函數(shù)有兩個參數(shù)召嘶,第一個參數(shù)指定隊列名稱父晶,debugger和性能工具會顯示此處隊列名稱以幫助跟蹤任務(wù)執(zhí)行情況。第二個參數(shù)指定是串行還是并發(fā)隊列苍蔬。在iOS 4.3之前诱建,該參數(shù)默認為NULL
,隊列為串行隊列碟绑。
你可以創(chuàng)建任意數(shù)量串行隊列,但這些串行隊列之間是并發(fā)關(guān)系茎匠。例如格仲,創(chuàng)建了四個串行隊列,每個串行隊列執(zhí)行一個任務(wù)诵冒,系統(tǒng)可能同時執(zhí)行這四個任務(wù)凯肋。
雖然你可以創(chuàng)建任意數(shù)量的串行隊列,但不要寄希望于串行隊列間并發(fā)運行這一特性來提高性能汽馋,而應(yīng)該提交多個任務(wù)到并發(fā)隊列侮东。
4. 通過demo學習GCD
由于這篇文章的目的是優(yōu)化代碼圈盔,以及從不同線程安全地調(diào)用代碼。因此悄雅,你將通過下面網(wǎng)址下載一個模版文件驱敲。
Demo名稱:GrandCentralDispatch模板
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/GrandCentralDispatch模板
該模板文件是一個未優(yōu)化、非線程安全的demo宽闲。在該demo內(nèi)众眨,可以通過相冊或設(shè)定的網(wǎng)址獲取圖片,使用Core Image的CIDetector
API為圖片中的眼睛添加標記容诬。
運行下載的模版娩梨,如下所示:
可以看到當通過網(wǎng)絡(luò)獲取圖片時,UIAlertController
過早的彈出览徒,稍后會修復(fù)這一問題狈定。
這個demo主要有以下四部分:
- CollectionViewController:啟動程序后的第一個視圖控制器,通過略縮圖顯示所有照片习蓬。
- DetailViewController:為眼睛添加圖像纽什,并在
UIImageView
中顯示圖片。 - Photo:為來自
NSURL
類或ALAsset
類的對象實例化照片友雳,最后提供照片稿湿、略縮圖。如果是通過網(wǎng)絡(luò)下載的照片押赊,還會提供下載狀態(tài)饺藤。 - PhotoManager:管理Photo實例。
5. 使用dispatch_async
處理后臺任務(wù)
再次打開app流礁,從相冊添加照片涕俗,或從網(wǎng)絡(luò)下載照片。
點擊CollectionViewController
中的略縮圖神帅,觀察從略縮圖跳轉(zhuǎn)到DetailViewController
的過程再姑,可以觀察到有明顯滯后。這是因為viewDidLoad
中添加了過多的任務(wù)找御,導(dǎo)致初始化DetailViewController
時間變長元镀。如果可能,最好減少viewDidLoad
中工作量霎桅,加快視圖控制器加載速度栖疑。dispatch_async
適合用來處理這一任務(wù)。
進入DetailViewController.m
文件滔驶,使用下面代碼替換viewDidLoad
中代碼:
- (void)viewDidLoad {
[super viewDidLoad];
self.imageView.image = self.image;
NSAssert(self.image, @"Image not set; required to use view controller");
// 1.將代碼從主線程移入全局隊列遇革,因為是異步執(zhí)行,不會堵塞主線程。
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
UIImage *overlayImage = [self faceOverlayImageFromImage:self.image];
// 2.目前已經(jīng)獲取到新的照片萝快,在主線程更新UI锻霎。
dispatch_async(dispatch_get_main_queue(), ^{
[self fadeInNewImage:overlayImage];
});
});
}
再次運行demo,選擇略縮圖揪漩,可以看到DetailViewController
初始化速度變快了旋恼,短暫延遲之后為眼睛添加圖像。如果加載一張更大照片氢拥,app也不會卡在加載視圖控制器環(huán)節(jié)了蚌铜。
dispatch_async
提交任務(wù)到隊列后立即返回,由GCD決定任務(wù)具體執(zhí)行時間嫩海。當需要獲取網(wǎng)絡(luò)資源或進行大量計算時冬殃,應(yīng)當使用dispatch_async
避免堵塞主線程。
dispatch_async
常用隊列:
- 自定義串行隊列Custom Serial Queue:和
dispatch_async
結(jié)合是很好的方案叁怪。 - 主隊列Main Queue(Serial):完成并發(fā)隊列任務(wù)后常用主隊列更新UI审葬,這時的代碼通常在之前的并發(fā)隊列塊內(nèi)。如果當前在主隊列中奕谭,又以主隊列為目標調(diào)用
dispatch_async
涣觉,則會在當前方法結(jié)束后才執(zhí)行此任務(wù)。 - 并發(fā)隊列Concurrent Queue:在后臺執(zhí)行非UI工作的常用選擇血柳。
6. 使用dispatch_after
推遲任務(wù)
現(xiàn)在考慮下app的UE官册,當用戶第一次打開應(yīng)用時會疑惑這個應(yīng)用是做什么的?當PhotoManager
類沒有任何photo實例時难捌,應(yīng)該添加一個提示幫助用戶了解如何使用這個app膝宁。同時需要注意的是,如果提示彈出太快根吁,用戶注意力可能在其他位置员淫,將不能捕捉到提示。延遲一秒顯示提示足以吸引到用戶的注意力击敌。
進入CollectionViewController.m
文件介返,更新showOrHideNavPrompt
方法颤专,如下所示:
- (void)showOrHideNavPrompt {
NSUInteger count = [[PhotoManager sharedManager] photos].count;
double delayInSeconds = 1.0;
// 聲明需要延遲的時間冻辩,單位為納秒nanosecond,一秒的十億分之一痕届。
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
// 等待popTime時長后衡瓶,異步提交block到主隊列捅彻。
dispatch_after(popTime, dispatch_get_main_queue(), ^{
if (count) {
self.headerSize = CGSizeZero;
[self.collectionView reloadData];
}
else
{
self.headerSize = CGSizeMake(40, 40);
[self.collectionView reloadData];
}
});
}
運行app,稍有延遲后鞍陨,sectionHeader
部分會出現(xiàn)如何操作的提示。
dispatch_after
的時間參數(shù)支持設(shè)置為DISPATCH_TIME_NOW
,但不如直接使用dispatch_async
诚撵;時間參數(shù)不支持設(shè)置為DISPATCH_TIME_FOREVER
缭裆。
dispatch_after
常用隊列:
- 自定義串行隊列:需要小心使用,最好用在主隊列中寿烟。
- 主隊列:這是
dispatch_after
的最佳隊列澈驼。Xcode有一個自動完成模版用來在主隊列使用dispatch_after
。 - 并發(fā)隊列:很少需要在并發(fā)隊列使用
dispatch_after
筛武。
7. dispatch_once
讓單例線程安全
單例(Singleton)常被同時從多個控制器調(diào)用缝其,因此,必須確保單例線程安全徘六。
單例模式的線程問題包括初始化内边、讀取、寫入等待锈。PhotoManager
類被設(shè)計為單例漠其,但其目前并不是線程安全的「鸵簦可以通過創(chuàng)建race condition來驗證該問題和屎。
打開PhotoManager.m
文件,sharedManager
方法如下所示:
+ (instancetype)sharedManager {
// 創(chuàng)建單例
static PhotoManager *sharedPhotoManager = nil;
if (!sharedPhotoManager) {
sharedPhotoManager = [[PhotoManager alloc] init];
// 初始化私有photosArray數(shù)組
sharedPhotoManager->_photosArray = [NSMutableArray array];
}
return sharedPhotoManager;
}
由于if
語句不是線程安全的春瞬,多次調(diào)用會出現(xiàn)以下情況:一個線程(稱為線程A)進入if
語句柴信,在sharedManager
分配內(nèi)存前進行了context switch,另一線程(稱為線程B)進入了if
語句宽气,初始化了一個sharedManager
并結(jié)束随常。當系統(tǒng)再次context switch回線程A時,會再次初始化一個sharedManager
抹竹。目前為止线罕,我們有兩個單例對象,而這不是我們想要的窃判。
為強制出現(xiàn)上面的情況钞楼,更新sharedManager
方法代碼如下:
+ (instancetype)sharedManager {
// 創(chuàng)建單例
static PhotoManager *sharedPhotoManager = nil;
if (!sharedPhotoManager) {
// 通過使用sleepForTimeInterval:方法強制進行環(huán)境切換。
[NSThread sleepForTimeInterval:2.0];
sharedPhotoManager = [[PhotoManager alloc] init];
NSLog(@"%i %s",__LINE__, __PRETTY_FUNCTION__);
NSLog(@"Singleton has memory address at: %@",sharedPhotoManager);
[NSThread sleepForTimeInterval:2.0];
// 初始化私有photosArray數(shù)組
sharedPhotoManager->_photosArray = [NSMutableArray array];
}
return sharedPhotoManager;
}
進入AppDelegate.m
文件袄琳,導(dǎo)入PhotoManager.h
文件询件,更新application:didFinishLaunchingWithOptions:
方法如下:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[[UINavigationBar appearance] setBarStyle:UIBarStyleBlack];
// 創(chuàng)建兩個異步并發(fā)調(diào)用,來實例化單例唆樊,以產(chǎn)生race condition宛琅。通過輸出可以看出初始化了幾個單例對象。
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
[PhotoManager sharedManager];
});
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
[PhotoManager sharedManager];
});
return YES;
}
運行app逗旁,控制臺輸出如下:
GrandCentralDispatch[1349:412491] 28 +[PhotoManager sharedManager]
GrandCentralDispatch[1349:412490] 28 +[PhotoManager sharedManager]
GrandCentralDispatch[1349:412490] Singleton has memory address at: <PhotoManager: 0x1c0010f60>
GrandCentralDispatch[1349:412491] Singleton has memory address at: <PhotoManager: 0x1c0010f60>
GrandCentralDispatch[1349:412376] 28 +[PhotoManager sharedManager]
GrandCentralDispatch[1349:412376] Singleton has memory address at: <PhotoManager: 0x1c40114f0>
可以看到代碼的critical condition(即初始化單例部分)執(zhí)行了不止一次嘿辟,雖然這里是強制產(chǎn)生race condition舆瘪,但這種情況也會無意中發(fā)生。
線程問題可能難以調(diào)試红伦,因為它們往往難以重現(xiàn)英古。
為解決該錯誤,實例化代碼應(yīng)只執(zhí)行一次昙读,并在初始化的過程中阻止其他線程進入critical section召调,這正是dispatch_once
所做的。
更新sharedManager
方法如下:
+ (instancetype)sharedManager {
// 創(chuàng)建單例
static PhotoManager *sharedPhotoManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 通過使用sleepForTimeInterval:方法強制進行環(huán)境切換蛮浑。
[NSThread sleepForTimeInterval:2.0];
sharedPhotoManager = [[PhotoManager alloc] init];
NSLog(@"%i %s",__LINE__, __PRETTY_FUNCTION__);
NSLog(@"Singleton has memory address at: %@",sharedPhotoManager);
[NSThread sleepForTimeInterval:2.0];
// 初始化私有photosArray數(shù)組
sharedPhotoManager->_photosArray = [NSMutableArray array];
});
return sharedPhotoManager;
}
運行demo唠叛,通過控制臺可以看到單例只有一個實例。
最后沮稚,移除AppDelegate.m
中的dispatch_async
部分代碼艺沼,移除sharedManager
方法內(nèi)強制產(chǎn)生race condition的代碼和NSLog
輸出。
dispatch_once
只是使共享的實例(即sharedManager
)線程安全壮虫,而不會使類(即PhotoManager)線程安全澳厢,類中可能還會有其他critical section,如對數(shù)據(jù)操作部分囚似。這些critical section也需要使用其他方式實現(xiàn)線程安全剩拢,比如使用synchronize方式訪問數(shù)據(jù)。
8. 單例讀寫線程安全
在處理單例時饶唤,如果單例中的屬性是可變對象徐伐,那么需要考慮該對象本身是否線程安全。如果該屬性是Foundation中容器類募狂,則答案是不一定線程安全办素。一般不可變對象是線程安全,可變對象是非線程安全祸穷,這里的非線程安全很多時候指可以通過多個線程使用性穿,但不能同時使用,具體是否線程請查閱Apple文檔雷滚。
在PhotoManager
單例中使用的NSMutableArray
是非線程安全的需曾。當多個線程同時讀取NSMutableArray
時不會出現(xiàn)問題,但當有線程正在寫時祈远,其他線程進行讀取就會出現(xiàn)問題呆万。目前,PhotoManager
單例并不能阻止這種情況發(fā)生车份。
現(xiàn)在查看PhotoManager.m
文件中的addPhoto:
和photos
方法:
// 這是一個寫方法谋减,其修改了私有可變數(shù)組對象。
- (void)addPhoto:(Photo *)photo {
if (photo) {
[_photosArray addObject:photo];
dispatch_async(dispatch_get_main_queue(), ^{
[self postContentAddedNotification];
});
}
}
// 這是一個讀方法扫沼,通過獲取不可變副本防止調(diào)用者改變數(shù)據(jù)出爹。
- (NSArray *)photos {
return [NSArray arrayWithObject:self.photosArray];
}
在讀方法中庄吼,通過獲取不可變副本可以防止調(diào)用者改變數(shù)據(jù),但不能阻止一個線程調(diào)用addPhoto:
以政,同時另一線程調(diào)用photos
霸褒。
這是軟件開發(fā)中經(jīng)典的Readers-Writes問題,GCD提供了一個優(yōu)雅的解決方案盈蛮,那就是使用dispatch barriers創(chuàng)建Readers-write lock。
當dispatch barriers在concurrent queues隊列上使用時技矮,其充當了串行風格的隊列抖誉。使用GCD的barrier API可以確保提交到指定隊列的塊運行時,該隊列上其他任務(wù)不會運行衰倦。這意味著袒炉,執(zhí)行到dispatch barrier時,dispatch barrier的任務(wù)不會立即執(zhí)行樊零,會等待其他任務(wù)執(zhí)行完畢后才開始執(zhí)行我磁。
當dispatch barrier開始執(zhí)行任務(wù)時,隊列不會執(zhí)行其他任務(wù)驻襟。dispatch barrier任務(wù)執(zhí)行完畢后夺艰,隊列立即恢復(fù)至原來狀態(tài)。GCD提供了同步屏障dispatch_barrier_sync
和異步屏障dispatch_barrier_async
兩種方法沉衣。
下圖說明了barrier函數(shù)對異步塊的影響:
從上圖可以看到郁副,在進入dispatch barrier前,隊列就像正常的并發(fā)隊列豌习。當進入dispatch barrier隊列后存谎,隊列變?yōu)榱舜嘘犃校琤arrier block是唯一在執(zhí)行的任務(wù)肥隆。當barrier block結(jié)束后既荚,隊列立即恢復(fù)為原來的并發(fā)隊列。
創(chuàng)建一個自定義并發(fā)隊列栋艳,使用barrier函數(shù)區(qū)分開PhotoManager
中讀寫操作恰聘,在自定義并發(fā)隊列中將允許多個線程同時讀取數(shù)據(jù)。
進入PhotoManager.m
文件嘱巾,在私有接口部分聲明一個并發(fā)隊列憨琳,并更新addPhoto:
方法:
@interface PhotoManager ()
@property (strong, nonatomic) NSMutableArray *photosArray;
@property (strong, nonatomic) dispatch_queue_t concurrentPhotoQueue; // 添加此屬性。
@end
- (void)addPhoto:(Photo *)photo {
if (photo) {
// 將寫操作添加到barrier函數(shù)旬昭,以便執(zhí)行到寫操作時隊列只執(zhí)行這一項任務(wù)篙螟。
dispatch_barrier_async(self.concurrentPhotoQueue, ^{
// 由于dispatch_barrier_async函數(shù)的存在,執(zhí)行到下面操作時问拘,concurrentPhotoQueue隊列不會執(zhí)行其他任務(wù)遍略。
[_photosArray addObject:photo];
// 因為要更新UI惧所,所以在主隊列發(fā)送通知。
dispatch_async(dispatch_get_main_queue(), ^{
[self postContentAddedNotification];
});
});
}
}
另外绪杏,還需要將讀操作添加到concurrentPhotoQueue
隊列下愈。當讀操作執(zhí)行完畢后,寫操作才會開始蕾久,所以這里的讀操作沒有必要異步提交势似,dispatch_sync
適用目前的情況。
使用dispatch_sync
和dispatch barrier可以用來跟蹤任務(wù)進度僧著,或者用于等待操作完成才能獲取數(shù)據(jù)的情況履因。如果是第二種情況,需要在dispatch_sync
塊外用__block
修飾變量盹愚,點擊這里查看詳細介紹栅迄。
dispatch_sync
常用隊列:
- 自定義串行隊列:需要謹慎使用。如果
dispatch_sync
函數(shù)將任務(wù)提交至當前隊列皆怕,將產(chǎn)生死鎖毅舆。 - 主隊列:與自定義串行隊列一樣,需要謹慎使用愈腾。
- 并發(fā)隊列:非常好的選擇憋活。通過dispatch barrier同步任務(wù),或等待任務(wù)完成以便進一步處理數(shù)據(jù)顶滩。
繼續(xù)在PhotoManager.m
文件內(nèi)余掖,更新photos
方法如下:
- (NSArray *)photos {
// 使用__block修飾,以便塊可以修改array內(nèi)容礁鲁。
__block NSArray *array;
// 同步執(zhí)行讀操作盐欺。
dispatch_sync(self.concurrentPhotoQueue, ^{
// 將讀操作結(jié)果保存到array,以便返回給調(diào)用者仅醇。
array = self.photosArray;
});
return array;
}
最后冗美,初始化concurrentPhotoQueue
屬性,更新sharedManager
方法如下:
+ (instancetype)sharedManager {
...
dispatch_once(&onceToken, ^{
...
// 初始化concurrentPhotoQueue析二。
sharedPhotoManager->_concurrentPhotoQueue = dispatch_queue_create("com.GCD.photoQueue", DISPATCH_QUEUE_CONCURRENT);
});
return sharedPhotoManager;
}
現(xiàn)在粉洼,PhotoManager
已經(jīng)線程安全了,可以從任意線程讀取叶摄、寫入數(shù)據(jù)属韧。
Dispatch barrier常用隊列:
- 自定義串行隊列:因為串行隊列每次執(zhí)行一個任務(wù),所以沒有必要在串行隊列使用barrier函數(shù)蛤吓。
- 全局并發(fā)隊列:因為其他部分可能也在使用global concurrent queue宵喂,而執(zhí)行barrier函數(shù)部分時可能會堵塞線程,所以不要在全局并發(fā)隊列使用barrier函數(shù)会傲。
- 自定義全局隊列:實現(xiàn)barrier函數(shù)的最佳隊列锅棕。
9. 使用dispatch group修復(fù)彈窗過早問題
當使用Internet獲取圖片時拙泽,UIAlertController會在圖片下載完成之前就彈出,如下所示:
錯誤出現(xiàn)在PhotoManager.m
的downloadPhotoWithCompletionBlock:
方法中:
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock {
__block NSError *error;
for (int i=0; i<3; ++i) {
NSURL *url;
switch (i) {
case 0:{
NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"Penny.jpg"];
url = [NSURL URLWithString:urlString];
break;
}
case 1:{
NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"Friends.jpg"];
url = [NSURL URLWithString:urlString];
break;
}
case 2:{
NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"NightKing.jpg"];
url = [NSURL URLWithString:urlString];
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);
}
}
在上面方法的最后調(diào)用了completionBlock
塊裸燎,這時我們在假設(shè)下載已經(jīng)完成顾瞻。但Photo
類初始化方法下載圖片時使用的NSURLSession
類是異步的,即調(diào)用下載后立即返回德绿,導(dǎo)致出現(xiàn)上面錯誤荷荤。
downloadPhotosWithCompletionBlock:
方法應(yīng)該在所有圖片下載完畢后才調(diào)用自身的completionBlock
,但是如何獲取異步并發(fā)任務(wù)的下載進度呢脆炎?我們無法獲知異步并發(fā)任務(wù)什么時間完成梅猿,也不知道其完成的順序。
也許秒裕,你可以寫很多BOOL
屬性來跟蹤每一個下載任務(wù),但那樣會產(chǎn)生很多代碼钞啸,也很容易出錯几蜻。GCD中的dispatch group剛好可以解決這個問題。
在dispatch group內(nèi)任務(wù)完成時体斩,GCD API提供了兩種通知方式:
-
dispatch_group_wait
:該函數(shù)會等待與dispatch group關(guān)聯(lián)的塊完成梭稚。如果在指定時間完成,返回0絮吵,dispatch group變?yōu)榭盏幕】荆蝗绻瑫r沒有完成,返回非0蹬敲,dispatch group恢復(fù)初始狀態(tài)暇昂;如果dispatch group沒有關(guān)聯(lián)任務(wù),則該函數(shù)立即返回伴嗡。 -
dispatch_group_notify
:當dispatch group關(guān)聯(lián)塊完成時急波,該函數(shù)會將任務(wù)提交到指定隊列。如果dispatch group沒有關(guān)聯(lián)任務(wù)瘪校,則立即提交任務(wù)到指定隊列澄暮。提交任務(wù)后,group變?yōu)榭盏内逖铮梢酝ㄟ^dispatch_release
釋放泣懊,或重用于其他塊對象。
9.1 dispatch_group_wait
的使用
進入PhotoManager.m
文件麻惶,更新downloadPhotosWithCompletionBlock:
方法如下:
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock {
// 因為是在主線程中執(zhí)行dispatch_group_wait馍刮,使用dispatch_async將整個代碼添加到其他隊列避免堵塞主線程。
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
// 創(chuàng)建dispatch group,其更像是未完成任務(wù)的計數(shù)器用踩。
dispatch_group_t downloadGroup = dispatch_group_create();
__block NSError *error;
for (int i=0; i<3; ++i) {
NSURL *url;
switch (i) {
case 0:{
NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"Penny.jpg"];
url = [NSURL URLWithString:urlString];
break;
}
case 1:{
NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"Friends.jpg"];
url = [NSURL URLWithString:urlString];
break;
}
case 2:{
NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"NightKing.jpg"];
url = [NSURL URLWithString:urlString];
break;
}
default:
break;
}
// 增加dispatch group中未完成任務(wù)數(shù)渠退,必須與dispatch_group_leave成對使用忙迁。
dispatch_group_enter(downloadGroup);
Photo *photo = [[Photo alloc] initWithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) {
if (_error) {
error = _error;
}
// 減小dispatch group中未完成任務(wù)數(shù),必須與dispatch_group_enter成對使用碎乃。
dispatch_group_leave(downloadGroup);
}];
[[PhotoManager sharedManager] addPhoto:photo];
}
// 等待downloadGroup內(nèi)任務(wù)完成姊扔。
dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER);
// 目前,所有圖片已下載完成梅誓。在主隊列中調(diào)用完成處理程序恰梢。
dispatch_async(dispatch_get_main_queue(), ^{
if (completionBlock) {
completionBlock(error);
}
});
});
}
一項任務(wù)可以添加到多個dispatch group,
運行demo梗掰,通過網(wǎng)絡(luò)獲取圖片嵌言,可以看到圖片下載完成后才出現(xiàn)的彈窗。
如果網(wǎng)絡(luò)速度太快及穗,不能清晰看到圖片下載進度和彈窗順序摧茴,可以打開設(shè)置 > 開發(fā)者 > Network Link Conditioner,選取其中的Very Bad Network開啟網(wǎng)速限制埂陆,使用完畢后記得關(guān)閉網(wǎng)速限制苛白。
9.2 dispatch_group_notify
的使用
使用dispatch_group_wait
會堵塞當前線程,dispatch_group_notify
可以避免這種情況焚虱。
進入PhotoManager.m
文件购裙,更新downloadPhotosWithCompletionBlock:
方法如下:
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock {
// 因為dispatch_group_notify不會堵塞主線程,這里不需要使用dispatch_async鹃栽。
dispatch_group_t downloadGroup = dispatch_group_create();
__block NSError *error;
for (int i=0; i<3; ++i) {
NSURL *url;
switch (i) {
case 0:{
NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"Penny.jpg"];
url = [NSURL URLWithString:urlString];
break;
}
case 1:{
NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"Friends.jpg"];
url = [NSURL URLWithString:urlString];
break;
}
case 2:{
NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"NightKing.jpg"];
url = [NSURL URLWithString:urlString];
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];
}
// 當downloadGroup內(nèi)任務(wù)完成時躏率,將completionBlock塊提交到主隊列。
dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
if (completionBlock) {
completionBlock(error);
}
});
}
當downloadGroup
沒有關(guān)聯(lián)任務(wù)時民鼓,dispatch_group_notify
立即將task提交到指定隊列薇芝。
除手動添加任務(wù)到dispatch group外,還可以使用dispatch_group_async
提交任務(wù)到隊列的同時將任務(wù)添加到group摹察。
- (void)doSomething {
dispatch_group_t testGroup = dispatch_group_create();
dispatch_group_async(testGroup, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
// Do some work here.
});
dispatch_group_notify(testGroup, dispatch_get_main_queue(), ^{
// Won't get here untill everything has finished.
});
}
dispatch_group_async
并不適合我們上面遇到的情況恩掷。因為圖片下載完成前就會返回,會導(dǎo)致dispatch group誤認為已經(jīng)完成任務(wù)供嚎,因此黄娘,必須在圖片下載的完成處理程序中手動調(diào)用dispatch_group_leave
。
dispatch group常用隊列:
- 自定義串行隊列:當一組任務(wù)完成后克滴,使用dispatch group是很好選擇逼争。
- 主隊列:可以很好使用。如果不想堵塞主線程劝赔,應(yīng)謹慎使用
dispatch_group_wait
誓焦。當多個長時間運行任務(wù)完成后(如網(wǎng)絡(luò)調(diào)用),異步模型更新UI是有吸引力的方式。 - 并發(fā)隊列:這也是完成通知的理想選擇杂伟。
10. 使用dispatch_apply
枚舉
繼續(xù)查看downloadPhotosWithCompletionBlock:
方法移层,可以看到該方法內(nèi)有一個for
循環(huán),for
循環(huán)每次只能枚舉一個對象赫粥,依次枚舉观话。使用dispatch_apply
可以并發(fā)枚舉,提高效率越平。
dispatch_apply
函數(shù)提交block到指定隊列以進行多個調(diào)用频蛔,并在所有塊完成后返回。如果指定隊列是由dispatch_get_global_queue
返回的并發(fā)隊列秦叛,則該塊可以并發(fā)調(diào)用晦溪。在并發(fā)隊列使用該函數(shù)時可以將其作為高效的for
循環(huán),但dispatch_apply
內(nèi)塊完成順序是無法確定的挣跋。
更新downloadPhotosWithCompletionBlock:
方法如下:
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock {
dispatch_group_t downloadGroup = dispatch_group_create();
__block NSError *error;
// 使用dispatch_apply并發(fā)枚舉三圆。
dispatch_apply(3, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^(size_t i) {
NSURL *url;
switch (i) {
case 0:{
NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"Penny.jpg"];
url = [NSURL URLWithString:urlString];
break;
}
case 1:{
NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"Friends.jpg"];
url = [NSURL URLWithString:urlString];
break;
}
case 2:{
NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"NightKing.jpg"];
url = [NSURL URLWithString:urlString];
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);
}
});
}
雖然
PhotoManager
單例線程安全,但下載圖片完成的進度可能不一致避咆,最終圖片順序不同嫌术。
運行demo,查看通過網(wǎng)絡(luò)獲取圖片是否更快了呢牌借?
在這個demo中,真的有必要使用dispatch_apply
嗎割按?
-
dispatch_apply
應(yīng)當用來迭代每一個循環(huán)都非常耗時的操作膨报。在這個demo中,可能創(chuàng)建線程的開銷大于并發(fā)帶來的性能提升适荣。 - 不要過度優(yōu)化代碼现柠,應(yīng)當把有限的時間放在更值得的地方。使用Instruments查看性能瓶頸在哪里弛矛,優(yōu)化那些能顯著提高性能的地方够吩。
- 通常,優(yōu)化代碼會使代碼更加復(fù)雜丈氓、難以維護周循,請確保增加的這些代碼是值得的。
dispatch_apply
常用隊列:
- 自定義串行隊列:在串行隊列中万俗,
dispatch_apply
無法并發(fā)枚舉湾笛,不如直接使用for
循環(huán)。 - 主隊列:和自定義串行隊列一樣闰歪,不是一個好的選擇嚎研。
- 并發(fā)隊列:
dispatch_apply
在并發(fā)隊列可以并發(fā)枚舉,能夠提高效率库倘,還可以跟蹤任務(wù)進度临扮。
11. 使用dispatch semaphores
信號量(semaphore)是持有計數(shù)的信號论矾。
假設(shè)有一個房子(對應(yīng)進程概念),房子里住有多個人(對應(yīng)線程概念)杆勇,一個房子可以住多個人(一個進程可以包括多個線程)贪壳,這個房子(進程)的很多資源(如客廳、廚房)是所有人(線程)共享的靶橱。但有些地方(如臥室)最多只能有兩個人進去寥袭,這時可以在臥室門口掛上兩把鑰匙,進去的人拿著鑰匙進去关霸,出來時把鑰匙掛回門口传黄,沒有鑰匙不能進入臥室。
這時門口鑰匙數(shù)量就是信號量(semaphore)队寇。很明顯膘掰,信號量為0時需要等待,信號量大于0時佳遣,無需等待且減去一识埋。
進入GrandCentralDispatchTests.m
文件,添加以下代碼:
// Xcode會在主線程測試所有以test開頭的方法零渐。
- (void)testPennyImageURL {
NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString, @"Penny.jpg"];
[self downloadImageURLWithString:urlString];
}
- (void)testFriendsImageURL {
NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"Friends.jpg"];
[self downloadImageURLWithString:urlString];
}
- (void)testNightKingImageURL {
NSString *urlString = [NSString stringWithFormat:@"%@%@",baseURLString,@"NightKing.jpg"];
[self downloadImageURLWithString:urlString];
}
- (void)downloadImageURLWithString:(NSString *)urlString {
// 創(chuàng)建信號窒舟,參數(shù)指信號量初始值。
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);
}
// 增加信號量诵盼。
dispatch_semaphore_signal(semaphore);
}];
dispatch_time_t timeOutTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC));
// 等待信號10秒鐘(即timeOutTime)惠豺。這會堵塞線程,直到信號量大于0风宁。當超時沒有完成時洁墙,返回非零值,即測試失敗戒财。在指定時間完成時热监,返回零。
if (dispatch_semaphore_wait(semaphore, timeOutTime)) {
XCTFail(@"%@ timed out",urlString);
}
}
點擊Product > Test測試(快捷鍵?+U)饮寞。斷開網(wǎng)絡(luò)再次測試孝扛,10秒中后會失敗。
Xcode中的測試是在XCTestCase子類上執(zhí)行的骂际,會運行任何以test
開頭的方法疗琉。測試是在主線程進行的,所以多個測試是以串行方式進行的歉铝。
當測試返回后盈简,XCTest
會認為該方法已完成,并進行下一個測試方法。這意味著下一個測試運行時柠贤,來自先前測試的異步代碼將繼續(xù)運行香浩。
通過網(wǎng)絡(luò)獲取代碼的方法通常是異步的,這使測試網(wǎng)絡(luò)相關(guān)代碼變得困難臼勉。這時我們可以像上面那樣使用dispatch_semaphore_wait
阻止測試代碼完成邻吭。
總結(jié)
通過這篇文章,相信你對Grand Central Dispatch有一個整體的了解宴霸。以下是關(guān)于GCD的幾點總結(jié):
- Dispatch queues之間是并發(fā)執(zhí)行任務(wù)的囱晴,串行僅限于串行隊列內(nèi)部。
- GCD決定每刻同時執(zhí)行任務(wù)數(shù)量瓢谢。因此畸写,一個app中有100個隊列,每個隊列1個任務(wù)氓扛,但這100個任務(wù)并不會同時執(zhí)行枯芬,除非其有100個內(nèi)核。
- 當要開始新任務(wù)時采郎,任務(wù)的優(yōu)先級Quality of Service會是一個參考要素千所。
- Dispatch queues會復(fù)制添加到隊列的塊,并在完成時釋放該塊蒜埋。也就是說淫痰,不需要先顯式復(fù)制塊,再提交到隊列整份。因
dispatch_sync
同步執(zhí)行任務(wù)黑界,提交到dispatch_sync
的塊不會執(zhí)行block_copy。 - Dispatch queues自身是線程安全的皂林。也就是說,你可以將任務(wù)從任何線程提交到dispatch queue蚯撩,無需先鎖定或同步對隊列的訪問础倍。
- 不要使用
dispatch_sync
將任務(wù)提交給當前隊列,那樣會產(chǎn)生死鎖胎挎。如果需要將任務(wù)添加到當前隊列沟启,請使用dispatch_async
。
盡管NSOperation
和dispatch queues是執(zhí)行任務(wù)的首選方式犹菇,但你仍有可能需要創(chuàng)建自定義的線程德迹。例如:必須實時(real time)運行的代碼。GCD會盡可能快速運行任務(wù)揭芍,但其不是實時的胳搞。如果你確定需要自己創(chuàng)建線程,那么應(yīng)當創(chuàng)建盡可能少的線程。
Demo名稱:GrandCentralDispatch
源碼地址:https://github.com/pro648/BasicDemos-iOS
參考資料: