定时器,用来延迟或重复执行某些方法,例如:网络定时刷新,UI间隔刷新,动画效果......iOS中的定时器大致分为这几类:
NSObject
GCD定时器
NSTimer
CADisplayLink
RunLoop
在讲解定时器之前,先普及下RunLoop的基本知识。传送门: iOS - RunLoop 深入理解感谢ibireme整理了一份完整讲解,从 CFRunLoop 的源码入手,介绍 RunLoop 的概念以及底层实现原理。在此,总结性的介绍下。
RunLoop概念
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
这种模型通常被称作 Event Loop。 Event Loop 在很多系统和框架里都有实现,比如 Node.js 的事件处理,比如 Windows 程序的消息循环,再比如 OSX/iOS 里的 RunLoop。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。所以,RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。
苹果不允许直接创建 RunLoop,需要类似像主线程调用一样调用,即[NSRunLoop mainRunLoop],在 CoreFoundation 里面关于 RunLoop 有5个类:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
其中CFRunLoopTimerRef 是基于时间的触发器,其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。后面要讲的NSTimer 其实就是 CFRunLoopTimerRef。
NSObject
iOS框架图
在object-c中,绝大部分类的基类都是NSObject,使用NSObject延迟执行也被用于网络定时刷新,配套使用代码cancelPreviousPerformRequestsWithTarget与performSelector,相对而言比较简洁。当然NSObject与RunLoop之间的联系远不只于此,例如事件响应和手势识别,就不做展开。
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
当调用 cancelPreviousPerformRequestsWithTarget时,实际上就是讲Timer 从RunLoop中移除。
GCD
GCD定时器其实是一种特殊的分派源,它是基于分派队列的,而NSTimer是基于运行循环的,所以,尤其是在多线程中,GCD定时器要比NSTimer好用的多。另外,GCD定时器使用dispatch_block_t,而不是方法选择器,也就是说GCD实现的定时器是不受RunLoop约束。
实际上 RunLoop 底层也会用到 GCD 的东西,在
CFRrunLoop.c中我们能发现引用了
#include,例如RunLoop 的超时时间就是使用 GCD 中的 dispatch_source_t来实现的,摘自 __CFRunLoopRun中的源码:
dispatch_source_t timeout_timer = NULL;
struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
if (seconds <= 0.0) { // instant timeout
seconds = 0.0;
timeout_context->termTSR = 0ULL;
} else if (seconds <= TIMER_INTERVAL_LIMIT) { //超时时间在最大限制内,才创建timeout_timer
dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();
timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_retain(timeout_timer);
timeout_context->ds = timeout_timer;
timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
dispatch_resume(timeout_timer);
} else { // infinite timeout
seconds = 9999999999.0;
timeout_context->termTSR = UINT64_MAX;
}
在GCD API 记录 (三)中的 dispatch_source中的timer中有详细讲解。
但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。
GCD定时器实现:
- 执行一次
double delayTimer = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayTimer * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
//do
});
- 重复执行
NSTimeInterval delayTimer = 1.0;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), delayTimer * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(_timer, ^{
//do
});
dispatch_resume(_timer);
NSTimer
在介绍RunLoop时已经提到过:NSTimer 其实就是 CFRunLoopTimerRef。他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
使用方法:
- 创建方法
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(action:) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
其中,
TimerInterval : 执行之前等待的时间。比如设置成1.0,就代表1秒后执行方法
target : 需要执行方法的对象。
selector : 需要执行的方法
repeats : 是否需要循环
- 结束方法
[timer invalidate];
CADisplayLink
简单地说,CADisplayLink就是一个定时器,保持跟屏幕刷新率相同的频率刷新。
虽然CADisplayLink使用场合相对专一,只适合做UI的不停重绘,但并不妨碍他成为很多高手热爱的技巧之一。在做精细的动画效果时,CADisplayLink将是一个很好的助手,例如自定义动画引擎或者视频播放的渲染;类似于siri语音输入效果就用到了CADisplayLink;很多模仿wave效果也多采用CADisplayLink刷新界面。
iOS设备的屏幕刷新频率是固定的,我们在使用时不用关心屏幕的刷新频率,因为它本身就是跟屏幕刷新同步的。CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。
属性
- timestamp:只读,屏幕显示的上一帧的时间戳,timestamp = duration * frameInterval。
- duration:只读,系统屏幕每次刷新的时间戳,在target的selector被首次调用以后被系统赋值。
- targetTimestamp:只读
- paused:读写,控制CADisplayLink的运行,即暂停操作。结束一个CADisplayLink实例需要调用invalidate从runloop删除
- frameInterval:读写,标识间隔多少帧调用一次selector 方法,默认为1,即每帧都调用一次。对于iOS设备来说那刷新频率就是60HZ也就是每秒60次,如果将 frameInterval 设为2 那么就会两帧调用一次,也就是变成了每秒刷新30次。
- preferredFramesPerSecond:
使用方法:
- 创建方法
CADisplayLink *timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(wave)];
[timer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
- 结束方法
[timer invalidate];
衍生:CADisplayLink与UIBezierPath、CAShapeLayer的激情碰撞
如果说CADisplayLink是控制动画流畅度的尚方宝剑,那UIBezierPath与CAShapeLayer就是实现动画效果的倚天屠龙。