1. 简介
iOS常用的计时器大概有三种,分别是:NSTimer、CADisplayLink、dispatch_source_t。以及NSDelayedPerforming、dispatch_after两种延时执行的机制。本文只介绍他们基本的用法以及使用过程中注意的问题。
2. 计时器
2.1 NSTimer
NSTimir的8种系统初始化方法在使用过程中容易出现循环引用导致内存泄漏的问题,我在这篇文章中有详细的说明。
关于这个问题YYKit中做了很好的处理。借助在NSTimer+YYAdd
与YYWeakProxy
我们可以轻易的规避这些问题。
2.1.1 开启定时器
2.1.1.1 方法一
需要引入
NSTimer+YYAdd
__weak typeof(self) weakSelf = self;
_yyTimer = [NSTimer scheduledTimerWithTimeInterval:1 block:^(NSTimer * _Nonnull timer) {
NSLog(@"定时器触发, %@", weakSelf);
} repeats:YES];
2.1.1.2 方法二
需要引入
YYWeakProxy
- (void)startTimer{
//初始化代理
YYWeakProxy* wProxy = [[YYWeakProxy alloc] initWithTarget:self];
//开启定时器
_yyTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:wProxy selector:@selector(timerAction) userInfo:nil repeats:YES];
}
- (void)timerAction {
NSLog(@"定时器触发, %@", self);
}
2.1.2 销毁定时器
//可以在任意需要停止的时刻销毁定时器。eg:在dealloc方法中销毁
- (void)dealloc {
if (_yyTimer){
[_yyTimer invalidate];
_yyTimer = nil;
}
}
2.2 CADisplayLink
2.2.1 简介
CADisplayLink是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。我们在应用中创建一个新的 CADisplayLink 对象,把它添加到一个runloop中,并给它提供一个 target 和 selector 在屏幕刷新的时候调用。
2.2.2 属性说明
duration:提供了每帧之间的时间,也就是屏幕每次刷新之间的的时间。该属性在target的selector被首次调用以后才会被赋值。selector的调用间隔时间计算方式是:时间=duration×frameInterval。 我们可以使用这个时间来计算出下一帧要显示的UI的数值。但是 duration只是个大概的时间,如果CPU忙于其它计算,就没法保证以相同的频率执行屏幕的绘制操作,这样会跳过几次调用回调方法的机会。
timestamp: 只读的CFTimeInterval值,表示屏幕显示的上一帧的时间戳,这个属性通常被target用来计算下一帧中应该显示的内容。 打印timestamp值,其样式类似于:179699.631584。
pause:控制CADisplayLink的运行。当我们想结束一个CADisplayLink的时候,应该调用-(void)invalidate 从runloop中删除并删除之前绑定的 target 跟 selector。
frameInterval:是可读可写的NSInteger型值,标识间隔多少帧调用一次selector 方法,默认值是1,即每帧都调用一次。如果每帧都调用一次的话,对于iOS设备来说那刷新频率就是60HZ也就是每秒60次,如果将 frameInterval 设为2 那么就会两帧调用一次,也就是变成了每秒刷新30次。
2.2.3 开启定时器
- (void)startDisplayLink{
//初始化代理
YYWeakProxy* wProxy = [[YYWeakProxy alloc] initWithTarget:self];
//初始化定时器
_displayLink = [CADisplayLink displayLinkWithTarget:wProxy selector:@selector(displayLinkAction)];
//添加到 Runloop 中
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
//定时执行方法
- (void)displayLinkAction{
}
2.2.4 销毁定时器
- (void)dealloc {
if (_displayLink){
[_displayLink invalidate];
_displayLink = nil;
}
}
注意: CADisplayLink 不能被继承。
2.3 CADisplayLink 与 NSTimer 的不同
2.3.1 原理不同
CADisplayLink是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。 CADisplayLink以特定模式注册到runloop后, 每当屏幕显示内容刷新结束的时候,runloop就会向 CADisplayLink指定的target发送一次指定的selector消息, CADisplayLink类对应的selector就会被调用一次。
NSTimer以指定的模式注册到runloop后,每当设定的周期时间到达后,runloop会向指定的target发送一次指定的selector消息。
2.3.2 周期设置方式不同
iOS设备的屏幕刷新频率(FPS)是60Hz,因此CADisplayLink的selector 默认调用周期是每秒60次,这个周期可以通过frameInterval属性设置, CADisplayLink的selector每秒调用次数=60/ frameInterval。比如当 frameInterval设为2,每秒调用就变成30次。因此, CADisplayLink 周期的设置方式略显不便。
NSTimer的selector调用周期可以在初始化时直接设定,相对就灵活的多。
2.3.3 精确度不同
iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。
NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。并且 NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间的延迟范围。
2.3.4 使用场景
CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。
NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。
2.4 dispatch_source_t
2.4.1 简介
NSTimer受runloop的影响,由于runloop需要处理很多任务,导致NSTimer的精度降低,在日常开发中,如果我们需要对定时器的精度要求很高的话,可以考虑dispatch_source_t去实现 。dispatch_source_t精度很高,系统自动触发,系统级别的源。
2.4.2 使用方法
//创建定时器
- (void)createSourceTimer{
//创建全局队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//使用全局队列创建计时器
_sourceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//设置定时器间隔时间
NSTimeInterval timeInterval = 1.0f;
//设置定时器延迟(开始)时间
NSTimeInterval delayTime = 1.0f;
dispatch_time_t startDelayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayTime * NSEC_PER_SEC));
//设置计时器
dispatch_source_set_timer(_sourceTimer,startDelayTime,timeInterval*NSEC_PER_SEC,0.1*NSEC_PER_SEC);
//定期执行事件
__weak typeof(self) weakSelf = self;
dispatch_source_set_event_handler(_sourceTimer,^{
NSLog(@"定期执行的 block %@", weakSelf);
});
//销毁定时器时执行的 block,调用dispatch_source_cancel时触发
dispatch_source_set_cancel_handler(_sourceTimer, ^{
NSLog(@"销毁定时器时执行的 block %@", weakSelf);
});
//启动计时器
dispatch_resume(_sourceTimer);
}
//销毁定时器
- (void)destoryTimer{
dispatch_source_cancel(_sourceTimer);
}
2.4.3 封装拓展
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
YYTimer is a thread-safe timer based on GCD. It has similar API with `NSTimer`.
YYTimer object differ from NSTimer in a few ways:
* It use GCD to produce timer tick, and won't be affected by runLoop.
* It make a weak reference to the target, so it can avoid retain cycles.
* It always fire on main thread.
*/
@interface YYTimer : NSObject
+ (YYTimer *)timerWithTimeInterval:(NSTimeInterval)interval
target:(id)target
selector:(SEL)selector
repeats:(BOOL)repeats;
- (instancetype)initWithFireTime:(NSTimeInterval)start
interval:(NSTimeInterval)interval
target:(id)target
selector:(SEL)selector
repeats:(BOOL)repeats NS_DESIGNATED_INITIALIZER;
@property (readonly) BOOL repeats;
@property (readonly) NSTimeInterval timeInterval;
@property (readonly, getter=isValid) BOOL valid;
- (void)invalidate;
- (void)fire;
@end
#import "YYTimer.h"
#import <pthread.h>
#define LOCK(...) dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \
__VA_ARGS__; \
dispatch_semaphore_signal(_lock);
@implementation YYTimer {
BOOL _valid;
NSTimeInterval _timeInterval;
BOOL _repeats;
__weak id _target;
SEL _selector;
dispatch_source_t _source;
dispatch_semaphore_t _lock;
}
+ (YYTimer *)timerWithTimeInterval:(NSTimeInterval)interval
target:(id)target
selector:(SEL)selector
repeats:(BOOL)repeats {
return [[self alloc] initWithFireTime:interval interval:interval target:target selector:selector repeats:repeats];
}
- (instancetype)init {
@throw [NSException exceptionWithName:@"YYTimer init error" reason:@"Use the designated initializer to init." userInfo:nil];
return [self initWithFireTime:0 interval:0 target:self selector:@selector(invalidate) repeats:NO];
}
- (instancetype)initWithFireTime:(NSTimeInterval)start
interval:(NSTimeInterval)interval
target:(id)target
selector:(SEL)selector
repeats:(BOOL)repeats {
self = [super init];
_repeats = repeats;
_timeInterval = interval;
_valid = YES;
_target = target;
_selector = selector;
__weak typeof(self) _self = self;
_lock = dispatch_semaphore_create(1);
_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(_source, dispatch_time(DISPATCH_TIME_NOW, (start * NSEC_PER_SEC)), (interval * NSEC_PER_SEC), 0);
dispatch_source_set_event_handler(_source, ^{[_self fire];});
dispatch_resume(_source);
return self;
}
- (void)invalidate {
dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
if (_valid) {
dispatch_source_cancel(_source);
_source = NULL;
_target = nil;
_valid = NO;
}
dispatch_semaphore_signal(_lock);
}
- (void)fire {
if (!_valid) return;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
id target = _target;
if (!target) {
dispatch_semaphore_signal(_lock);
[self invalidate];
} else {
dispatch_semaphore_signal(_lock);
[target performSelector:_selector withObject:self];
if (!_repeats) {
[self invalidate];
}
}
#pragma clang diagnostic pop
}
- (BOOL)repeats {
LOCK(BOOL repeat = _repeats); return repeat;
}
- (NSTimeInterval)timeInterval {
LOCK(NSTimeInterval t = _timeInterval) return t;
}
- (BOOL)isValid {
LOCK(BOOL valid = _valid) return valid;
}
- (void)dealloc {
[self invalidate];
}
@end
2.5 NSDelayedPerforming
2.5.1 使用方法
//设置延迟执行,delay单位为秒
//在指定的某些mode下
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
//在当前mode下
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
//取消对应的的延迟执行。需要注意的是参数的一致性,否则无法取消
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
//取消所有的延迟执行
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;
2.5.2 使用过程中需要注意的问题
Perform Delay 的实现原理就是一个不循环(repeat 为 NO)的 timer,所以使用这两个接口的注意事项跟使用 timer 类似。
2.5.2.1 取消时的传参
取消对应的的延迟执行。需要注意的是参数的一致性,否则无法取消。
//开启延时执行
[self performSelector:@selector(delayPerform:) withObject:@(0) afterDelay:1.0f];
//无法取消
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(delayPerform:) object:nil];
//可以取消
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(delayPerform:) object:@(1)];
[NSObject cancelPreviousPerformRequestsWithTarget:self];
//延时执行方法
- (void)delayPerform:(NSNumber*)param{
}
2.5.2.2 可能无法触发
在非主线程使用的时候,需要保证线程的runloop是运行的,否则不会执行。或者切回主线程中使用。
2.5.2.3 内存问题
需要在适当的地方调用取消的方法,避免循环引用导致的内存泄漏或者造成内存问题(实例都释放了还在调用实例方法)导致crash。具体可以参考这篇文章,如果有更好的解决方法或者文章推荐,欢迎在评论区留言。
2.6 dispatch_after
GCD中dispatch_after方法也可以实现延迟。而且不会阻塞线程,效率较高,并且可以在参数中选择执行的线程,但是无法取消。
//设置延时时长
CGFloat delayTime = 3.f;
//开启延时
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (self){
NSLog(@"定时器触发, %@", self);
}
});
注意:如果延时执行的block还没有执行,当前的控制器就 pop 的情况下。使用了 self 的话, 就只能在执行了这个 block 之后,当前的 self 才能被销毁.
2.7 UIView动画实现延时
UIView可以实现动画延迟,延时操作写在block里面。这里需要说明的是,block中的代码对于是支持animation的代码,才会有延迟效果,对于不支持animation的代码不会有延迟效果。
[UIView animateWithDuration:1.f delay:2.f options:UIViewAnimationOptionCurveLinear animations:^{
//延时执行的block
} completion:^(BOOL finished) {
//执行完毕
}];