現代系統通常提供異步接口,允許應用向系統提交請求,然后在系統處理請求時應用可以繼續處理自己的事情。Grand Central Dispatch
正是基于這個基本行為而設計,允許你提交請求,并通過block
和dispatch queue
報告結果。
dispatch source
是基礎數據類型,協調特定底層系統事件的處理。Grand Central Dispatch
支持以下dispatch source
:
- Timer dispatch source:定期產生通知
- Signal dispatch source:UNIX信號到達時產生通知
- Descriptor dispatch source:各種文件和socket操作的通知
- 數據可讀
- 數據可寫
- 文件在文件系統中被刪除、移動、重命名
- 文件元數據信息改變
Process dispatch source:進程相關的事件通知
- 當進程退出時
- 當進程發起fork或exec等調用
- 信號被遞送到進程
- Mach port dispatch source:Mach相關事件的通知
- Custom dispatch source:你自己定義并自己觸發
Dispatch source
替代了異步回調函數,來處理系統相關的事件。當你配置一個dispatch source
時,你指定要監測的事件、dispatch queue
、以及處理事件的代碼(block或函數
)。當事件發生時,dispatch source
會提交你的block
或函數到指定的queue
去執行
和手工提交到queue
的任務不同,dispatch source
為應用提供連續的事件源。除非你顯式地取消,dispatch source
會一直保留與dispatch queue
的關聯。只要相應的事件發生,就會提交關聯的代碼到dispatch queue
去執行。
為了防止事件積壓到dispatch queue
,dispatch source
實現了事件合并機制。如果新事件在上一個事件處理器出列并執行之前到達,dispatch source
會將新舊事件的數據合并。根據事件類型的不同,合并操作可能會替換舊事件,或者更新舊事件的信息。
創建Dispatch Source
創建dispatch source
需要同時創建事件源和dispatch source
本身。事件源是處理事件所需要的native數據結構,例如基于描述符的dispatch source
,你需要打開描述符;基于進程的事件,你需要獲得目標程序的進程ID。
然后可以如下創建相應的dispatch source:
使用
dispatch_source_create
函數創建dispatch source
配置dispatch source
:
為dispatch source
設置一個事件處理器
對于定時器源,使用 dispatch_source_set_timer 函數設置定時器信息
為dispatch source
賦予一個取消處理器(可選)調用 dispatch_resume
函數開始處理事件由于dispatch source
必須進行額外的配置才能被使用,dispatch_source_create
函數返回的dispatch source
將處于掛起狀態。此時dispatch source
會接收事件,但是不會進行處理。這時候你可以安裝事件處理器,并執行額外的配置。
編寫和安裝一個事件處理器
你需要定義一個事件處理器來處理事件,可以是函數或block對象,并使用 dispatch_source_set_event_handler
或 dispatch_source_set_event_handler_f
安裝事件處理器。事件到達時,dispatch source
會提交你的事件處理器到指定的dispatch queue
,由queue
執行事件處理器。
事件處理器的代碼負責處理所有到達的事件。如果事件處理器已經在queue
中并等待處理已經到達的事件,如果此時又來了一個新事件,dispatch source
會合并這兩個事件。事件處理器通常只能看到最新事件的信息,不過某些類型的dispatch source
也能獲得已經發生以及合并的事件信息。
如果事件處理器已經開始執行,一個或多個新事件到達,dispatch source
會保留這些事件,直到前面的事件處理器完成執行。然后以新事件再次提交處理器到queue
。
函數事件處理器有一個context
指針指向dispatch source
對象,沒有返回值。Block
事件處理器沒有參數,也沒有返回值。
// Block-based event handler
void (^dispatch_block_t)(void)
// Function-based event handler
void (dispatch_function_t)(void )
在事件處理器中,你可以從dispatch source
中獲得事件的信息,函數處理器可以直接使用參數指針,Block
則必須自己捕獲到dispatch source
指針,一般block定義時會自動捕獲到外部定義的所有變量。
disatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
myDescriptor, 0, myQueue);
dispatch_source_set_event_handler(source, ^{
// Get some data from the source variable, which is captured
// from the parent context.
size_t estimated = dispatch_source_get_data(source);
// Continue reading the descriptor…
});
dispatch_resume(source);
Block
捕獲外部變量允許更大的靈活性和動態性。當然,在Block
中這些變量默認是只讀的,雖然可以使用__block
來修改捕獲的變量,但是你最好不要在事件處理器中這樣做。因為Dispatch source
異步執行事件處理器,當事件處理器修改原始外部變量時,有可能這些變量已經不存在了。
下面是事件處理器能夠獲得的事件信息:
- 函數 描述
dispatch_source_get_handle
這個函數返回dispatch source
管理的底層系統數據類型。
- 對于描述符
dispatch source
,函數返回一個int
,表示關聯的描述符 - 對于信號
dispatch source
,函數返回一個int
,表示最新事件的信號數值 - 對于進程
dispatch source
,函數返回一個pid_t
數據結構,表示被監控的進程 - 對于
Mach port dispatch source
,函數返回一個mach_port_t
數據結構 - 對于其它
dispatch source
,函數返回的值未定義
dispatch_source_get_data
這個函數返回事件關聯的所有未決數據。 - 對于從文件中讀取數據的描述符
dispatch source
,這個函數返回可以讀取的字節數 - 對于向文件中寫入數據的描述符
dispatch source
,如果可以寫入,則返回正數值 - 對于監控文件系統活動的描述符
dispatch source
,函數返回一個常量,表示發生的事件類型,參考dispatch_source_vnode_flags_t
枚舉類型 - 對于進程
dispatch source
,函數返回一個常量,表示發生的事件類型,參考dispatch_source_proc_flags_t
枚舉類型 - 對于
Mach port dispatch source
,函數返回一個常量,表示發生的事件類型,參考dispatch_source_machport_flags_t
枚舉類型 - 對于自定義
dispatch source
,函數返回從現有數據創建的新數據,以及傳遞給dispatch_source_merge_data
函數的新數據。
dispatch_source_get_mask
這個函數返回用來創建dispatch source的事件標志 - 對于進程
dispatch source
,函數返回dispatch source
接收到的事件掩碼,參考dispatch_source_proc_flags_t
枚舉類型 - 對于發送權利的
Mach port dispatch source
,函數返回期望事件的掩碼,參考dispatch_source_mach_send_flags_t
枚舉類型 - 對于自定義 “或” 的
dispatch source
,函數返回用來合并數據值的掩碼。
安裝一個取消處理器
取消處理器在dispatch soruce
釋放之前執行清理工作。多數類型的dispatch source
不需要取消處理器,除非你對dispatch source
有自定義行為需要在釋放時執行。但是使用描述符或Mach port
的dispatch source
必須設置取消處理器,用來關閉描述符或釋放Mach port
。否則可能導致微妙的bug
,這些結構體會被系統其它部分或你的應用在不經意間重用。
你可以在任何時候安裝取消處理器,但通常我們在創建dispatch source
時就會安裝取消處理器。使用 dispatch_source_set_cancel_handler
或 dispatch_source_set_cancel_handler_f
函數來設置取消處理器。
下面取消處理器關閉描述符:
dispatch_source_set_cancel_handler(mySource, ^{
close(fd); // Close a file descriptor opened earlier.
});
修改目標Queue
在創建dispatch source
時可以指定一個queue
,用來執行事件處理器和取消處理器。不過你也可以使用 dispatch_set_target_queue
函數在任何時候修改目標queue
。修改queue可以改變執行dispatch source
事件的優先級。
修改dispatch source
的目標queue
是異步操作,dispatch source
會盡可能快地完成這個修改。如果事件處理器已經進入queue
并等待處理,它會繼續在原來的Queue
中執行。隨后到達的所有事件的處理器都會在后面修改的queue
中執行。
關聯自定義數據到dispatch source
和Grand Central Dispatch
的其它類型一樣,你可以使用 dispatch_set_context
函數關聯自定義數據到dispatch source
。使用context
指針存儲事件處理器需要的任何數據。如果你在context
指針中存儲了數據,你就應該安裝一個取消處理器,在dispatch source
不再需要時釋放這些context
自定義數據。
如果你使用block
實現事件處理器,你也可以捕獲本地變量,并在Block
中使用。雖然這樣也可以代替context
指針,但是你應該明智地使用Block捕獲變量。因為dispatch source
長時間存在于應用中,Block
捕獲指針變量時必須非常小心,因為指針指向的數據可能會被釋放,因此需要復制數據或retain
。不管使用哪種方法,你都應該提供一個取消處理器,在最后釋放這些數據。
Dispatch Source的內存管理
Dispatch Source
也是引用計數的數據類型,初始計數為1,可以使用dispatch_retain
和 dispatch_release
函數來增加和減少引用計數。引用計數到達0時,系統自動釋放dispatch source
數據結構。
dispatch source
的所有權可以由dispatch source
內部或外部進行管理。外部所有權時,另一個對象擁有dispatch source
,并負責在不需要時釋放它。內部所有權時,dispatch source
自己擁有自己,并負責在適當的時候釋放自己。雖然外部所有權很常用,當你希望創建自主dispatch source
,并讓它自己管理自己的行為時,可以使用內部所有權。例如dispatch source
應用單一全局事件時,可以讓它自己處理該事件,并立即退出。
Dispatch Source示例
創建一個定時器
定時器dispatch source
定時產生事件,可以用來發起定時執行的任務,如游戲或其它圖形應用,可以使用定時器來更新屏幕或動畫。你也可以設置定時器,并在固定間隔事件中檢查服務器的新信息。
所有定時器dispatch source
都是間隔定時器,一旦創建,會按你指定的間隔定期遞送事件。你需要為定時器dispatch source
指定一個期望的定時器事件精度,也就是leeway值,讓系統能夠靈活地管理電源并喚醒內核。例如系統可以使用leeway值來提前或延遲觸發定時器,使其更好地與其它系統事件結合。創建自己的定時器時,你應該盡量指定一個leeway
值。
就算你指定leeway
值為0,也不要期望定時器能夠按照精確的納秒來觸發事件。系統會盡可能地滿足你的需求,但是無法保證完全精確的觸發時間。
當計算機睡眠時,定時器dispatch source
會被掛起,稍后系統喚醒時,定時器dispatch source
也會自動喚醒。根據你提供的配置,暫停定時器可能會影響定時器下一次的觸發。如果定時器dispatch source
使用 dispatch_time
函數或DISPATCH_TIME_NOW
常量設置,定時器dispatch source
會使用系統默認時鐘來確定何時觸發,但是默認時鐘在計算機睡眠時不會繼續。
如果你使用dispatch_walltime
函數來設置定時器dispatch source
,則定時器會根據掛鐘時間來跟蹤,這種定時器比較適合觸發間隔相對比較大的場合,可以防止定時器觸發間隔出現太大的誤差。
下面是定時器dispatch source
的一個例子,每30秒觸發一次,leeway
值為1,因為間隔相對較大,使用 dispatch_walltime
來創建定時器。定時器會立即觸發第一次,隨后每30秒觸發一次。MyPeriodicTask
和 MyStoreTimer
是自定義函數,用于實現定時器的行為,并存儲定時器到應用的數據結構。
dispatch_source_t CreateDispatchTimer(uint64_t interval,
uint64_t leeway,
dispatch_queue_t queue,
dispatch_block_t block)
{
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
0, 0, queue);
if (timer)
{
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
dispatch_source_set_event_handler(timer, block);
dispatch_resume(timer);
}
return timer;
}
void MyCreateTimer()
{
dispatch_source_t aTimer = CreateDispatchTimer(30ull * NSEC_PER_SEC,
1ull * NSEC_PER_SEC,
dispatch_get_main_queue(),
^{ MyPeriodicTask(); });
// Store it somewhere for later use.
if (aTimer)
{
MyStoreTimer(aTimer);
}
}
雖然定時器dispatch source
是接收時間事件的主要方法,你還可以使用其它選擇。如果想在指定時間間隔后執行一個block
,可以使用 dispatch_after
或 dispatch_after_f
函數。這兩個函數非常類似于dispatch_async
,但是只允許你指定一個時間值,時間一到就自動提交block
到queue
中執行,時間值可以指定為相對或絕對時間。
從描述符中讀取數據
要從文件或socket中讀取數據,需要打開文件或socket
,并創建一個 DISPATCH_SOURCE_TYPE_READ
類型的dispatch source
。你指定的事件處理器必須能夠讀取和處理描述符中的內容。對于文件,需要讀取文件數據,并為應用創建適當的數據結構;對于網絡socket
,需要處理最新接收到的網絡數據。
讀取數據時,你總是應該配置描述符使用非阻塞操作,雖然你可以使用dispatch_source_get_data
函數查看當前有多少數據可讀,但在你調用它和實際讀取數據之間,可用的數據數量可能會發生變化。如果底層文件被截斷,或發生網絡錯誤,從描述符中讀取會阻塞當前線程,停止在事件處理器中間并阻止dispatch queue
去執行其它任務。對于串行queue
,這樣還可能會死鎖,即使是并發queue,也會減少queue
能夠執行的任務數量。
下面例子配置dispatch source
從文件中讀取數據,事件處理器讀取指定文件的全部內容到緩沖區,并調用一個自定義函數來處理這些數據。調用方可以使用返回的dispatch source
在讀取操作完成之后,來取消這個事件。為了確保dispatch queue
不會阻塞,這里使用了fcntl
函數,配置文件描述符執行非阻塞操作。dispatch source
安裝了取消處理器,確保最后關閉了文件描述符。
dispatch_source_t ProcessContentsOfFile(const char* filename)
{
// Prepare the file for reading.
int fd = open(filename, O_RDONLY);
if (fd == -1)
return NULL;
fcntl(fd, F_SETFL, O_NONBLOCK); // Avoid blocking the read operation
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
fd, 0, queue);
if (!readSource)
{
close(fd);
return NULL;
}
// Install the event handler
dispatch_source_set_event_handler(readSource, ^{
size_t estimated = dispatch_source_get_data(readSource) + 1;
// Read the data into a text buffer.
char* buffer = (char*)malloc(estimated);
if (buffer)
{
ssize_t actual = read(fd, buffer, (estimated));
Boolean done = MyProcessFileData(buffer, actual); // Process the data.
// Release the buffer when done.
free(buffer);
// If there is no more data, cancel the source.
if (done)
dispatch_source_cancel(readSource);
}
});
// Install the cancellation handler
dispatch_source_set_cancel_handler(readSource, ^{close(fd);});
// Start reading the file.
dispatch_resume(readSource);
return readSource;
}
在這個例子中,自定義的 MyProcessFileData
函數確定讀取到足夠的數據,返回YES
告訴dispatch source
讀取已經完成,可以取消任務。通常讀取描述符的dispatch source
在還有數據可讀時,會重復調度事件處理器。如果socket
連接關閉或到達文件末尾,dispatch source
自動停止調度事件處理器。如果你自己確定不再需要dispatch source
,也可以手動取消它。
向描述符寫入數據
向文件或socket
寫入數據非常類似于讀取數據,配置描述符為寫入操作后,創建一個DISPATCH_SOURCE_TYPE_WRITE
類型的dispatch source
,創建好之后,系統會調用事件處理器,讓它開始向文件或socket寫入數據。當你完成寫入后,使用 dispatch_source_cancel
函數取消dispatch source
。
寫入數據也應該配置文件描述符使用非阻塞操作,雖然 dispatch_source_get_data
函數可以查看當前有多少可用寫入空間,但這個值只是建議性的,而且在你執行寫入操作時可能會發生變化。如果發生錯誤,寫入數據到阻塞描述符,也會使事件處理器停止在執行中途,并阻止dispatch queue
執行其它任務。串行queue
會產生死鎖,并發queue
則會減少能夠執行的任務數量。
下面是使用dispatch source
寫入數據到文件的例子,創建文件后,函數傳遞文件描述符到事件處理器。MyGetData
函數負責提供要寫入的數據,在數據寫入到文件之后,事件處理器取消dispatch source
,阻止再次調用。此時dispatch source的擁有者需負責釋放dispatch source
。
dispatch_source_t WriteDataToFile(const char* filename)
{
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC,
(S_IRUSR | S_IWUSR | S_ISUID | S_ISGID));
if (fd == -1)
return NULL;
fcntl(fd, F_SETFL); // Block during the write.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE,
fd, 0, queue);
if (!writeSource)
{
close(fd);
return NULL;
}
dispatch_source_set_event_handler(writeSource, ^{
size_t bufferSize = MyGetDataSize();
void* buffer = malloc(bufferSize);
size_t actual = MyGetData(buffer, bufferSize);
write(fd, buffer, actual);
free(buffer);
// Cancel and release the dispatch source when done.
dispatch_source_cancel(writeSource);
});
dispatch_source_set_cancel_handler(writeSource, ^{close(fd);});
dispatch_resume(writeSource);
return (writeSource);
}
監控文件系統對象
如果需要監控文件系統對象的變化,可以設置一個 DISPATCH_SOURCE_TYPE_VNODE
類型的dispatch source
,你可以從這個dispatch source
中接收文件刪除、寫入、重命名等通知。你還可以得到文件的特定元數據信息變化通知。
在dispatch source
正在處理事件時,dispatch source
中指定的文件描述符必須保持打開狀態。
下面例子監控一個文件的文件名變化,并在文件名變化時執行一些操作(自定義的 MyUpdateFileName
函數)。由于文件描述符專門為dispatch source
打開,dispatch source
安裝了取消處理器來關閉文件描述符。這個例子中的文件描述符關聯到底層的文件系統對象,因此同一個dispatch source
可以用來檢測多次文件名變化。
dispatch_source_t MonitorNameChangesToFile(const char* filename)
{
int fd = open(filename, O_EVTONLY);
if (fd == -1)
return NULL;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE,
fd, DISPATCH_VNODE_RENAME, queue);
if (source)
{
// Copy the filename for later use.
int length = strlen(filename);
char* newString = (char*)malloc(length + 1);
newString = strcpy(newString, filename);
dispatch_set_context(source, newString);
// Install the event handler to process the name change
dispatch_source_set_event_handler(source, ^{
const char* oldFilename = (char*)dispatch_get_context(source);
MyUpdateFileName(oldFilename, fd);
});
// Install a cancellation handler to free the descriptor
// and the stored string.
dispatch_source_set_cancel_handler(source, ^{
char* fileStr = (char*)dispatch_get_context(source);
free(fileStr);
close(fd);
});
// Start processing events.
dispatch_resume(source);
}
else
close(fd);
return source;
}
監測信號
應用可以接收許多不同類型的信號,如不可恢復的錯誤(非法指令)、或重要信息的通知(如子進程退出)。傳統編程中,應用使用 sigaction
函數安裝信號處理器函數,信號到達時同步處理信號。如果你只是想信號到達時得到通知,并不想實際地處理該信號,可以使用信號dispatch source
來異步處理信號。
信號dispatch source
不能替代 sigaction
函數提供的同步信號處理機制。同步信號處理器可以捕獲一個信號,并阻止它中止應用。而信號dispatch source
只允許你監測信號的到達。此外,你不能使用信號dispatch source
獲取所有類型的信號,如SIGILL, SIGBUS, SIGSEGV
信號。
由于信號dispatch source
在dispatch queue
中異步執行,它沒有同步信號處理器的一些限制。例如信號dispatch source
的事件處理器可以調用任何函數。靈活性增大的代價是,信號到達和dispatch source
事件處理器被調用的延遲可能會增大。
下面例子配置信號dispatch source
來處理SIGHUP
信號,事件處理器調用 MyProcessSIGHUP
函數,用來處理信號。
void InstallSignalHandler()
{
// Make sure the signal does not terminate the application.
signal(SIGHUP, SIG_IGN);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGHUP, 0, queue);
if (source)
{
dispatch_source_set_event_handler(source, ^{
MyProcessSIGHUP();
});
// Start processing signals
dispatch_resume(source);
}
}
監控進程
進程dispatch source
可以監控特定進程的行為,并適當地響應。父進程可以使用dispatch source
來監控自己創建的所有子進程,例如監控子進程的死亡;類似地,子進程也可以使用dispatch source
來監控父進程,例如在父進程退出時自己也退出。
下面例子安裝了一個進程dispatch source
,監控父進程的終止。當父進程退出時,dispatch source
設置一些內部狀態信息,告知子進程自己應該退出。MySetAppExitFlag
函數應該設置一個適當的標志,允許子進程終止。由于dispatch source
自主運行,因此自己擁有自己,在程序關閉時會取消并釋放自己。
void MonitorParentProcess()
{
pid_t parentPID = getppid();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC,
parentPID, DISPATCH_PROC_EXIT, queue);
if (source)
{
dispatch_source_set_event_handler(source, ^{
MySetAppExitFlag();
dispatch_source_cancel(source);
dispatch_release(source);
});
dispatch_resume(source);
}
}
取消一個Dispatch Source
除非你顯式地調用 dispatch_source_cancel
函數,dispatch source
將一直保持活動,取消一個dispatch source
會停止遞送新事件,并且不能撤銷。因此你通常在取消dispatch source
后立即釋放它:
void RemoveDispatchSource(dispatch_source_t mySource)
{
dispatch_source_cancel(mySource);
dispatch_release(mySource);
}
取消一個dispatch source
是異步操作,調用 dispatch_source_cancel
之后,不會再有新的事件被處理,但是正在被dispatch source
處理的事件會繼續被處理完成。在處理完最后的事件之后,dispatch source
會執行自己的取消處理器。
取消處理器是你最后的執行機會,在那里執行內存或資源的釋放工作。例如描述符或mach port
類型的dispatch source
,必須提供取消處理器,用來關閉描述符或mach port
掛起和繼續Dispatch Source
你可以使用 dispatch_suspend
和dispatch_resume
臨時地掛起和繼續dispatch source
的事件遞送。這兩個函數分別增加和減少dispatch
對象的掛起計數。因此,你必須每次dispatch_suspend
調用之后,都需要相應的 dispatch_resume
才能繼續事件遞送。
掛起一個dispatch source
期間,發生的任何事件都會被累積,直到dispatch source
繼續。但是不會遞送所有事件,而是先合并到單一事件,然后再一次遞送。例如你監控一個文件的文件名變化,就只會遞送最后一次的變化事件