[iOS] GCD是神马-timer相关操作

iOS中定时器的实现主要有三种:NSTimer,GCD,CADisplayLink。我们项目中用的比较多的是GCD的,毕竟写起来比较方便,所以这里先看一下GCD的各种timer相关的方法,有机会再探讨一下另外两种吧。

// source
dispatch_source_create
dispatch_source_cancel
dispatch_source_testcancel
dispatch_source_set_timer
dispatch_source_set_event_handler
dispatch_source_set_cancel_handler
dispatch_source_set_registration_handler

// object操作(暂停、继续、取消)
dispatch_release
dispatch_retain
dispatch_activate
dispatch_suspend
dispatch_resume
dispatch_cancel

① dispatch_source_xxx 创建timer的source以及处理事件

(1) dispatch_source_create

dispatch_source_create类似于产生时钟信号的源,假设如果我们每三秒钟需要敲一个字母,它就是用于每3秒将任务(敲一个字母)提交给队列的。

dispatch_source_t dispatch_source_create(dispatch_source_type_t type,
                                         uintptr_t handle,
                                         unsigned long mask,
                                         dispatch_queue_t _Nullable queue);

dispatch_source_create不仅可以用于timer,它主要是用于监听事件,在事件触发后将指定的任务抛入queue中,它有四个参数:

  1. type:指定Dispatch Source类型,共有11个类型,特定的类型监听特定的事件。
DISPATCH_SOURCE_TYPE_DATA_ADD:属于自定义事件,可以通过dispatch_source_get_data函数获取事件变量数据,在我们自定义的方法中可以调用dispatch_source_merge_data函数向Dispatch Source设置数据,下文中会有详细的演示。
DISPATCH_SOURCE_TYPE_DATA_OR:属于自定义事件,用法同上面的类型一样。
DISPATCH_SOURCE_TYPE_MACH_SEND:Mach端口发送事件。
DISPATCH_SOURCE_TYPE_MACH_RECV:Mach端口接收事件。
DISPATCH_SOURCE_TYPE_PROC:与进程相关的事件。
DISPATCH_SOURCE_TYPE_READ:读文件事件。
DISPATCH_SOURCE_TYPE_WRITE:写文件事件。
DISPATCH_SOURCE_TYPE_VNODE:文件属性更改事件。
DISPATCH_SOURCE_TYPE_SIGNAL:接收信号事件。
DISPATCH_SOURCE_TYPE_TIMER:定时器事件。
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE:内存压力事件。
  1. handle:取决于要监听的事件类型,比如如果是监听Mach端口相关的事件,那么该参数就是mach_port_t类型的Mach端口号,如果是监听事件变量数据类型的事件那么该参数就不需要,设置为0就可以了。
  2. mask:取决于要监听的事件类型,比如如果是监听文件属性更改的事件,那么该参数就标识文件的哪个属性,比如DISPATCH_VNODE_RENAME。
  3. queue:设置回调函数所在的队列。
※典型的创建timer源的方式如下:
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));

在source被创建以后,其实他还是deactive状态,需要设置它的handler之类的处理任务,即监听到信号以后要干啥,以及要给哪个队列之类的,在设置完以后,需要调用dispatch_activate()或者dispatch_resume()来激活source

(2) dispatch_source_cancel

dispatch_source_cancel可以取消source,即不再监听事件,但如果handler任务已经提交给了queue,那么这个任务是不会取消的哈。

(3) dispatch_source_testcancel

dispatch_source_testcancel用于检测source是不是已经被cancel了,如果是的话就会返回0,如果没有被cancel就返回非0。

(4) dispatch_source_set_timer
dispatch_source_set_timer(dispatch_source_t source,
    dispatch_time_t start,
    uint64_t interval,
    uint64_t leeway);

这个函数是指定source从start时间开始,每隔interval时间触发一次,即将指定任务抛入到指定队列。

※some tips:
·如果是希望只运行一次的任务,可以将interval设为DISPATCH_TIME_FOREVER~
·leeway即对时间的精确度要求,如果是0的话,即希望系统尽可能的精确,所带来的的性能消耗也就更大一点,可以设为几纳秒之类的如果不是对时间有非常严格的要求。
·对已经cancel的source设置timer是不会生效的哦
·可以多次调用改变开始&间隔,之前的就会被清掉,并且如果已经是active的状态是不需要重新active滴

(5) dispatch_source_set_event_handler

终于到了设置任务的时候啦,这个就是设置当timer被触发以后,将什么block提交给create source时候指定的queue内的。

dispatch_source_set_event_handler(source, ^{
       // source触发后需要执行的任务
});
(6) dispatch_source_set_cancel_handler

给source设置当被cancel时执行的任务,例如释放一些资源之类的。

dispatch_source_set_cancel_handler(source, ^{
  NSLog(@"cancelled");
});
(7) dispatch_source_set_registration_handler

当source被register,即调用dispatch_resume()等激活时,会触发registration_handler;如果在set handler之前source已经被激活了,handler会立刻执行。但只会触发一次哦,不会每次dispatch_resume都触发,只有第一次激活触发。

