支持原创
现代系统通常提供异步接口,允许应用向系统提交请求,然后在系统处理请求时应用可以继续处理自己的事情。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
继续。但是不会递送所有事件,而是先合并到单一事件,然后再一次递送。例如你监控一个文件的文件名变化,就只会递送最后一次的变化事件