GCD、dispatch函數介紹,gcddispatch函數
iOS多線程的方法有3種:
- NSThread
- NSOperation
- GCD(Grand Central Dispatch)
其中,由蘋果所倡導的為多核的並行運算提出的解決方案:GCD能夠訪問線程池,並且可在應用的整個生命的周期裡面使用,一般來說,GCD會盡量維護一些適合機器體繫結構的線程,在有工作需求的時候,自動利用更多的處理器核心,一次來充分使用更強大的機器系統效能,以前的iOS裝置為單核處理器的,線程池的用處並不大,但是現在的行動裝置,包括iOS裝置,愈發地朝多核的方向邁進,因此GCD中的線程池,能夠在此類裝置中,更加強大地使系統效能上得到更加完善的使用。
GCD,無疑是最便捷的,基於C語言的所設計的。在使用GCD的過程中,最方便的,莫過於不需要編寫基礎線程代碼,其生命週期也不需要我們手動管理,建立需要的任務,然後添加到已建立好的queue隊列。GCD便會負責建立線程和調度任務,由系統直接提供線程管理。
這樣一種多線程的方式,我們也會在實際項目中經常看到:app中,由於資料的執行與交換所消耗的時間長,導致需要反饋給使用者UI介面往往出現延遲的現象。這樣我們可以通過多線程的方法,讓需要調用的方法在後台執行、在主線程上進行UI介面的切換,這樣不僅是使用者體驗更加友好美觀,也使得程式設計井然有序。本文主要粗略介紹GCD的一般使用,以及GCD中dispatch_首碼方法調用的作用和使用範圍。
UI介面如,通過建立4個按鈕事件,分析4種不同的函數所執行的程式塊運行方式:
【本次開發環境: Xcode:7.2 iOS Simulator:iphone6 By:啊左】
一、GCD的使用
GCD對於開發人員來說,最簡單的,就是通過調用dispatch把一連串的非同步任務添加到隊列中,進行非同步執行操作。
代碼調用如下:
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
- async表示非同步運行;
- queue為我們提前建立的隊列;
- block也就是“塊”,讓我們執行事件的模組;
當然,我們也可以使用同步任務,使用dispatch_sync函數添加到相應的隊列中,而這個函數會阻塞當前調用線程,直到相應任務完成執行。
但是,也正因為這樣的同步特性,在實際項目中,當有同步任務添加到正在執行同步任務的隊列時,串列的隊列會出現死結。而且由於同步任務會阻塞主線程的運行,可能會導致某個事件無法響應。
需要注意的是,調用dispatch_async不會讓塊運行,而是把塊添加到隊列末尾。隊列不是線程,它的作用是組織塊。(如果讀者學過資料結構的知識,就會知道隊列的基本特徵如飯堂排隊隊,先到的排前面,先打到飯,也就是“先進先出”原理)
在GCD中,可以給開發人員調用的常見公用隊列有以下兩種:
還有另外一種,dispatch_get_current_queue,用於擷取當前正在執行任務的隊列,主要用於調試,但是在iOS 6.0之後蘋果已經廢棄,原因是容易造成死結。詳情可以查看官方注釋。
這兩種公用隊列的調用便可以解決我們剛剛關於後台執行任務、主線程用於更新UI介面的問題,
結構如下:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 把邏輯計算等需要消耗長時間的任務,放在此處的全域共用的並發隊列執行; dispatch_async(dispatch_get_main_queue(), ^{ // 回到主線程更新UI介面; }); });
在有一些項目中,會涉及到非同步下載圖片,這個時候就可以使用這樣一種結構來進行任務的分配:
// 非同步下載圖片 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //先把下載資料的任務放在全域共用並發隊列中執行 NSURL *url = [NSURL URLWithString:@"圖片的URL"]; NSData * data = [[NSData alloc]initWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:data]; if(data != nil) { // 完成後,回到主線程顯示圖片 dispatch_async(dispatch_get_main_queue(), ^{ self.imageView.image = image; }); } });
二、 串列隊列 and 並行隊列
1.串列(Serial)的執行:指同一時間每次只能執行一個任務。 線程池只提供一個線程用來執行任務,所以後一個任務必須等到前一個任務執行結束才能開始。
可以添加多個任務到隊列中,執行次序FIFO,但是當程式需要執行大量的任務時,雖然系統允許,但是鑒於程式的資源分派,應該交給全域並發隊列來完成才能更好地發揮系統效能。
建立串列隊列的方式如下:
dispatch_queue_t serialQueue = dispatch_queue_create("zuoA", NULL);//第一個參數是隊列的名稱,通常使用公司的反網域名稱;第二個參數是隊列相關屬性,一般用NULL
關於什麼是FIFO次序,我們用代碼解釋一下
- (IBAction)SerialQueue:(UIButton *)sender { dispatch_queue_t serialQueue = dispatch_queue_create("zuoA", NULL); dispatch_async(serialQueue, ^{ sleep(3); NSLog(@"A任務"); }); dispatch_async(serialQueue, ^{ sleep(2); NSLog(@"B任務"); }); dispatch_async(serialQueue, ^{ sleep(1); NSLog(@"C任務"); });}
console控制台顯示如下:
2016-03-15 15:04:11.909 dispatch_queue的多任務GCD使用[92316:2538875] A任務2016-03-15 15:04:13.910 dispatch_queue的多任務GCD使用[92316:2538875] B任務2016-03-15 15:04:14.910 dispatch_queue的多任務GCD使用[92316:2538875] C任務
可以看得到,即使需要等待幾秒,後面所添加的任務也必須等待前面的任務完成後才能執行,隊列完全按照"先進先出"的順序,也即是所執行的順序取決於:開發人員將工作任務添加進隊列的順序.
2.並行(concurrent)的執行:可同一時間可以同時執行多個任務。
- 負荷:並發執行任務與系統有關,能夠同時執行任務的數量是由系統根據應用和此時的系統狀態等動態變化決定的。
- 順序:由於並行隊列也是隊列(吐槽這是廢話T^T),因此每個任務的啟動時間也是按照FIFO次序,也就是加入queue的順序,但是結束的順序則依賴各自的任務所需要消耗的時間
- 與串列的不同:雖然啟動時間一致,但是這是“並發執行”,因此不需要等到上一個任務完成後才進行下一個任務。
上代碼,找不同。。。
- (IBAction)concurrentQueue:(UIButton *)sender { dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(concurrentQueue, ^{ dispatch_async(concurrentQueue, ^{ sleep(3); NSLog(@"A任務"); }); dispatch_async(concurrentQueue, ^{ sleep(2); NSLog(@"B任務"); }); dispatch_async(concurrentQueue, ^{ sleep(1); NSLog(@"C任務"); }); });}
console控制台顯示如下:
2016-03-15 15:02:06.911 dispatch_queue的多任務GCD使用[92294:2537296] C任務2016-03-15 15:02:07.907 dispatch_queue的多任務GCD使用[92294:2537147] B任務2016-03-15 15:02:08.908 dispatch_queue的多任務GCD使用[92294:2537177] A任務
通過控制台左邊的時間記錄,與串列隊列不同的是,並行隊列中這3個任務的並行調用,與串列不同的是,不需要等到A任務調用完,就已經在調用B、C,顯著地提高了線程的執行速度,凸顯了並行隊列所執行的非同步作業的並行特性;
我們可以看到這段代碼中,不同的是串列隊列需要建立一個新的隊列,而並行隊列中,只需要調用iOS系統中為我們提供的全域共用dispatch_get_global_queue就可以了:
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
第一個參數為iOS系統為全域共用隊列提供4種調度的方式,主要區別即是優先順序的不同而已:
我們採用預設的DISPATCH_QUEUE_PRIORITY_DEFAULT方式,而右邊的第二個參數是蘋果預留的,暫時沒有其他的含義,所以,一般預設為:0。
並發的好處就是不需要像串列一樣按照順序執行,並發執行可以顯著地提高速度。
三、dispatch_group_async的使用
有時候,我們會遇到這樣的情況,UI介面部分的顯示,需要在完成幾個任務,例如3張圖片下載完畢,才通知UI介面已經完成任務。
我們可以通過指派組(dispatch group)進行並發程式塊分配的運用,將非同步指派(dispatch_async)的所有程式塊設定為鬆散,或者分配給多個線程來執行,監聽到這組任務完成後,使用dispatch_group_notify()完後才能最後一個例如UI介面的程式塊。
代碼:
- (IBAction)groupQueue:(UIButton *)sender { dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, queue, ^{ sleep(3); NSLog(@"A任務"); }); dispatch_group_async(group, queue, ^{ sleep(2); NSLog(@"B任務"); }); //group組中的任務完後,通知並調用notify中的塊 dispatch_group_notify(group, queue, ^{ NSLog(@"主任務"); });}
console控制台顯示如下:
2016-03-16 11:18:41.306 dispatch_queue的多任務GCD使用[94865:2718342] B任務2016-03-16 11:18:42.302 dispatch_queue的多任務GCD使用[94865:2718341] A任務2016-03-16 11:18:42.303 dispatch_queue的多任務GCD使用[94865:2718341] 主任務
除了驗證前面說的知道指派組任務都完後,notify添加的任務塊才會執行。
眼尖的讀者可能也發現,整個工作群組完成的時間比2個任務分別啟動並執行時間還要短!這得益於我們同時進行了兩種計算~
當然在真實的開發運用中,這種明顯已耗用時間縮短的效果,取決於所需要執行的工作量和可用的資源,以及多個CPU核心的可用性,因此在多核技術日益完善的大環境下,這樣一種多線程技術將得到更有效利用。
四、dispatch_barrier_async的使用
dispatch_barrier(指派屏障)是當前面的任務執行完後,才執行barrier塊的任務,而且後面的任務也得等到barrier塊的執行完畢後才能開始執行。
很好地突顯了“障礙物”這樣的特性,那麼代碼上應該怎麼寫呢?
按照並發的性質,我們在barrierQueue方法中敲入以下代碼:
- (IBAction)barrierQueue:(UIButton *)sender { dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(queue, ^{ sleep(2); NSLog(@"A任務"); }); dispatch_async(queue, ^{ sleep(1); NSLog(@"B任務"); }); dispatch_barrier_async(queue, ^{ NSLog(@"barrier任務"); }); dispatch_async(queue, ^{ sleep(1); NSLog(@"C任務"); });}
console控制台顯示如下:
2016-03-16 13:18:47.525 dispatch_queue的多任務GCD使用[95191:2752854] barrier任務2016-03-16 13:18:48.529 dispatch_queue的多任務GCD使用[95191:2752839] B任務2016-03-16 13:18:48.529 dispatch_queue的多任務GCD使用[95191:2752844] C任務2016-03-16 13:18:49.528 dispatch_queue的多任務GCD使用[95191:2752840] A任務
任務的執行順序依然是跟並行隊列的方法一樣,barrier沒有發揮它的“障礙物”的界限作用。這是因為barrier這一塊的是依賴隊列queue的模型來執行的,當隊列為全域共用時,barrier無法發揮其作用。而是需要新建立一個隊列,
dispatch_queue_t queue = dispatch_queue_create("zuoA", DISPATCH_QUEUE_SERIAL);
完整的程式如下:
- (IBAction)barrierQueue:(UIButton *)sender { dispatch_queue_t queue = dispatch_queue_create("zuoA", DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{ sleep(2); NSLog(@"A任務"); }); dispatch_async(queue, ^{ sleep(1); NSLog(@"B任務"); }); dispatch_barrier_async(queue, ^{ NSLog(@"barrier任務"); }); dispatch_async(queue, ^{ sleep(1); NSLog(@"C任務"); });}
console控制台顯示如下:
2016-03-16 13:30:14.251 dispatch_queue的多任務GCD使用[95263:2759658] A任務2016-03-16 13:30:15.255 dispatch_queue的多任務GCD使用[95263:2759658] B任務2016-03-16 13:30:15.255 dispatch_queue的多任務GCD使用[95263:2759658] barrier任務2016-03-16 13:30:16.256 dispatch_queue的多任務GCD使用[95263:2759658] C任務
這就是我們想要得到的效果:確實只有在前面A、B任務完成後,barrier任務才能執行,最後才能執行C任務。
那麼,dispatch_queue_create為什麼要用 DISPATCH_QUEUE_SERIAL,可以用其他嗎?答案是肯定的。把參數換成DISPATCH_QUEUE_SERIAL
可以得到以下輸出:
2016-03-16 13:34:23.855 dispatch_queue的多任務GCD使用[95294:2762604] B任務2016-03-16 13:34:24.853 dispatch_queue的多任務GCD使用[95294:2762603] A任務2016-03-16 13:34:24.853 dispatch_queue的多任務GCD使用[95294:2762603] barrier任務2016-03-16 13:34:25.856 dispatch_queue的多任務GCD使用[95294:2762603] C任務
也就是說,A、B、C任務完全是按照隊列的順序執行,只是由於barrier塊的“屏障”作用,使得後來加入的C任務只有等到barrier塊執行完畢才能運行;
五、dispatch_suspend(暫停)和dispatch_resume(繼續)
- 暫停:當需要暫停某個隊列queue時, 調用dispatch_suspend(queue),此時阻止了queue執行塊對象,且此時
queue的引用計數增加;
- 繼續:繼續queue時,調用
dispatch_resume(queue),此時queue啟動執行塊的操作,queue的引用計數減少;
需要注意的是,suspend與resume是非同步,只在block塊之間調用,而且是成對存在的。
還有一些其他的dispatch函數,例如
dispatch_once:可以使特定的塊在整個應用程式生命週期中只被執行一次~
dispatch_apply:執行某個程式碼片段n次(開發人員可以自己設定)。
dispatch_after:當我們需要等待幾秒後進行某個操作,可以使用這個函數;
注意事項:
1.在上面的例子中,我們沒有使用過手動內管其記憶體,因為系統會自動管理。
如果你部署的最低目標低於 iOS 6.0 or Mac OS X 10.8
就需要自己管理GCD對象,使用(dispatch_retain,dispatch_release),ARC並不會去管理它們對於最低sdk版本>=ios6.0來說,GCD對象已經納入了ARC的管理範圍,我們就不需要再手工調用 dispatch_release和dispatch_retain函數來增加和減少引用計數以管理記憶體了。2.之所以一直在說dispatch函數,而不是方法,方法歸類、對象所有,而dispatch是C語言下的函數,函數與方法是不同的,函數歸檔案所有。 本文旨在介紹dispatch_等函數的運用,文字篇幅有些大,敬請見諒(╯▽╰)