系列文章目录
iOS基础—Block
iOS基础—Protocol
iOS基础—KVC vs KVO
iOS网络—AFNetworking
iOS网络—NSURLSession
iOS内存管理—MRC vs ARC
iOS基础—Category vs Extension
iOS基础—多线程:GCD、NSThread、NSOperation
iOS基础—常用三方库:Masonry、SDWebImage
iOS基础—定时器:GCD、NSTimer、CADisplayLink
一、GCD
1.Dispatch Source简介
要学习GCD提供的定时器,我们首先要了解 Dispatch Source:
Dispatch Source 是 Grand Central Dispatch (GCD) 提供的一种数据结构,用于监控来自系统和应用程序的各种类型的异步事件。这些事件包括文件描述符活动、信号、定时器事件等。通过 Dispatch Source,开发者可以在特定事件发生时执行代码,而无需创建复杂的回调机制或者使用多线程轮询。Dispatch Source 提供了多种类型,每种类型对应不同的系统资源或事件:
- Timer Dispatch Sources:用于创建定时任务,如周期性检查或更新数据。
- Signal Dispatch Sources:用于捕获 UNIX 信号,如 SIGTERM 或 SIGSTOP,允许程序在这些信号发生时执行特定的处理逻辑。
- Descriptor Dispatch Sources:包括文件描述符和 socket 描述符,用于监控 I/O 操作的完成,如文件读写或网络通信。
- Process Dispatch Sources:用于监控进程相关的事件,如进程结束。
- Mach Port Dispatch Sources:用于监控 Mach 端口消息,适用于更低级别的操作系统功能。
下面介绍一下 Dispatch Source 的使用:
a.创建 dispatch_source_t 对象
//用于创建 dispatch_source_t 对象的函数,这种对象用于监控系统级别的异步事件,如文件描述符的读写状态、定时器事件、信号等。
dispatch_source_t dispatch_source_create(dispatch_source_type_t type,
uintptr_t handle,
unsigned long mask,
dispatch_queue_t queue);
type:指定要创建的 dispatch source 的类型。这个类型决定了 dispatch source 将要监控的事件类型:
DISPATCH_SOURCE_TYPE_TIMER:用于创建定时器。handle 和 mask 参数通常设置为 0。
DISPATCH_SOURCE_TYPE_READ:用于监控文件描述符的读取状态。handle 是文件描述符,mask 设置为 0。
DISPATCH_SOURCE_TYPE_WRITE:用于监控文件描述符的写入状态。handle 是文件描述符,mask 设置为 0。
DISPATCH_SOURCE_TYPE_SIGNAL:用于接收 UNIX 信号。handle 是信号编号,如 SIGTERM,mask 设置为 0。
b.配置 GCD 定时器行为
//用于配置 GCD 定时器行为的函数,它设置了定时器的开始时间、重复间隔和精度。
void dispatch_source_set_timer(dispatch_source_t source,
dispatch_time_t start,
uint64_t interval,
uint64_t leeway);
source:配置的定时器源,这个源必须是通过 dispatch_source_create 创建的 DISPATCH_SOURCE_TYPE_TIMER 类型。
start:定时器首次触发的时间。可以使用 dispatch_time 或 dispatch_walltime 函数来计算相对或绝对时间。
interval:定时器触发的时间间隔,单位是纳秒(ns)。例如,要设置间隔为1秒,应使用 1 * NSEC_PER_SEC。
leeway:定时器的时间误差容忍范围,也是以纳秒为单位。这个参数允许系统有一定的灵活性来优化定时器的触发,以提高能效和其他系统活动的整体性能。设置为0表示请求尽可能精确的计时,但通常建议提供一个非零的值
c.设置 dispatch_source_t 对象的事件处理器
//用于设置 dispatch_source_t 对象的事件处理器的函数。
void dispatch_source_set_event_handler(dispatch_source_t source,
dispatch_block_t handler);
source:你想要设置事件处理器的 dispatch_source_t 对象。
handler:一个无参数、无返回值的 block,当 dispatch_source_t 监听到的事件发生时执行。这个 block 在 dispatch_source_t 关联的队列上异步执行。
d.暂停/恢复dispatch 对象
//用于恢复之前被暂停的 dispatch 对象(如 dispatch queues 和 dispatch sources)
void dispatch_resume(dispatch_object_t object);
object:要恢复的 dispatch 对象。这可以是一个 dispatch queue、dispatch source 或任何其他支持暂停和恢复的 dispatch 对象。
注意事项:
Dispatch Sources:当创建一个新的 dispatch source 时,它默认是暂停状态。你必须显式调用 dispatch_resume 来启动它,以便它开始响应事件。
Dispatch Queues:虽然通常不需要手动暂停和恢复 dispatch queues,但在某些特殊情况下,如果你确实暂停了一个队列,需要使用 dispatch_resume 来恢复执行队列中的任务。
//用于暂时停止队列的执行或暂停 dispatch source 的事件处理。
void dispatch_suspend(dispatch_object_t object);
注意事项:
暂停队列:当你暂停一个队列时,队列上当前正在执行的任务将继续执行直到完成,但队列不会开始执行任何新的任务。
多次暂停需要多次恢复:每次调用 dispatch_suspend 都会增加挂起计数,你需要相应地调用 dispatch_resume 相同的次数来恢复队列的执行。
避免死锁:在队列上调用 dispatch_suspend 并在同一队列的任务中调用 dispatch_resume 可能导致死锁,因为 dispatch_resume 永远不会被执行。
e.dispatch_after
//用于在指定的时间之后异步执行一个代码块。
//这个函数非常适合用于延迟执行任务,比如更新用户界面、延迟发送网络请求、或者简单地延迟执行任何需要稍后进行的操作。
//dispatch_after只是封装调用了dispatch source定时器,然后在回调函数中执行定义的block。
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);
when:指定代码块应该被调度执行的时间。这个时间是相对于系统的绝对时间,通常使用 dispatch_time 函数来计算。
queue:指定执行代码块的队列。这可以是串行队列或并发队列。
block:要执行的代码块。
2.GCD定时器的使用
a.常见的使用方法
int main(int argc, const char * argv[]) {
// 创建一个串行队列
dispatch_queue_t queue = dispatch_queue_create("com.example.myqueue", DISPATCH_QUEUE_SERIAL);
// 创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置定时器
uint64_t interval = 2 * NSEC_PER_SEC; // 间隔时间,2秒
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, interval, 0.1 * NSEC_PER_SEC); // 允许0.1秒的误差
// 设置定时器触发时执行的块
dispatch_source_set_event_handler(timer, ^{
NSLog(@"Timer fired->%@",[NSDate date]);
});
// source创建后默认是暂停状态,需要手动调用dispatch_resume启动定时器。
dispatch_resume(timer);
// 创建并运行主运行循环
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
return 0;
}
运行结果如下,误差在允许范围内:
定时器误差的来源
- 系统调度策略:操作系统的任务调度器可能会推迟定时器的执行以优化能源使用或整体系统性能。
- 线程竞争:如果定时器的回调函数在一个繁忙的队列(如全局并发队列)上执行,可能会因为队列上其他任务的执行而延迟。
- 资源限制:系统资源的限制(如内存和CPU)也可能影响定时器的准确性。
b.引发crash的情况
(1) 循环引用:因为 dispatch_source_set_event_handler 回调是个block,在添加到source的链表上时会执行copy并被source强引用,如果block里持有了self,self又持有了source的话,就会引起循环引用。正确的方法是使用weak+strong或者提前调用dispatch_source_cancel 取消timer。下面是一个循环引用的例子:
@interface MyClass : NSObject
@property (strong, nonatomic) dispatch_source_t timer;