在之前我們介紹過NSOperation的一些東西,這次我們來聊一聊另一個iOS開發最經常使用的技術之一 --- GCD,GCD將線程的管理移到系統級別,你只需要定義好要執行的任務,然后丟到合適的Dispatch queue,GCD會負責創建線程來執行你的代碼,由于這部分是處于系統級別,所以執行的性能通常非常高。GCD這部分代碼蘋果已開源,有興趣的可以去下載了解一下:地址
在介紹GCD之前我們先了解一下Quality of Service:
Quality of Service(QoS)
這是在iOS8之后提供的新功能,蘋果提供了幾個Quality of Service枚舉來使用:user interactive, user initiated, utility 和 background,通過這告訴系統我們在進行什么樣的工作,然后系統會通過合理的資源控制來最高效的執行任務代碼,其中主要涉及到CPU調度的優先級、IO優先級、任務運行在哪個線程以及運行的順序等等,我們通過一個抽象的Quality of Service參數來表明任務的意圖以及類別。
- NSQualityOfServiceUserInteractive
與用戶交互的任務,這些任務通常跟UI級別的刷新相關,比如動畫,這些任務需要在一瞬間完成 - NSQualityOfServiceUserInitiated
由用戶發起的并且需要立即得到結果的任務,比如滑動scroll view時去加載數據用于后續cell的顯示,這些任務通常跟后續的用戶交互相關,在幾秒或者更短的時間內完成 - NSQualityOfServiceUtility
一些可能需要花點時間的任務,這些任務不需要馬上返回結果,比如下載的任務,這些任務可能花費幾秒或者幾分鐘的時間 - NSQualityOfServiceBackground
這些任務對用戶不可見,比如后臺進行備份的操作,這些任務可能需要較長的時間,幾分鐘甚至幾個小時 - NSQualityOfServiceDefault
優先級介于user-initiated 和 utility,當沒有 QoS信息時默認使用,開發者不應該使用這個值來設置自己的任務
Qos可以跟GCD queue做個對照:
下面我們了解一下GCD的一些用法:
Dispatch Queue
開發者將需要執行的任務添加到合適的Dispatch Queue中即可,Dispatch Queue會根據任務添加的順序先到先執行,其中有以下幾種隊列:
- main dispatch queue
功能跟主線程一樣,通過dispatch_get_main_queue()來獲取,提交到main queue的任務實際上都是在主線程執行的,所以這是一個串行隊列 - global dispatch queues
系統給每個應用提供四個全局的并發隊列,這四個隊列分別有不同的優先級:高、默認、低以及后臺,用戶不能去創建全局隊列,只能根據優先級去獲取:
dispatch_queue_t queue ;
queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- user create queue
用戶可以通過dispatch_queue_create自己創建隊列,該函數有兩個參數,第一個是隊列的名稱,在debug的時候方便區分;第二個是隊列的一些屬性,NULL或者DISPATCH_QUEUE_SERIAL創建出來的隊列是串行隊列,如果傳遞DISPATCH_QUEUE_CONCURRENT則為并行隊列。
//創建并行隊列
dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MyQueue", DISPATCH_QUEUE_CONCURRENT);
- 隊列優先級
dispatch_queue_create創建隊列的優先級跟global dispatch queue的默認優先級一樣,假如我們需要設置隊列的優先級,可以通過dispatch_queue_attr_make_with_qos_class或者dispatch_set_target_queue方法;
//指定隊列的QoS類別為QOS_CLASS_UTILITY
dispatch_queue_attr_t queue_attr = dispatch_queue_attr_make_with_qos_class (DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY,-1);
dispatch_queue_t queue = dispatch_queue_create("queue", queue_attr);
dispatch_set_target_queue的第一個參數為要設置優先級的queue,第二個參數是對應的優先級參照物
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.MyQueue",NULL);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0);
//serialQueue現在的優先級跟globalQueue的優先級一樣
dispatch_set_target_queue(serialQueue, globalQueue);
- dispatch_set_target_queue
dispatch_set_target_queue除了能用來設置隊列的優先級之外,還能夠創建隊列的層次體系,當我們想讓不同隊列中的任務同步的執行時,我們可以創建一個串行隊列,然后將這些隊列的target指向新創建的隊列即可,比如
dispatch_queue_t targetQueue = dispatch_queue_create("target_queue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
dispatch_set_target_queue(queue1, targetQueue);
dispatch_set_target_queue(queue2, targetQueue);
dispatch_async(queue1, ^{
NSLog(@"do job1");
[NSThread sleepForTimeInterval:3.f];
});
dispatch_async(queue2, ^{
NSLog(@"do job2");
[NSThread sleepForTimeInterval:2.f];
});
dispatch_async(queue2, ^{
NSLog(@"do job3");
[NSThread sleepForTimeInterval:1.f];
});
可以看到執行的結果如下,這些隊列會同步的執行任務。
GCDTests[13323:569147] do job1
GCDTests[13323:569147] do job2
GCDTests[13323:569147] do job3
- dispatch_barrier_async
dispatch_barrier_async用于等待前面的任務執行完畢后自己才執行,而它后面的任務需等待它完成之后才執行。一個典型的例子就是數據的讀寫,通常為了防止文件讀寫導致沖突,我們會創建一個串行的隊列,所有的文件操作都是通過這個隊列來執行,比如FMDB,這樣就可以避免讀寫沖突。不過其實這樣效率是有提升的空間的,當沒有更新數據時,讀操作其實是可以并行進行的,而寫操作需要串行的執行,如何實現呢:
dispatch_queue_t queue = dispatch_queue_create("Database_Queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"reading data1");
});
dispatch_async(queue, ^{
NSLog(@"reading data2");
});
dispatch_barrier_async(queue, ^{
NSLog(@"writing data1");
[NSThread sleepForTimeInterval:1];
});
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"reading data3");
});
執行結果如下:
GCDTests[13360:584316] reading data2
GCDTests[13360:584317] reading data1
GCDTests[13360:584317] writing data1
GCDTests[13360:584317] reading data3
我們將寫數據的操作放在dispatch_barrier_async中,這樣能確保在寫數據的時候會等待前面的讀操作完成,而后續的讀操作也會等到寫操作完成后才能繼續執行,提高文件讀寫的執行效率。
- dispatch_queue_set_specific 、dispatch_get_specific
這兩個API類似于objc_setAssociatedObject跟objc_getAssociatedObject,FMDB里就用到這個來防止死鎖,來看看FMDB的部分源碼
static const void * const kDispatchQueueSpecificKey = &kDispatchQueueSpecificKey;
//創建一個串行隊列來執行數據庫的所有操作
_queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
//通過key標示隊列,設置context為self
dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
當要執行數據庫操作時,如果在queue里面的block執行過程中,又調用了 indatabase方法,需要檢查是不是同一個queue,因為同一個queue的話會產生死鎖情況
- (void)inDatabase:(void (^)(FMDatabase *db))block {
FMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey);
assert(currentSyncQueue != self && "inDatabase: was called reentrantly on the same queue, which would lead to a deadlock");
}
- dispatch_apply
dispatch_apply類似一個for循環,會在指定的dispatch queue中運行block任務n次,如果隊列是并發隊列,則會并發執行block任務,dispatch_apply是一個同步調用,block任務執行n次后才返回。
簡單的使用方法:
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_CONCURRENT);
//并發的運行一個block任務5次
dispatch_apply(5, queue, ^(size_t i) {
NSLog(@"do a job %zu times",i+1);
});
NSLog(@"go on");
輸出結果:
GCDTests[10029:760640] do a job 2 times
GCDTests[10029:760640] do a job 1 times
GCDTests[10029:760640] do a job 3 times
GCDTests[10029:760640] do a job 5 times
GCDTests[10029:760640] do a job 4 times
GCDTests[10029:760640] go on
在某些場景下使用dispatch_apply會對性能有很大的提升,比如你的代碼需要以每個像素為基準來處理計算image圖片。同時dispatch apply能夠避免一些線程爆炸的情況發生(創建很多線程)
//危險,可能導致線程爆炸以及死鎖
for (int i = 0; i < 999; i++){
dispatch_async(q, ^{...});
}
dispatch_barrier_sync(q, ^{});
// 較優選擇, GCD 會管理并發
dispatch_apply(999, q, ^(size_t i){...});
Dispatch Block
添加到gcd隊列中執行的任務是以block的形式添加的,block封裝了需要執行功能,block帶來的開發效率提升就不說了,gcd跟block可以說是一對好基友,能夠很好的配合使用。
- 創建block
我們可以自己創建block并添加到queue中去執行
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
//創建block
dispatch_block_t block = dispatch_block_create(0, ^{
NSLog(@"do something");
});
dispatch_async(queue, block);
在創建block的時候我們也可以通過設置QoS,指定block對應的優先級,在dispatch_block_create_with_qos_class中指定QoS類別即可:
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
dispatch_block_t block = dispatch_block_create_with_qos_class(0, QOS_CLASS_USER_INITIATED, -1, ^{
NSLog(@"do something with QoS");
});
dispatch_async(queue, block);
- dispatch_block_wait
當需要等待前面的任務執行完畢時,我們可以使用dispatch_block_wait這個接口,設置等待時間DISPATCH_TIME_FOREVER會一直等待直到前面的任務完成:
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
dispatch_block_t block = dispatch_block_create(0, ^{
NSLog(@"before sleep");
[NSThread sleepForTimeInterval:1];
NSLog(@"after sleep");
});
dispatch_async(queue, block);
//等待前面的任務執行完畢
dispatch_block_wait(block, DISPATCH_TIME_FOREVER);
NSLog(@"coutinue");
程序運行結果:
GCDTests[16679:863641] before sleep
GCDTests[16679:863641] after sleep
GCDTests[16679:863529] coutinue
- dispatch_block_notify
dispatch_block_notify當觀察的某個block執行結束之后立刻通知提交另一特定的block到指定的queue中執行,該函數有三個參數,第一參數是需要觀察的block,第二個參數是被通知block提交執行的queue,第三參數是當需要被通知執行的block,函數的原型:
void dispatch_block_notify(dispatch_block_t block, dispatch_queue_t queue,
dispatch_block_t notification_block);
具體使用的方法:
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
dispatch_block_t previousBlock = dispatch_block_create(0, ^{
NSLog(@"previousBlock begin");
[NSThread sleepForTimeInterval:1];
NSLog(@"previousBlock done");
});
dispatch_async(queue, previousBlock);
dispatch_block_t notifyBlock = dispatch_block_create(0, ^{
NSLog(@"notifyBlock");
});
//當previousBlock執行完畢后,提交notifyBlock到global queue中執行
dispatch_block_notify(previousBlock, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), notifyBlock);
運行結果:
GCDTests[17129:895673] previousBlock begin
GCDTests[17129:895673] previousBlock done
GCDTests[17129:895673] notifyBlock
- dispatch_block_cancel
之前在介紹nsopreration的時候提到它的一個優點是可以取消某個operation,現在在iOS8之后,提交到gcd隊列中的dispatch block也可取消了,只需要簡單的調用dispatch_block_cancel傳入想要取消的block即可:
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
dispatch_block_t block1 = dispatch_block_create(0, ^{
NSLog(@"block1 begin");
[NSThread sleepForTimeInterval:1];
NSLog(@"block1 done");
});
dispatch_block_t block2 = dispatch_block_create(0, ^{
NSLog(@"block2 ");
});
dispatch_async(queue, block1);
dispatch_async(queue, block2);
dispatch_block_cancel(block2);
可以看到如下的執行結果,block2不再執行了。
GCDTests[17271:902981] block1 begin
GCDTests[17271:902981] block1 done
Dispatch Group
當我們想在gcd queue中所有的任務執行完畢之后做些特定事情的時候,也就是隊列的同步問題,如果隊列是串行的話,那將該操作最后添加到隊列中即可,但如果隊列是并行隊列的話,這時候就可以利用dispatch_group來實現了,dispatch_group能很方便的解決同步的問題。dispatch_group_create可以創建一個group對象,然后可以添加block到該組里面,下面看下它的一些用法:
- dispatch_group_wait
dispatch_group_wait會同步地等待group中所有的block執行完畢后才繼續執行,類似于dispatch barrier
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
//將任務異步地添加到group中去執行
dispatch_group_async(group,queue,^{ NSLog(@"block1"); });
dispatch_group_async(group,queue,^{ NSLog(@"block2"); });
dispatch_group_wait(group,DISPATCH_TIME_FOREVER);
NSLog(@"go on");
執行結果如下,只有block1跟block2執行完畢后才會執行dispatch_group_wait后面的內容。
GCDTests[954:41031] block2
GCDTests[954:41032] block1
GCDTests[954:40847] go on
- dispatch_group_notify
功能與dispatch_group_wait類似,不過該過程是異步的,不會阻塞該線程,dispatch_group_notify有三個參數
void dispatch_group_notify(dispatch_group_t group, //要觀察的group
dispatch_queue_t queue, //block執行的隊列
dispatch_block_t block); //當group中所有任務執行完畢之后要執行的block
簡單的示意用法:
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group,queue,^{ NSLog(@"block1"); });
dispatch_group_async(group,queue,^{ NSLog(@"block2"); });
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"done");
});
NSLog(@"go on");
可以看到如下的執行結果
GCDTests[1046:45104] go on
GCDTests[1046:45153] block1
GCDTests[1046:45152] block2
GCDTests[1046:45104] done
- dispatch_group_enter dispatch_group_leave
假如我們不想使用dispatch_group_async異步的將任務丟到group中去執行,這時候就需要用到dispatch_group_enter跟dispatch_group_leave方法,這兩個方法要配對出現,以下這兩種方法是等價的:
dispatch_group_async(group, queue, ^{
});
等價于
dispatch_group_enter(group);
dispatch_async(queue, ^{
dispatch_group_leave(group);
});
簡單的使用方法,可以自己試試沒有寫dispatch_group_leave會發生什么。
dispatch_group_t group = dispatch_group_create();
for (int i =0 ; i<3; i++) {
dispatch_group_enter(group);
NSLog(@"do block:%d",i);
dispatch_group_leave(group);
}
//等待上面的任務完成
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"go on");
Dispatch Semaphore
dispatch semaphore也是用來做解決一些同步的問題,dispatch_semaphore_create會創建一個信號量,該函數需要傳遞一個信號值,dispatch_semaphore_signal會使信號值加1,如果信號值的大小等于1,dispatch_semaphore_wait會使信號值減1,并繼續往下走,如果信號值為0,則等待。
//創建一個信號量,初始值為0
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"do some job");
sleep(1);
NSLog(@"increase the semaphore");
dispatch_semaphore_signal(sema); //信號值加1
});
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);//等待直到信號值大于等1
NSLog(@"go on");
執行結果如下:
GCDTests[1394:92383] do some job
GCDTests[1394:92383] increase the semaphore
GCDTests[1394:92326] go on
Dispatch Timer
dispatch timer通常配合dispatch_after使用,完成一些延時的任務:
//延遲5秒后執行任務
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^{
NSLog(@"do job afer 5 seconds");
});
Dispatch IO
當我們要讀取一份較大文件的時候,多個線程同時去讀肯定比一個線程去讀的速度要快,要實現這樣的功能可以通過dispatch io跟dispatch data來實現,通過dispatch io去讀文件時,會使用global dispatch queue將一個文件按照一個指定的分塊大小同時去讀取數據,類似于:
dispatch_async(queue, ^{/* 讀取0-99字節 */});
dispatch_async(queue, ^{/* 讀取100-199字節 */});
dispatch_async(queue, ^{/* 讀取200-299字節 */});
...
將文件分成一塊一塊并行的去讀取,讀取的數據通過Dispatch Data可以更為簡單地進行結合和分割 。
- dispatch_io_create
生成Dispatch IO,指定發生錯誤時用來執行處理的Block,以及執行該Block的Dispatch Queue - dispatch_io_set_low_water
設定一次讀取的大小(分割的大小) - dispatch_io_read
使用Global Dispatch Queue開始并列讀取,當每個分割的文件塊讀取完畢時,會將含有文件數據的dispatch data返回到dispatch_io_read設定的block,在block中需要分析傳遞過來的dispatch data進行合并處理
可以看下蘋果的系統日志API(Libc-763.11 gen/asl.c)的源代碼使用到了dispatch IO:源碼地址
//dispatch_io_create出錯時handler執行的隊列
pipe_q = dispatch_queue_create("PipeQ", NULL);
pipe_channel = dispatch_io_create(DISPATCH_IO_STREAM, fd, pipe_q, ^(int err){
//出錯時執行的handler
close(fd);
});
*out_fd = fdpair[1];
//設定一次讀取的大小(分割大小)
dispatch_io_set_low_water(pipe_channel, SIZE_MAX);
dispatch_io_read(pipe_channel, 0, SIZE_MAX, pipe_q, ^(bool done, dispatch_data_t pipedata, int err){
if (error)
return;
if (err == 0)
{
//每次讀取到數據進行數據的處理
size_t len = dispatch_data_get_size(pipedata);
if (len > 0)
{
const char *bytes = NULL;
char *encoded;
uint32_t eval;
dispatch_data_t md = dispatch_data_create_map(pipedata, (const void **)&bytes, &len);
encoded = asl_core_encode_buffer(bytes, len);
asl_msg_set_key_val(aux, ASL_KEY_AUX_DATA, encoded);
free(encoded);
eval = _asl_evaluate_send(NULL, (aslmsg)aux, -1);
_asl_send_message(NULL, eval, aux, NULL);
asl_msg_release(aux);
dispatch_release(md);
}
}
if (done)
{
//并發讀取完畢
dispatch_semaphore_signal(sem);
dispatch_release(pipe_channel);
dispatch_release(pipe_q);
}
});
假如你的數據文件比較大,可以考慮采用dispatch IO的方式來提高讀取的速率。
Dispatch Source
dispatch框架提供一套接口用于監聽系統底層對象(如文件描述符、Mach端口、信號量等),當這些對象有事件產生時會自動把事件的處理block函數提交到dispatch隊列中執行,這套接口就是Dispatch Source API,Dispatch Source其實就是對kqueue功能的封裝,可以去查看dispatch_source的c源碼實現(什么是kqueue?Google,什么是Mach端口? Google Again),Dispatch Source主要處理以下幾種事件:
DISPATCH_SOURCE_TYPE_DATA_ADD 變量增加
DISPATCH_SOURCE_TYPE_DATA_OR 變量OR
DISPATCH_SOURCE_TYPE_MACH_SEND Mach端口發送
DISPATCH_SOURCE_TYPE_MACH_RECV Mach端口接收
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE 內存壓力情況變化
DISPATCH_SOURCE_TYPE_PROC 與進程相關的事件
DISPATCH_SOURCE_TYPE_READ 可讀取文件映像
DISPATCH_SOURCE_TYPE_SIGNAL 接收信號
DISPATCH_SOURCE_TYPE_TIMER 定時器事件
DISPATCH_SOURCE_TYPE_VNODE 文件系統變更
DISPATCH_SOURCE_TYPE_WRITE 可寫入文件映像
當有事件發生時,dispatch source自動將一個block放入一個dispatch queue執行。
dispatch_source_create
創建一個dispatch source,需要指定事件源的類型,handler的執行隊列,dispatch source創建完之后將處于掛起狀態。此時dispatch source會接收事件,但是不會進行處理,你需要設置事件處理的handler,并執行額外的配置;同時為了防止事件堆積到dispatch queue中,dispatch source還會對事件進行合并,如果新事件在上一個事件處理handler執行之前到達,dispatch source會根據事件的類型替換或者合并新舊事件。dispatch_source_set_event_handler
給指定的dispatch source設置事件發生的處理handlerdispatch_source_set_cancel_handler
給指定的dispatch source設置一個取消處理handler,取消處理handler會在dispatch soruce釋放之前做些清理工作,比如關閉文件描述符:
dispatch_source_set_cancel_handler(mySource, ^{
close(fd); //關閉文件秒速符
});
- dispatch_source_cancel
異步地關閉dispatch source,這樣后續的事件發生時不去調用對應的事件處理handler,但已經在執行的handler不會被取消。
很多第三方庫會用到dispatch source的功能,比如著名的IM框架XMPPFramework在涉及到定時器的時候都采用這種方法,比如發送心跳包的時候(setupKeepAliveTimer)。
一個簡單的例子:
//如果dispatch source是本地變量,會被釋放掉,需要這么聲明
@property (nonatomic)dispatch_source_t timerSource;
//事件handler的處理隊列
dispatch_queue_t queue = dispatch_queue_create("myqueue", NULL);
//
_timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//定時器間隔時間
uint64_t interval = 2 * NSEC_PER_SEC;
//設置定時器信息
dispatch_source_set_timer(_timerSource,DISPATCH_TIME_NOW, interval , 0);
//設置事件的處理handler
dispatch_source_set_event_handler(_timerSource, ^{
NSLog(@"receive time event");
//if (done)
// dispatch_source_cancel(_timerSource);
});
//開始處理定時器事件,dispatch_suspend暫停處理事件
dispatch_resume(_timerSource);
定時器還可以通過NSTimer實現,不過NSTimer會跟runloop關聯在一起,主線層默認有一個runloop,假如你nstimer是運行在子線程,就需要自己手動開啟一個runloop,而且nstimer默認是在NSDefaultRunLoopMode模式下的,所以當runloop切換到其它模式nstimer就不會運行,需要手動將nstimer添加到NSRunLoopCommonModes模式下;而dispatch source timer不跟runloop關聯,所以有些場景可以使用這種方法。
本文總結了GCD的一些用法,不過有些API可能iOS8之后才可以用,如有還有什么可以補充的,歡迎提出~