Dispatch Source 源是一个偏底层的函数集合,使用时CPU负荷非常小,尽量不占资源,开发过程中大多是配合定时器使用。
1、Dispatch Source 类型
typedef const struct dispatch_source_type_s *dispatch_source_type_t; /* 当同一时间,一个事件的的触发频率很高,那么Dispatch Source会将这些响应以ADD的方式进行累积,然后等系统空闲时最终处理,如果触发频率比较零散,那么Dispatch Source会将这些事件分别响应。 */ DISPATCH_SOURCE_TYPE_DATA_ADD 自定义的事件,变量增加 DISPATCH_SOURCE_TYPE_DATA_OR 自定义的事件,变量OR DISPATCH_SOURCE_TYPE_DATA_REPLACE 自定义的事件,变量Replace DISPATCH_SOURCE_TYPE_MACH_SEND MACH端口发送 DISPATCH_SOURCE_TYPE_MACH_RECV MACH端口接收 DISPATCH_SOURCE_TYPE_MEMORYPRESSURE 内存报警 DISPATCH_SOURCE_TYPE_PROC 进程监听,如进程的退出、创建一个或更多的子线程、进程收到UNIX信号 DISPATCH_SOURCE_TYPE_READ IO操作,如对文件的操作、socket操作的读响应 DISPATCH_SOURCE_TYPE_SIGNAL 接收到UNIX信号时响应 DISPATCH_SOURCE_TYPE_TIMER 定时器 DISPATCH_SOURCE_TYPE_VNODE 文件状态监听,文件被删除、移动、重命名 DISPATCH_SOURCE_TYPE_WRITE IO操作,如对文件的操作、socket操作的写响应 DISPATCH_MACH_SEND_DEAD /// 系统内存压力状态恢复正常。 #define DISPATCH_MEMORYPRESSURE_NORMAL 0x01 /// 系统内存压力状态变为“警告”。 #define DISPATCH_MEMORYPRESSURE_WARN 0x02 /// 系统内存压力状态变为“紧急”。 #define DISPATCH_MEMORYPRESSURE_CRITICAL 0x04
这是很有用的东西,它支持所有kqueue所支持的事件、mach端口、内建计时器支持(这样我们就不用使用超时参数来创建自己的计时器)、用户事件。
常用方法:
dispatch_source_create:创建源 dispatch_source_set_event_handler: 设置源事件回调 dispatch_source_merge_data:置源事件设置数据 dispatch_source_get_data:获取源事件数据 dispatch_resume: 继续 dispatch_suspend: 挂起 dispatch_cancel: 取消
2、Dispatch Source 常用方法
2.1、创建Dispatch Source
创建 dispatch source 需要同时创建事件源和 dispatch source 本身。事件源是处理事件所需要的native数据结构
/** 创建dispatch_source * type: dispatch源可处理的事件 * handle: 可以理解为句柄、索引或id,假如要监听进程,需要传入进程的ID * mask: 可以理解为描述,提供更详细的描述,让它知道具体要监听什么 * queue: 自定义源需要的一个队列,用来处理所有的响应句柄 */ dispatch_source_t source = dispatch_source_create(dispatch_source_type_t type, uintptr_t handle, unsigned long mask, dispatch_queue_t queue)
句柄是一种指向指针的指针。它指向的是一个类或结构,它和系统有很密切的关系。
HINSTANCE实例句柄、HBITMAP位图句柄、HDC设备表述句柄、HICON图标句柄 等。其中还有一个通用句柄,就是HANDLE。
2.2、设置事件处理器
void dispatch_source_set_event_handler(dispatch_source_t source, dispatch_block_t _Nullable handler); void dispatch_source_set_event_handler_f(dispatch_source_t source, dispatch_function_t _Nullable handler);
事件处理器的代码负责处理所有到达的事件。
1、如果事件处理器已经在queue中并等待处理已经到达的事件,如果此时又来了一个新事件,dispatch source会合并这两个事件。事件处理器通常只能看到最新事件的信息,不过某些类型的dispatch source也能获得已经发生以及合并的事件信息。
2、如果事件处理器已经开始执行,一个或多个新事件到达,dispatch source会保留这些事件,直到前面的事件处理器完成执行。然后以新事件再次提交处理器到queue。
函数事件处理器有一个context指针指向dispatch source对象,没有返回值。Block事件处理器没有参数,也没有返回值。
2.3、取消处理器
你可以在任何时候安装取消处理器,但通常我们在创建dispatch source时就会安装取消处理器。
/** * 除非你调用 dispatch_source_cancel 函数, * 否则 dispatch source 将一直保持活动, * 取消一个dispatch source只会停止递送新事件,并且不能撤销。 * 因此你通常在取消 dispatch source 后立即释放 */ /** * 取消一个 dispatch source 是异步操作, * 调用 dispatch_source_cancel 之后,不会再有新的事件被处理, * 但是正在被 dispatch source 处理的事件会继续被处理完成。 * 在处理完最后的事件之后,dispatch source 会执行自己的取消处理器。 * 取消处理器是你最后的执行机会,在那里执行内存或资源的释放工作。 * 例如描述符或mach port类型的dispatch source, * 必须提供取消处理器,用来关闭描述符或mach port */ void dispatch_source_cancel(dispatch_source_t source); long dispatch_source_testcancel(dispatch_source_t source); void dispatch_source_set_cancel_handler(dispatch_source_t source, dispatch_block_t _Nullable handler); void dispatch_source_set_cancel_handler_f(dispatch_source_t source, dispatch_function_t _Nullable handler);
取消处理器在 dispatch source 释放之前执行清理工作。
多数类型的dispatch source不需要取消处理器,除非你对 dispatch source 有自定义行为需要在释放时执行。
但是使用描述符或Mach port的dispatch source必须设置取消处理器,用来关闭描述符或释放Mach port。
否则可能导致微妙的bug,这些结构体会被系统其它部分或你的应用在不经意间重用。
2.4、挂起(dispatch_suspend)与继续(dispatch_resume)
/// 继续事件递送,增加挂起计数 dispatch_resume(source) /// 临时挂起事件递送,减少挂起计数 dispatch_suspend(source)
每次调用 dispatch_suspend 之后,都需要相应的 dispatch_resume 才能继续事件递送。
挂起一个dispatch source期间,发生的任何事件都会被累积,直到dispatch source继续。但是不会递送所有事件,而是先合并到单一事件,然后再一次递送。例如你监控一个文件的文件名变化,就只会递送最后一次的变化事件。
Dispatch Source定时器使用时也有一些需要注意的地方,不然很可能会引起crash:
1、循环引用:因为dispatch_source_set_event_handler回调是个block,在添加到source的链表上时会执行copy并被source强引用,如果block里持有了self,self又持有了source的话,就会引起循环引用。正确的方法是使用weak+strong或者提前调用dispatch_source_cancel取消timer。
2、调用次数平衡:dispatch_resume和dispatch_suspend调用次数需要平衡,如果重复调用dispatch_resume则会崩溃,因为重复调用会让dispatch_resume代码里if分支不成立,从而执行了DISPATCH_CLIENT_CRASH(“Over-resume of an object”)导致崩溃。
3、source在suspend状态下,如果直接设置source = nil或者重新创建source都会造成crash。正确的方式是在resume状态下调用dispatch_source_cancel(source)释放当前的source。
3、应用
3.1、用户事件
定义:任何线程调用它的函数 dispatch_source_merge_data 后,会执行Dispatch Source事先定义好的句柄(可以把句柄简单理解为一个block),这个过程叫custom event,即用户事件。这是dispatch_source支持处理的一种事件。
特征:GCD会在事件句柄被执行之前自动将多个事件进行联结。你可以将数据“拼接”至dispatch source中任意次,并且如果dispatch queue在这期间繁忙的话,GCD只会调用该句柄一次。
用户事件两种类型参数:
DISPATCH_SOURCE_TYPE_DATA_ADD:事件在联结时会把这些数字相加
DISPATCH_SOURCE_TYPE_DATA_OR:事件在联结时会把这些数字逻辑与运算
当事件句柄执行时,我们可以使用dispatch_source_get_data函数访问当前值,然后这个值会被重置为0。
3.2、进度条更新
让我假设一种情况。假设一些异步执行的代码会更新一个进度条。因为主线程只不过是GCD的另一个dispatch queue而已,所以我们可以将GUI更新工作push到主线程中。然而,这些事件可能会有一大堆,我们不想对GUI进行频繁而累赘的更新,理想的情况是当主线程繁忙时将所有的改变联结起来。
用dispatch source就完美了,使用DISPATCH_SOURCE_TYPE_DATA_ADD,我们可以将工作拼接起来,然后主线程可以知道从上一次处理完事件到现在一共发生了多少改变,然后将这一整段改变一次更新至进度条。
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue()); dispatch_source_set_event_handler(source, ^{ NSLog(@"progressIndicator >>>>>>>>>> %lu",dispatch_source_get_data(source)); [progressIndicator incrementBy:dispatch_source_get_data(source)]; }); dispatch_resume(source); // 模拟实现 dispatch_apply([array count], globalQueue, ^(size_t index) { // do some work on data at index NSLog(@"IndicatorIndex >>>>>>>>>> %lu",index); int x = arc4random()%50; CGFloat xf = x/1000.0; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(xf * NSEC_PER_SEC)), globalQueue, ^{ dispatch_source_merge_data(source, 1); }); }); /* 假设array.count = 100,dispatch_source_create 的type参数: DISPATCH_SOURCE_TYPE_DATA_ADD:dispatch_source_get_data(source)返回积累次数加起来为100; DISPATCH_SOURCE_TYPE_DATA_OR:dispatch_source_get_data(source)返回次数每次都是为1。 */
- (void)dispatch_source_TSET { __block NSInteger totalComplete = 0; // 创建串行队列 dispatch_queue_t queue = dispatch_queue_create("HJ", NULL); // 创建主队列,源类型为 DISPATCH_SOURCE_TYPE_DATA_ADD dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue()); // 设置事件回调 dispatch_source_set_event_handler(source, ^{ NSLog(@"%@",[NSThread currentThread]); NSUInteger value = dispatch_source_get_data(source); totalComplete += value; NSLog(@"进度: %.2f", totalComplete/100.0); }); // 开启源事件 dispatch_resume(source); // 模拟实现 for (int i= 0; i<100; i++) { dispatch_async(queue, ^{ sleep(1); // 发送源数据 dispatch_source_merge_data(source, 1); }); } 2021-02-19 14:19:04.969558+0800 007---Dispatch_source[29658:494902] <NSThread: 0x600003048080>{number = 1, name = main} 2021-02-19 14:19:04.969941+0800 007---Dispatch_source[29658:494902] 进度: 0.01 2021-02-19 14:19:05.970992+0800 007---Dispatch_source[29658:494902] <NSThread: 0x600003048080>{number = 1, name = main} 2021-02-19 14:19:05.971493+0800 007---Dispatch_source[29658:494902] 进度: 0.02 2021-02-19 14:19:06.975953+0800 007---Dispatch_source[29658:494902] <NSThread: 0x600003048080>{number = 1, name = main} 2021-02-19 14:19:06.976476+0800 007---Dispatch_source[29658:494902] 进度: 0.03 2021-02-19 14:19:07.981136+0800 007---Dispatch_source[29658:494902] <NSThread: 0x600003048080>{number = 1, name = main} 2021-02-19 14:19:07.981392+0800 007---Dispatch_source[29658:494902] 进度: 0.04 2021-02-19 14:19:08.986199+0800 007---Dispatch_source[29658:494902] <NSThread: 0x600003048080>{number = 1, name = main} 2021-02-19 14:19:08.986579+0800 007---Dispatch_source[29658:494902] 进度: 0.05 ----------------------------------------------------------------------------------------------------
2.3、定时器
- (void)use033{ //倒计时时间 __block int timeout = 3; //创建队列 dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0); //创建timer dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, globalQueue); //设置1s触发一次,0s的误差 /* - source 分派源 - start 控制计时器触发的时刻。参数类型是 dispatch_time_t,这是一个opaque类型,我们不能直接操作它。我们得需要 dispatch_time 和 dispatch_walltime 函数来创建它们。另外,常量 DISPATCH_TIME_NOW 和 DISPATCH_TIME_FOREVER 通常很有用。 - interval 间隔时间 - leeway 计时器触发的精准程度 */ dispatch_source_set_timer(timer,dispatch_walltime(NULL, 0),1.0*NSEC_PER_SEC, 0); //触发的事件 dispatch_source_set_event_handler(timer, ^{ //倒计时结束,关闭 if (timeout <= 0) { //取消dispatch源 dispatch_source_cancel(timer); timer = NULL; }else{ timeout--; dispatch_async(dispatch_get_main_queue(), ^{ //更新主界面的操作 NSLog(@"倒计时 - %d", timeout); }); } }); //开始执行dispatch源 dispatch_resume(timer); }
因为dispatch_source不依赖于Runloop,而是直接和底层内核交互,准确性更高。
时间准确,可以使用子线程