IOS 定时器
NSTimer
GCD定时器
dispatch_after
-(void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
CADisplayLink
NSTimer
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
这三个方法直接将timer添加到了当前runloop default mode,而不需要我们自己操作,当然这样的代价是runloop只能是当前runloop,模式是default mode:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui repeats:(BOOL)rep;
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
下面五种创建,不会自动添加到runloop,还需调用addTimer:forMode:
坑一:子线程启动定时器问题:
我们都知道iOS是通过runloop作为消息循环机制,主线程默认启动了runloop,可是子线程没有默认的runloop,因此,我们在子线程启动定时器是不生效的。解决的方式也简单,在子线程启动一下runloop就可以了。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSTimer* timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(Timered:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
});
坑二:runloop的mode问题:
我们注意到schedule方式启动的timer是add到runloop的NSDefaultRunLoopMode中,这就会出现其他mode时timer得不到调度的问题。最常见的问题就是在UITrackingRunLoopMode,即UIScrollView滑动过程中定时器失效。
解决方式就是把timer add到runloop的NSRunLoopCommonModes。UITrackingRunLoopMode和kCFRunLoopDefaultMode都被标记为了common模式,所以只需要将timer的模式设置为NSRunLoopCommonModes,就可以在默认模式和追踪模式都能够运行。
坑三:循环引用问题:
究其原因,就是NSTimer的target被强引用了,而通常target就是所在的控制器,他又强引用的timer,造成了循环引用
不是所有的NSTimer都会造成循环引用。就像不是所有的block都会造成循环引用一样。以下两种timer不会有循环引用:
非repeat类型的。非repeat类型的timer不会强引用target,因此不会出现循环引用。
block类型的,新api。iOS 10之后才支持,因此对于还要支持老版本的app来说,这个API暂时无法使用。当然,block内部的循环引用也要避免。
不是解决了循环引用,target就可以释放了,别忘了在持有timer的类dealloc的时候执行invalidate。
NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(Timered:) userInfo:nil repeats:YES];
这个timer并没有被self引用,那么为什么self不会被释放呢?因为timer被加到了runloop中,timer又强引用了self,所以timer一直存在的话,self也不会释放。
**如何解决NSTime的循环引用 **
- 在合适的时机关闭定时器
- 自定义category使用Block解决
- 给
self
添加中间件proxy
GCD定时器
GCD定时器的好处是,他并不是加入runloop执行的,因此子线程也可以使用。也不会引起循环引用的问题。
WS(weakSelf);
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC, 1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
[weakSelf commentAnimation];
});
dispatch_resume(timer);
dispatch_after
dispatch_after内部也是使用的Dispatch Source。因此也避免了NSTimer的很多坑。
performSelector
这种方式通常是用于在延时后去处理一些操作,其内部也是基于将timer加到runloop中实现的。因此也存在NSTimer的关于子线程runloop的问题。
CADisplayLink
let cadTimer = CADisplayLink(target: self, selector: #selector(timerAction))
// 单位是帧,屏幕刷新多少帧时调用一次selector
// 默认值为0,选择使用设备的最高屏幕刷新频率(iOS为60次每秒)
// 所以执行时间间隔为:preferredFramesPerSecond * 最高屏幕刷新间隔,如iOS设备:preferredFramesPerSecond * 1/60 秒
cadTimer.preferredFramesPerSecond = 20
// 注册到runLoop中监听 加进去才会执行
cadTimer.add(to: RunLoop.current, forMode: .default)
// 挂起
// cadTimer.isPaused = true
// 终止
cadTimer?.invalidate()//从runloop移除从而停止了
cadTimer = nil
- 屏幕刷新时调用
CADisplayLink
是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。CADisplayLink
以特定模式注册到runloop
后,每当屏幕显示内容刷新结束的时候,runloop
就会向CADisplayLink
指定的target
发送一次指定的selector
消息,CADisplayLink
类对应的selector
就会被调用一次。所以通常情况下,按照iOS设备屏幕的刷新率60次/秒
- 延迟
iOS
设备的屏幕刷新频率是固定的,CADisplayLink
在正常情况下会在每次刷新结束都被调用,精确度相当高。但如果调用的方法比较耗时,超过了屏幕刷新周期,就会导致跳过若干次回调调用机会。
如果CPU过于繁忙,无法保证屏幕60次/秒
的刷新率,就会导致跳过若干次调用回调方法的机会,跳过次数取决CPU
的忙碌程度。
- 延迟
- 使用场景
从原理上可以看出,CADisplayLink
适合做界面的不停重绘,比如视频播放的时候需要不停地获取下一帧用于界面渲染