本文摘錄自《Objective-C高級編程》一書,附加一些自己的理解栋荸,作為對GCD的總結(jié)菇怀。
此篇主要包含以下幾個方面:
-
Dispatch Source
- dispatch_source_t
- dispatch_source_create
- dispatch_source_set_event_handler
- dispatch_source_resume
- dispatch_source_cancel
- dispatch_source_set_timer
dispatch_source
GCD中除了主要的Dispatch Queue外,還有不太引人注目的Dispatch Source晌块。它是BSD系內(nèi)核慣有功能kqueue的包裝爱沟。
kqueue 是在XNU內(nèi)核中發(fā)生各種事件時,在應(yīng)用程序編程方執(zhí)行處理的技術(shù)匆背。其CPU負(fù)荷非常小呼伸,盡量不占用資源。kqueue 可以說是應(yīng)用程序處理XNU內(nèi)核中發(fā)生的各種事件的方法中最優(yōu)秀的一種钝尸。
Dispatch Source可處理以下事件括享。如表所示。
名稱 | 內(nèi)容 |
---|---|
DISPATCH_SOURCE_TYPE_DATA_ADD | 變量增加 |
DISPATCH_SOURCE_TYPE_DATA_OR | 變量OR |
DISPATCH_SOURCE_TYPE_MACH_SEND | MACH端口發(fā)送 |
DISPATCH_SOURCE_TYPE_MACH_RECV | MACH端口接收 |
DISPATCH_SOURCE_TYPE_READ | 可讀取文件映像 |
DISPATCH_SOURCE_TYPE_WRITE | 可寫入文件映像 |
DISPATCH_SOURCE_TYPE_PROC | 監(jiān)測到與進(jìn)程相關(guān)的事件 |
DISPATCH_SOURCE_TYPE_SIGNAL | 接收信號 |
DISPATCH_SOURCE_TYPE_TIMER | 定時器 |
DISPATCH_SOURCE_TYPE_VNODE | 文件系統(tǒng)有變更 |
事件發(fā)生時珍促,在指定的Dispatch Queue中可執(zhí)行事件的處理铃辖。
下面我們使用DISPATCH_SOURCE_TYPE_READ
,異步讀取文件映像猪叙。
const char *fileName = "文件地址";
__block ssize_t total = 0;
/*
* 打開文件娇斩,獲取文件描述符(open成功則返回文件描述符,否則返回-1)
*/
int fd = open(fileName, O_RDWR);
if (fd == -1) return;
/*
* 設(shè)定為異步映像
*/
fcntl(fd, F_SETFL, O_NONBLOCK);
/*
* 獲取用于追加事件處理的 Global Dispatch Queue
*/
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/*
* 基于READ事件作成Dispatch Source
*/
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, queue);
if (source == NULL) {
close(fd);
return;
}
/*
* 指定發(fā)生READ事件時執(zhí)行的處理
*/
dispatch_source_set_event_handler(source, ^{
/*
* 預(yù)估要讀取的字節(jié)數(shù)
*
* dispatch_source_get_data()函數(shù)的返回值
* 要根據(jù)dispatch_source_create()創(chuàng)建source時所選的類型而定穴翩,
*
* DISPATCH_SOURCE_TYPE_READ: estimated bytes available to read
*
* DISPATCH_SOURCE_TYPE_TIMER: number of times the timer has
* fired since the last handler invocation
*/
size_t estimatedSize = dispatch_source_get_data(source);
/*
* 這個buff就是向堆申請的一塊內(nèi)存犬第,用來暫時緩存文件映像
* 參數(shù)是你要申請的內(nèi)存大小,使用dispatch_source_get_data獲取到的
* 是整個文件的大小芒帕,也可以一段一段讀取瓶殃,將參數(shù)寫成
* size_t estimatedSize = 1024;
* 這個1024可以根據(jù)項目需要定義成其它數(shù)字,如100副签、1000、10000...
*/
void *buff = malloc(estimatedSize);
if (buff) {
/*
* 從映像中讀取
*/
ssize_t length = read(fd, buff, estimatedSize);
total += length;
/*
* buff的處理
*
* buff可以讀取任何文件基矮,包括多媒體文件淆储。通過NSData可進(jìn)行轉(zhuǎn)換。
*
* NSLog有個系統(tǒng)bug家浇,打印不完整本砰。printf可以打印完整。UITextView也可以呈現(xiàn)完整钢悲。
*/
NSData *data = [NSData dataWithBytes:buff length:length];
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
// printf("%s\n=============\n", [text UTF8String]);
dispatch_async(dispatch_get_main_queue(), ^{
UITextView *textView = [[UITextView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:textView];
textView.text = text;
});
free(buff);
/*
* 此處我設(shè)置的buff緩存區(qū)大小正好就是文件大小点额,
* 所以直接調(diào)用 dispatch_source_cancel(source) 讓它直接結(jié)束舔株。
* 因為這次讀取的就是全部,不必再循環(huán)調(diào)用方法去讀取了还棱。
*/
dispatch_source_cancel(source);
}
});
dispatch_source_set_cancel_handler(source, ^{
close(fd);
NSLog(@"%zd", total);
});
/*
* 啟動Dispatch Source
*/
dispatch_resume(source);
上面這段代碼是參考了《Objective-C高級編程》和網(wǎng)上各位大神的博文载慈,我做了整合后整理出的demo。
《Objective-C高級編程》書中的代碼片段是一個大致思想珍手,而各類博文中大多將方法拆分办铡,看起來很高深(本人水平有限??,看網(wǎng)上大神的文章好多時候都是直撓頭)琳要。用網(wǎng)上的源代碼在自己的工程里運行的時候寡具,有很多NSLog
都是null
,而按照《Objective-C高級編程》中的方式雖然沒有null
稚补,但是打印不全童叠。于是找到了問題所在,不是代碼有問題课幕,而是NSLog
本身存在問題:大段的字符串打印不完全厦坛。使用printf
代替就會打印完整,而我用了一個更直觀的替代方式撰豺,把字符串用UITextView
展示出來粪般,這樣就很容易看了。
順便我去蘋果官方文檔看了一下dispatch_source_get_data
函數(shù)究竟是怎么回事污桦,這是原文鏈接亩歹,我在上面的源碼中帶了注釋,dispatch_source_get_data
函數(shù)的用途或者說返回值要視情況而定凡橱,這要看你創(chuàng)建dispatch_source_t
的時候選了什么參數(shù)類型小作,就是下面這句中的DISPATCH_SOURCE_TYPE_READ
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, queue);
使用DISPATCH_SOURCE_TYPE_READ
創(chuàng)建source時,dispatch_source_get_data
函數(shù)的返回值就是 estimated bytes available to read 即預(yù)估的可讀取字節(jié)數(shù)稼钩。但是我一開始還是用的迷糊了顾稀,因為dispatch_source_get_data
函數(shù)的調(diào)用時機不能太隨意,一定要在dispatch_source_set_event_handler
函數(shù)的回調(diào)中才會有效坝撑【哺眩《Objective-C高級編程》是這樣寫的代碼片段,而我自己實踐后發(fā)現(xiàn)確實是這樣??巡李!
這里面還有一個坑就是buff抚笔,實際上buff代表的是你申請的一塊堆內(nèi)存,而buff本身是這塊內(nèi)存的首地址侨拦。讓人迷糊的就是dispatch_source_set_event_handler
函數(shù)的block自身會循環(huán)多次調(diào)用殊橙,直到你強制停止為止。
當(dāng)buff申請的大小正好是文件大小的時候,
dispatch_source_set_event_handler
函數(shù)不會重復(fù)執(zhí)行膨蛮,只執(zhí)行一次就會結(jié)束叠纹。當(dāng)buff申請的大小比文件小的時候,就會不停的循環(huán)調(diào)用敞葛,直到所有文件內(nèi)存讀取完畢才會停止調(diào)用誉察,或者你提前直接調(diào)用
dispatch_source_cancel
強制其停止。
下面我們使用DISPATCH_SOURCE_TYPE_TIMER
實現(xiàn)一個定時器
// 我假裝搞了一個每秒執(zhí)行一次制肮,執(zhí)行10次就停止的定時器
static dispatch_source_t timer;
static int count = 0;
- (void)startTimer {
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
/*
* 設(shè)置定時器
* 第一個參數(shù):定時器(dispatch_source_t)
* 第二個參數(shù):定時器時間起點
* 第三個參數(shù):定時器時間間隔
* 第四個參數(shù):定時器允許延遲執(zhí)行的時間
*/
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC, 0ull * NSEC_PER_SEC);
/*
* 指定定時器啟動后要執(zhí)行的任務(wù)
*/
dispatch_source_set_event_handler(timer, ^{
if (count++ < 10) {
NSLog(@"定時器任務(wù)");
}
else {
dispatch_source_cancel(timer);
}
});
/*
* 指定取消 Dispatch Source 時的處理
*/
dispatch_source_set_cancel_handler(timer, ^{
NSLog(@"定時器結(jié)束咯冒窍!");
});
dispatch_resume(timer);
}
相對于文件映像的異步讀取來說,定時器就變得簡單的很了豺鼻,我能想到的都寫在源碼的注釋里了综液,就不多說了。
看了異步讀取文件映像用的源代碼和這個定時器用的源代碼后儒飒,有沒有注意到什么呢?實際上Dispatch Queue沒有“取消”這一概念谬莹。一旦將處理追加到Dispatch Queue中,就沒有方法可將該處理去除桩了,也沒有方法可在執(zhí)行中取消該處理附帽。編程人員要么在處理中導(dǎo)入取消這一概念,要么放棄取消井誉,或者使用NSOperationQueue等其他方法蕉扮。
Dispatch Source與Dispatch Queue不同,是可以取消的颗圣。而且取消時必須執(zhí)行的處理可指定為回調(diào)用的Block形式喳钟。因此使用Dispatch Source實現(xiàn)XNU內(nèi)核中發(fā)生的事件處理要比直接使用kqueue 實現(xiàn)更為簡單。在必須使用kqueue的情況下希望大家還是使用Dispatch Source,它比較簡單在岂。
那么Dispatch Source 與 Dispatch Queue 兩者在線程執(zhí)行上的是什么關(guān)系奔则?
答案是:沒有關(guān)系。兩者會獨立運行蔽午。 Dispatch Queue 像一個生產(chǎn)任務(wù)的生產(chǎn)者易茬,而 Dispatch Source 像處理任務(wù)的消費者〖袄希可以一邊異步生產(chǎn)抽莱,也可一邊異步消費。你可以在任意線程上調(diào)用 dispatch_source_merge_data
以觸發(fā) dispatch_source_set_event_handler
骄恶。而句柄的執(zhí)行線程岸蜗,取決于你創(chuàng)建句柄時所指定的線程,如果你像下面這樣創(chuàng)建叠蝇,那么句柄會在主線程執(zhí)行:
下面我們介紹DISPATCH_SOURCE_TYPE_DATA_ADD
相關(guān)用法
分派源提供了高效的方式來處理事件。首先注冊事件處理程序,事件發(fā)生時會收到通知悔捶。如果在系統(tǒng)還沒有來得及通知你之前事件就發(fā)生了多次铃慷,那么這些事件會被合并為一個事件。這對于底層的高性能代碼很有用蜕该,但是OS應(yīng)用開發(fā)者很少會用到這樣的功能犁柜。類似地,分派源可以響應(yīng)UNIX信號堂淡、文件系統(tǒng)的變化馋缅、其他進(jìn)程的變化以及Mach Port事件。它們中很多都在Mac系統(tǒng)上很有用绢淀,但是iOS開發(fā)者通常不會用到萤悴。
不過,自定義源在iOS中很有用皆的,尤其是在性能至關(guān)重要的場合進(jìn)行進(jìn)度反饋覆履。使用時,首先創(chuàng)建一個源:自定義源累積事件中傳遞過來的值费薄。累積方式可以是相加(DISPATCH_SOURCE_TYPE_DATA_ADD
), 也可以是邏輯或(DISPATCH_SOURCE_DATA_OR
)硝全。自定義源也需要一個隊列,用來處理所有的響應(yīng)處理塊楞抡。
創(chuàng)建源后伟众,需要提供相應(yīng)的處理方法。當(dāng)源生效時會分派注冊處理方法;當(dāng)事件發(fā)生時會分派事件處理方法;當(dāng)源被取消時會分派取消處理方法召廷。
在同一時間凳厢,只有一個處理方法塊的實例被分派。如果這個處理方法還沒有執(zhí)行完畢柱恤,另一個事件就發(fā)生了数初,事件會以指定方式(ADD或者OR)進(jìn)行累積。通過合并事件的方式梗顺,系統(tǒng)即使在高負(fù)載情況下也能正常工作泡孩。當(dāng)處理事件件被最終執(zhí)行時,計算后的數(shù)據(jù)可以通過 dispatch_source_get_data
來獲取寺谤。這個數(shù)據(jù)的值在每次響應(yīng)事件執(zhí)行后會被重置仑鸥。
讓GCD像OperationQueue那樣暫停任務(wù)
demo的原作者微博@iOS程序犭袁
- (void)viewDidLoad {
//1.
// 指定DISPATCH_SOURCE_TYPE_DATA_ADD,做成Dispatch Source(分派源)变屁。設(shè)定Main Dispatch Queue 為追加處理的Dispatch Queue
_processingQueueSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0,dispatch_get_main_queue());
__block NSUInteger totalComplete = 0;
dispatch_source_set_event_handler(_processingQueueSource, ^{
//當(dāng)處理事件被最終執(zhí)行時眼俊,dispatch_source_get_data獲取的值是dispatch_source_merge_data第二個參數(shù)傳過來的值。這個數(shù)據(jù)的值在每次響應(yīng)事件執(zhí)行后會被重置粟关,所以totalComplete的值是最終累積的值疮胖。
NSUInteger value = dispatch_source_get_data(self->_processingQueueSource);
totalComplete += value;
NSLog(@"進(jìn)度:%.2f", totalComplete/1000.0);
});
//分派源創(chuàng)建時默認(rèn)處于暫停狀態(tài),在分派源分派處理程序之前必須先恢復(fù)。
dispatch_resume(_processingQueueSource);
self.running = YES;
//2.
//恢復(fù)源后澎灸,就可以通過dispatch_source_merge_data向Dispatch Source(分派源)發(fā)送事件:
_queue = dispatch_queue_create("com.example", DISPATCH_QUEUE_SERIAL);
for (NSUInteger index = 0; index < 1000; index++) {
dispatch_async(_queue, ^{
if (!self.running) {
return;
}
// 調(diào)用dispatch_source_merge_data以觸發(fā)dispatch_source_set_event_handler回調(diào)
dispatch_source_merge_data(self->_processingQueueSource, 1);
usleep(10000);//0.01秒
});
}
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
[self changeStatus:self.running];
}
- (void)changeStatus:(BOOL)shouldPause {
if (shouldPause) {
[self pause];
} else {
[self resume];
}
}
- (void)resume {
if (self.running) {
return;
}
NSLog(@"?恢復(fù)Dispatch Source(分派源)以及_queue");
self.running = YES;
dispatch_resume(_processingQueueSource);
if (_queue) {
dispatch_resume(_queue);
}
}
- (void)pause {
if (!self.running) {
return;
}
NSLog(@"??暫停Dispatch Source(分派源)以及_queue");
self.running = NO;
dispatch_suspend(_processingQueueSource);
dispatch_suspend(_queue);
}
運行結(jié)果:
且慢院塞,寡人有一問
原作者微博@iOS程序犭袁在給隊列添加任務(wù)的時候,使用的是串行隊列性昭。為什么不使用并發(fā)隊列而是使用串行隊列呢拦止?
之前的《iOS GCD全析(四)》說過,dispatch_suspend
可以把GCD的隊列(queue)和派發(fā)源(source)掛起糜颠。但是掛起隊列有個問題就是即便調(diào)用dispatch_suspend
汹族,任務(wù)也不會立即停止,在調(diào)用dispatch_suspend
之前已經(jīng)開始執(zhí)行的任務(wù)不會受到影響其兴,仍然會繼續(xù)執(zhí)行顶瞒。而在調(diào)用dispatch_suspend
之后還未開始執(zhí)行的任務(wù)會受到影響,暫時不執(zhí)行忌警,進(jìn)入暫停狀態(tài)搁拙。
我們知道,串行隊列中任務(wù)的特點就是隊列中的任務(wù)會一個接一個執(zhí)行法绵,而并發(fā)隊列中的任務(wù)在異步添加后會并發(fā)執(zhí)行箕速。demo中 for
循環(huán)1000次,也就是向串行隊列添加了1000個任務(wù)朋譬,這1000個任務(wù)要一個接一個執(zhí)行盐茎。
再看demo中的暫停方法 - (void)pause
,方法中先掛起了source
dispatch_suspend(_processingQueueSource);
然后再掛起了queue徙赢。這樣在暫停的時候字柠,就是先讓source的回調(diào)先停下來,
dispatch_source_set_event_handler {
...
}
再讓queue中的任務(wù)停下來狡赐。微博@iOS程序犭袁使用串行隊列的好處就在這里窑业,在隊列掛起后,因為 dispatch_suspend
不能約束已經(jīng)開始執(zhí)行的任務(wù)枕屉,所以最多有一個任務(wù)在執(zhí)行中常柄,也就是說 “不聽 dispatch_suspend
使喚的任務(wù)” 最多只有這一個尚在執(zhí)行中的任務(wù)。
在恢復(fù)方法- (void)resume
中搀擂,則是先恢復(fù)了source西潘,然后恢復(fù)了queue。這樣一來哨颂,就在source已經(jīng)恢復(fù)而queue還未恢復(fù)的這個時機喷市,source的 dispatch_source_set_event_handler
回調(diào)有時間去處理那個 “不聽 dispatch_suspend
使喚的任務(wù)” 那一次調(diào)用的 dispatch_source_merge_data
,再讓queue中的任務(wù)繼續(xù)威恼。如此一來品姓,隊列queue和派發(fā)源source的暫停與恢復(fù)就會在表面上看起來真正的實現(xiàn)了寝并,沒有什么問題。
但是假如我們換成并發(fā)隊列缭黔,情況就會變得很不一樣食茎。在調(diào)用 dispatch_suspend
想要暫停時,由于多個任務(wù)并發(fā)調(diào)用 dispatch_source_merge_data
馏谨,這就造成 “不聽 dispatch_suspend
使喚的任務(wù)” 會有很多,而 dispatch_source_set_event_handler
回調(diào)又先行暫停附迷,不能處理惧互,這些任務(wù)調(diào)用的 dispatch_source_merge_data
就會先積壓下。
在恢復(fù)的時候喇伯,dispatch_source_set_event_handler
回調(diào)會先去處理積壓的 dispatch_source_merge_data
調(diào)用喊儡,這些調(diào)用會被合并,只執(zhí)行一次稻据。這樣一來艾猜,在一次暫停繼而恢復(fù)后,打印的進(jìn)度就會缺失一些捻悯,因為有些被合并執(zhí)行了匆赃。
細(xì)心的讀者可能已經(jīng)能發(fā)現(xiàn)了,如果換成并發(fā)隊列今缚,即便是不暫停算柳,也一樣會造成打印的進(jìn)度有缺失的情況。因為多個 dispatch_source_merge_data
一起調(diào)用姓言,根據(jù)source的特點瞬项,一定會被合并執(zhí)行。
下面是換成并發(fā)隊列的打印結(jié)果:
進(jìn)度:0.128
進(jìn)度:0.131
進(jìn)度:0.133
進(jìn)度:0.137
進(jìn)度:0.138
進(jìn)度:0.139
進(jìn)度:0.140
進(jìn)度:0.141
進(jìn)度:0.142
進(jìn)度:0.152
...
進(jìn)度:0.955
進(jìn)度:0.957
進(jìn)度:0.960
進(jìn)度:0.962
進(jìn)度:0.966
進(jìn)度:0.967
進(jìn)度:1.000