dispatch_source_set_registration_handler(source, ^{
  NSLog(@"register");
});

② dispatch_source_xxx 的一些应用

(1) timer应用

比较常用的有倒计时,每秒钟做一些事情,例如更新progress bar等。只要set一个每秒钟触发的timer,在countdown归0以后cancel source即可。

__block int count = 10;
    
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));

dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
    if (count == 0) {
        dispatch_source_cancel(timer);
        return;
    }
    count--;
    // 做一些任务
    NSLog(@"time:%f count:%d", [[NSDate date] timeIntervalSince1970], count);
});

dispatch_resume(timer);
NSLog(@"timer start: %f", [[NSDate date] timeIntervalSince1970]);

输出:
time:1566127883.057824 count:9
timer start: 1566127883.057791
time:1566127884.058723 count:8
time:1566127885.057968 count:7
time:1566127886.057911 count:6
time:1566127887.057965 count:5
time:1566127888.058147 count:4
time:1566127889.058436 count:3
time:1566127890.058170 count:2
time:1566127891.058068 count:1
time:1566127892.058062 count:0
(2) 非timer应用 - 例如网络请求

timer应用大家应该都很熟悉了,虽然这个part跑题了我也还是比较喜欢这个part的~
dispatch_source不仅仅有timer这个type,它还有一个type也比较常用,就是DISPATCH_SOURCE_TYPE_DATA_ADD及DISPATCH_SOURCE_TYPE_DATA_OR。

这两个事件是自定义事件,是由于我们自己调用dispatch_source_merge_data(value)触发的,还有一个DISPATCH_SOURCE_TYPE_DATA_REPLACE事件也是会被merge触发,只是不太常用。

这个value是神马呢?
就是最开始没有调用过dispatch_source_merge_data的时候source所绑定的value就是0,然后之后的每次调用dispatch_source_merge_data都会更新source所对应的的value(这里的value可以被称为pending value),如何更新要看source的type是ADD/OR/REPLACE啦。

DISPATCH_SOURCE_TYPE_DATA_ADD DISPATCH_SOURCE_TYPE_DATA_OR DISPATCH_SOURCE_TYPE_DATA_REPLACE
pending value = pending value + merge传入value pending value = pending value与merge传入value按位与操作的结果 pending value = merge传入value

※那么我们要如何知道现在的pending value是多少呢?
可以通过dispatch_source_get_data(source)来获取,并且其实对于不同type的source这个方法返回的值意义是不一样的,如果是timer,这个方法就可以获取timer被触发过多少次。

dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));

dispatch_source_set_event_handler(source, ^{
    NSLog(@"%lu 人已报名",dispatch_source_get_data(source));
});
dispatch_resume(source);

dispatch_apply(5, dispatch_get_global_queue(0, 0), ^(size_t index) {
    NSLog(@"用户%zd 报名郊游",index);
    dispatch_source_merge_data(source, 1); // 触发事件,传递数据
});

输出:
用户2 报名郊游
用户1 报名郊游
用户4 报名郊游
用户3 报名郊游
用户0 报名郊游
3 人已报名
2 人已报名

※有木有发现一个很神奇的事情,就是dispatch_source_merge_data明明被调用了5次,但是handler只被触发了两次,这是为什么呢?
dispatch_source_create的注释中有这么一段话:
Dispatch sources are not reentrant. Any events received while the dispatch source is suspended or while the event handler block is currently executing will be coalesced and delivered after the dispatch source is resumed or the event handler block has returned.

如果source频繁的被触发调用block,那么可能会将block积攒并且coalesced(联结合并)以后再调用,这也是为什么前面将source所绑定的value称为pending value,因为只有没有被处理的value会在每次调用dispatch_source_merge_data被更新,如果已经调用过event_handler,pending value就又会归零。
P.S.如果传入dispatch_source_merge_data的value是0,不会触发任何事情哦

☀️应用1:作为通知
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));

dispatch_source_set_event_handler(source, ^{
    dispatch_sync(dispatch_get_main_queue(), ^{
        //做任务例如更新UI
        NSLog(@"data: %lu", dispatch_source_get_data(source));
    });
});

dispatch_resume(source);

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    //模拟网络请求
    sleep(5);
    dispatch_source_merge_data(source, 1);
});

输出:
data: 1

类似于notification,但是不用所有人都监听,发一个全局通知比较浪费,只要自己类里面监听一下任务完成就可以。利用dispatch_source_merge_data会触发source事件来在handler里面做一些UI处理。

他和信号量的那种UI通知的区别是,那种只能执行一次,而这种监听是每次dispatch_source_merge_data传入不为0的数都会触发监听的block。

☀️应用2:更新进度条

其实有的时候我们定了每秒钟或者更短的事件去更新进度条会让UI更新的压力很大,以及如果我们更新下载进度,如果回调很频繁会让UI很忙碌,其实可以攒一会儿再更新对用户也没有太大影响,利用DISPATCH_SOURCE_TYPE_DATA_ADD可以很好地解决这个问题。

dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));

__block NSUInteger totalComplete = 0;

dispatch_source_set_event_handler(source, ^{
    NSUInteger value = dispatch_source_get_data(source);
    totalComplete += value;
    
    NSLog(@"progress:%f", totalComplete / 100.0f);
});

dispatch_resume(source);

for (NSUInteger index = 0; index < 100; index++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_source_merge_data(source, 1);
        
        usleep(20000);//0.02秒
    });
}

输出:
progress:0.010000
progress:0.030000
progress:0.050000
progress:0.060000
progress:0.090000
progress:0.120000
progress:0.130000
progress:0.160000
progress:0.200000
progress:0.280000
progress:0.420000
progress:0.430000
progress:0.440000
progress:0.450000
progress:0.630000
progress:1.000000

③ dispatch_xxx

这部分主要是一些通用的操作啦,只要是dispatch_object_t都可以进行这些操作,只是日常使用大多数还是对dispatch_source_t有这个需求。

(1) dispatch_activate

激活queue或者source,对已经active状态的object执行activate没有任何作用。
激活操作执行一次即可,它和suspend不对应,如果suspend以后调用activate是不会开始执行任务的,因为其实本来他就是active的所以没效果。

//队列:如果创建的是initial inactive状态的queue,则需要active哦
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT_INACTIVE);
dispatch_activate(concurrentQueue);

// source
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));
dispatch_activate(source);

在inactive状态被抛入的任务,在active以后都会执行的哈~

注意如果是没有引用计数的queue并且没有被active可能会引起crash,因为没有active就没有内部引用,在代码段执行结束后queue就会被置为nil,然后会产生野指针crash。

例如酱紫就会crash,因为不能传入nil给dispatch after:

- (void)testCrash {
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT_INACTIVE);
    
//    dispatch_activate(concurrentQueue);
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), concurrentQueue, ^{
        NSLog(@"hh");
    });
}
(2) dispatch_resume & dispatch_suspend

dispatch_resume: 继续/开始。

对inactive状态的source调用resume可以起到激活的作用,这个主要是为了兼容旧版本iOS做的处理,但其实不推荐用resume来激活source。并且对于非source的object,resume不可以起到激活作用。

dispatch_suspend: 暂停。

在suspend状态的queue如果被抛入了任务,在resume以后也是会继续执行的。
suspend每调用一次会增加object的suspend计数,resume与suspend对应,可以减少suspend的计数。当计数归0后,object将继续正常工作。但如果计数已经归0了还是继续调用resume是会crash的。它与suspend必须对应 ,不作为active使用的resume不能多于suspend。

dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));

__block NSUInteger totalComplete = 0;

dispatch_source_set_event_handler(source, ^{
    NSUInteger value = dispatch_source_get_data(source);
    
    totalComplete += value;
    
    NSLog(@"progress:%f", totalComplete / 100.0f);
});

dispatch_activate(source);

for (NSUInteger index = 0; index < 100; index++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        dispatch_source_merge_data(source, 1);
        
        usleep(20000); //0.02秒
    });
}

dispatch_suspend(source);

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    dispatch_resume(source);
});

输出:
progress:0.020000
progress:0.040000
progress:0.060000
progress:0.120000
progress:0.150000
progress:0.160000 // 被suspend了之后的
progress:1.000000

如果是对于queue而言,suspend不代表不执行任务,在suspend之前被抛入并已经分配给线程的任务是会正常执行的,suspend只是暂停了派发,但是没有办法暂停thread。

(3) dispatch_source_cancel & dispatch_cancel

cancel以后这个object相当于就不可以再被使用了,如果需要用就必须重新创建哦。

对于timer的释放我们一般都用dispatch_source_cancel,主要是防止之后还会触发event handler:

if (source) {
  dispatch_source_cancel(source);
  source = nil;
}

需要注意的是: (有一定概率,也不是每次都crash)
如果suspend状态的source,直接执行cancel&nil的话会crash,必须要先resume然后在cancel置空,所以需要一个变量记录source的状态是不是suspend。

(4) dispatch_retain 和 dispatch_release

和NSObject的release及retain很像,就是回收及增加引用计数。但在ARC开启的时候其实dispatch_object_t都会被自动管理release和retain,我们并不需要手动release的。


P.S. 推荐一个关于如何在iOS10以下实现NSTimer block形式的文章~
https://www.jianshu.com/p/52cb70530e6a

参考文献:

  1. 各种timer:https://www.jianshu.com/p/6b0a7d4ec1a8
    https://www.jianshu.com/p/d782791a4580
  2. dispatch source: https://www.jianshu.com/p/aeae7b73aee2
  3. 多线程:https://www.jianshu.com/p/880c2f9301b6
  4. GCD & source: https://www.jianshu.com/p/ba71de959d5f?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

你可能感兴趣的:([iOS] GCD是神马-timer相关操作)