这篇文章主要整理一下面试中会问到的一个知识点:几种计时器的知识点
(一)NSTimer
1.什么是NSTimer?
官方给出的解释:“A timer that fires after a certain time interval has elapsed, sending a specified message to a target object.”
即经过一定时间间隔后将指定的消息发送到目标对象的定时器。总结三要素:时间间隔,发送消息,目标对象
2.NSTimer工作原理?
官方给出的解释:“Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.”
即timer是与RunLoop一起使用才会生效。 总的来说就是我们创建一个timer,需要将它添加到当前线程对应的RunLoop的特定mode下,RunLoop保持对timer强引用,RunLoop调度timer,让timer特定时间间隔给target发送@selector消息,整个过程就是这样。
3.NSTimer都有哪些分类?
计时器分为一次计时器,重复计时器,在创建时即可指定类型
官方给出的解释:“You specify whether a timer is repeating or nonrepeating at creation time. A nonrepeating timer fires once and then invalidates itself automatically, thereby preventing the timer from firing again. By contrast, a repeating timer fires and then reschedules itself on the same run loop. A repeating timer always schedules itself based on the scheduled firing time, as opposed to the actual firing time.”
即一次计时器触发一次然后自动给自己发送invalidate消息,从而防止计时器再次触发。 相比之下,重复计时器会触发,然后在同一个运行循环中重新安排自己。 重复计时器总是根据计划的点火时间来计划自己,而不是实际的点火时间。重复计时器直到接收invalidate方法才停止发送消息。
4.NSTimer跟方法接收者之间怎样保持关系?
从上面的定义我们可以看出timer会在特定时间间隔后给targer发送@selector消息,那么如何保证timer在未来的某个时间能触发指定事件时,我们指定的方法是有效的呢?这就涉及到timer跟调用者之间怎么保持关系呢,其实很简单,只要timer强引用接收@selector的target即可,实际上系统也是这样做的。
4.NSTimer 跟方法接受者之间怎么造成了循环引用?
我们经常说到timer与self会造成循环引用,并不是因为RunLoop引起,而是timer本身会对self有强引用。
5.NSTimer为什么要添加到RunLoop中才起作用?
前面我们提到了timer是和RunLoop一起工作的,为什么呢?我们知道所有的Source如果要起作用,就得加到RunLoop中去,同理timer也属于资源,肯定是加到RunLoop中才会生效的,如果一个RunLoop中没有任何资源,运行该RunLoop就会立马退出的。
6.如果NSTimer加到RunLoop中了但是不起作用,主要原因是什么?
假如我们按照前面说的将timer添加到RunLoop中去了,但是还没触发事件,怎么回事呢?原因可能有两点:
a. RunLoop并未运行
每一个线程都有它自己的RunLoop,程序的主线程会自动的使runloop生效,但对于我们自己新建的线程,它的runloop是不会自己运行起来,当我们需要使用它的runloop时,就得自己启动。当将timer添加到非主线程对应的RunLoop中时,如果对应的RunLoop没有运行起来,在不会主动scheduledTimer的
b.RunLoop 的Mode
前面提到了将创建好的timer需要添加到特定RunLoop中的某种mode下,通常默认是主线程的defalut mode。同一线程的RunLoop在运行的时候,任何时候只能处于一种mode,所以当RunLoop所处的mode不是timer对应的mode时,也不会触发timer。
综上: 要让timer生效,必须保证该线程的rRunLoop已启动,而且其运行的RunLoop mode也要匹配
7.NSTimer 不会准时触发的原因是什么?
当线程空闲的时候timer的消息触发还是比较准确的。但是一旦当线程忙于其他计算,timer的触发消息就会有延迟,等线程忙完以后后面的消息触发的时间仍然都是整数倍与开始我们指定的时间,这也从侧面证明,timer并不会因为触发延迟而导致后面的触发时间发生延迟。综上: timer不是一种实时的机制,会存在延迟,而且延迟的程度跟当前线程的执行情况有关。
8.NSTimer使用方法
//类方法创建timer,默认添加到当前线程对应的RunLoop,mode是默认模式
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
//类方法创建timer,需手动添加到RunLoop,mode是可选的
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
//实例方法创建timer,需手动添加到RunLoop,mode是可选的
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep
//添加runloop方法
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;
//发射日期即第一次调用@selector方法的时间点
@property (copy) NSDate *fireDate;
//时间间隔
@property (readonly) NSTimeInterval timeInterval;
//取消timer发送消息的方法
- (void)invalidate;
对上面所有方法参数做个说明:
ti(interval)属性:定时器触发间隔时间,单位为秒,可以是小数。如果值小于等于0.0的话,系统会默认赋值0.1毫秒
invocation属性:这种形式用的比较少,大部分都是block和aSelector的形式
yesOrNo(rep属性:是否重复,如果是YES则重复触发,直到调用invalidate方法;如果是NO,则只触发一次就自动调用invalidate方法
aTarget(t)属性:发送消息的目标,timer会强引用aTarget,直到调用invalidate方法
aSelector(s)属性:将要发送给aTarget的消息,如果带有参数则应:- (void)timerFireMethod:(NSTimer *)timer声明
userInfo(ui)属性:传递的用户信息。使用的话,首先aSelector须带有参数的声明,然后可以通过[timer userInfo]获取,也可以为nil,那么[timer userInfo]就为空
date属性:触发的时间,一般情况下我们都写[NSDate date],这样的话定时器会立马触发一次,并且以此时间为基准。如果没有此参数的方法,则都是以当前时间为基准,第一次触发时间是当前时间加上时间间隔ti
block属性:timer触发的时候会执行这个操作,带有一个参数,无返回值
Note:着重说一下[timer invalidate]这个方法
官方的文档解释:“Calling this method requests the removal of the timer from the current run loop; as a result, you should always call the invalidate
method from the same thread on which the timer was installed.
”
即在哪个线程中添加的timer需要在哪个线程中调用这个方法,调用之后即从当前的RunLoop中移除timer,一旦timer被移除,RunLoop就会释放对timer的强引用。
(二)CADisplayLink
1.什么是CADisplayLink?
官方文档的解释:“A timer object that allows your application to synchronize its drawing to the refresh rate of the display.”
即允许应用内容绘制跟显示刷新频率一样的计时器, 其实CADisplayLink就是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器
- CADisplayLink 工作原理
我们在应用中创建一个新的 CADisplayLink 对象,把它添加到一个RunLoop中,并给它提供一个 target 和 selector 在屏幕刷新的时候调用。
一但 CADisplayLink 以特定的模式注册到runloop之后,每当屏幕需要刷新的时候,runloop就会调用CADisplayLink绑定的target上的selector,这时target可以读到 CADisplayLink 的每次调用的时间戳,用来准备下一帧显示需要的数据。例如一个视频应用使用时间戳来计算下一帧要显示的视频数据。在UI做动画的过程中,需要通过时间戳来计算UI对象在动画的下一帧要更新的大小等等。 - CADisplayLink 使用方法
//类方法创建CADisplayLink对象,绑定target跟sel
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
//CADisplayLink 需手动添加到RunLoop中,mode:可选
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
@property(readonly, nonatomic) CFTimeInterval duration;
@property(nonatomic) NSInteger frameInterval;
@property(getter=isPaused, nonatomic) BOOL paused;
@property(readonly, nonatomic) CFTimeInterval timestamp;
@property(readonly, nonatomic) CFTimeInterval targetTimestamp
duration属性:是可读可写的NSInteger型值,提供了每帧之间的时间,也就是屏幕每次刷新之间的的时间。该属性在target的selector被首次调用以后才会被赋值。selector的调用间隔时间计算方式是:时间=duration×frameInterval。 我们可以使用这个时间来计算出下一帧要显示的UI的数值。但是 duration只是个大概的时间,如果CPU忙于其它计算,就没法保证以相同的频率执行屏幕的绘制操作,这样会跳过几次调用回调方法的机会。
frameInterval属性:是可读可写的NSInteger型值,标识间隔多少帧调用一次selector 方法,默认值是1,即每帧都调用一次。如果每帧都调用一次的话,对于iOS设备来说那刷新频率就是60HZ也就是每秒60次,如果将 frameInterval 设为2 那么就会两帧调用一次,也就是变成了每秒刷新30次。
pause属性:控制CADisplayLink的运行。当我们想结束一个CADisplayLink的时候,应该调用-(void)invalidate 从runloop中删除并删除之前绑定的 target 跟 selector
timestamp属性: 只读的CFTimeInterval值,表示屏幕显示的上一帧的时间戳,这个属性通常被target用来计算下一帧中应该显示的内容。
targetTimestamp属性:只读的CFTimeInterval值,表示屏幕显示的下一帧的时间戳。
(三)GCD计时器
直接贴代码,
//GCD 创建一个计时器
__block count = 10;
//1.创建调度源
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
//2.设置调度源
dispatch_source_set_timer(timer, 0, 0, 0);
//3.设置调度方法
dispatch_source_set_event_handler(timer, ^{
count--;
if (count == 0) {
dispatch_source_cancel(timer);
NSLog(@"%@",[NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"-------%@",[NSThread currentThread]);
});
}
});
//只有dispatch_source_cancel 这个方法调用之后,才会调用dispatch_source_set_cancel_handler 这个方法中内容
dispatch_source_set_cancel_handler(timer, ^{
NSLog(@"source cancel");
});
//启动资源
dispatch_resume(timer);
(四)CADisplayLink 与 NSTimer的不同点?
1.原理不同
CADisplayLink是一个让我们以屏幕刷新频率将内容绘制到屏幕上的定时器,当创建一个CADisplayLink,必须手动以特定模式添加到RunLoop中,每当屏幕显示内容刷新结束的时候,RunLoop就会向CADisplayLink指定的target发送一次指定的selector消息, CADisplayLink类对应的selector就会被调用一次。
NSTimer就是一个一定时间间隔之后调用target的selector消息的计时器,当创建一个timer,可以手动以特定模式添加也可以自动添加到RunLoop中,设置一定时间间隔后,给绑定的target发送一次指定的selector消息,分为一次计时器跟重复计时器。
2.周期设置方式不同
iOS设备的屏幕刷新频率(FPS)是60Hz,因此CADisplayLink的selector 默认调用周期是每秒60次,但是刷新频率可以通过CADisplayLink属性frameInterval设置,CADisplayLink的selector每秒调用次数=60/ frameInterval。比如当 frameInterval设为2,每秒调用就变成30次。因此, CADisplayLink 周期的设置方式略显不便
NSTimer的selector调用周期可以在初始化时直接设定,相对就灵活的多
3.精确度不同
iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。
NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,RunLoop如果在阻塞状态,触发时间就会推迟到下一个RunLoop周期。并且 NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间的延迟范围。
4.使用场景
CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。
NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。
再次感谢各位大神优秀的文章分享,上面是我的一个总结,如有错误,欢迎大家批评指正,促进进步,下面是参考文章:
https://www.jianshu.com/p/0994b907450d
https://www.cnblogs.com/mddblog/p/6517377.html
https://www.cnblogs.com/oc-bowen/p/6000422.html
网友评论