1.NSTimer
iOS中最基本的定时器。其通过RunLoop来实现,一般情况下较为准确,但当当前循环耗时操作较多时,会出现延迟问题。同时,也受所加入的RunLoop的RunLoopMode影响。
NSTimer的类方法
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(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 *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
//iOS10.0之后可以用的方法
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
NSTimer的对象方法
这两个方法多了一个参数date,这个参数可以指定定时器什么时候开启,创建完之后需要手动加入Runloop
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;
- (void)fire;
//iOS10.0之后可以使用
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
可以看到定时器创建的类方法都分为invocation和selector两种方式。
1.1Timer定时器的创建
1.1.1传NSInvocation方法创建定时器
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:@selector(timered)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = self;
invocation.selector = @selector(timered);
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 invocation:invocation repeats:YES];
- (void)timered{
NSLog(@"定时器被调用");
}
1.1.2传SEL方式创建定时器
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timered) userInfo:nil repeats:YES];
1.1.3block方式创建定时器
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"定时器被调用");
}];
1.1.4NSTimer的fire方法
调用了fire方法之后会立即执行定时器的方法,fire方法不会改变预定周期性调度。即调用完fire方法之后不会从当前时间重新开始计算时间间隔,还是会从上一次计算时间间隔。
定时器如果不循环调用,提前调用了fire方法,不会在时间到了之后在调用一次,因为执行一次之后任务就结束了。
NSLog(@"当前时间");
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(timered) userInfo:nil repeats:YES];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[timer fire];
});
运行结果:
2020-03-02 17:54:02.436750+0800 Test[46183:3046157] 当前时间
2020-03-02 17:54:07.438237+0800 Test[46183:3046157] 定时器被调用
2020-03-02 17:54:12.439445+0800 Test[46183:3046157] 定时器被调用
定时器时间间隔设置了10秒,第五秒的时候调用了一次fire方法,定时器第二次调用是在第10秒的时候,而不是15秒的时候。
定时器如果不循环调用,提前调用了fire方法,不会在时间到了之后在调用一次,因为执行一次之后任务就结束了。
NSLog(@"当前时间");
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(timered) userInfo:nil repeats:NO];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[timer fire];
});
运行结果:
2020-03-02 17:56:39.859278+0800 Test[46250:3050494] 当前时间
2020-03-02 17:56:44.860940+0800 Test[46250:3050494] 定时器被调用
定时器在5秒之后调用一次就不再调用。
1.1.5timerWithTimeInterval和scheduledTimerWithTimeInterval的区别
scheduledTimerWithTimeInterval创建的时候就已经添加到runloop,
通过timerWithTimeInterval创建定时器之后需要手动添加到runloop才能开始运行,因为定时器的运行是依赖runloop的,xcode中对方法的解释
scheduledTimerWithTimeInterval
Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
timerWithTimeInterval
Creates and returns a new NSTimer object initialized with the specified block object. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
1.2定时器循环引用出现的原因
把定时器在一个控制器中创建,在控制器的dealloc方法中销毁定时器。
通过NSInvocation、selector和block三种方式创建定时器,分别在控制器的dealloc中销毁定时器
- (void)dealloc{
NSLog(@"控制器被销毁");
[self.timer invalidate];
self.timer = nil;
}
运行发现,当控制器被pop时,只有通过block方式创建的定时器会调用“控制器被销毁”。说明其他两种方式都会造成控制器无法释放,造成内存泄漏。
如果把是否循环的参数改成NO,表示定时器只执行一次,则控制器也能被销毁。
1.2.1定时器造成循环引用的原因
因为定时器要被加到runloop中才能运行,所以定时器被runloop强引用,因为定时器在运行时需要调用传入的target的方法,target一般都是控制器,所以定时器强引用了控制器,定时器的销毁又放在控制器的dealloc方法中,所以一直无法释放。
1.2.2 不会造成循环引用的情况
- 1.非重复计时器,即repeat参数传NO的,因为执行完一次之后会直接失效。相当于调用了invalidate方法,runloop会把定时器移除。所以控制器就可以被销毁了。苹果文档描述如下
//非重复计时器在触发后立即使自身失效。
A nonrepeating timer invalidates itself immediately after it fires.
- 2.通过iOS10.0之后的新方法,定时器调用block,方法的介绍中描述如下
//阻塞定时器执行体;在执行时,计时器本身作为参数传递给这个块,以帮助避免循环引用
- parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
1.3循环引用的解决
由于造成循环引用的原因是runloop强引用NSTimer,NSTimer强引用控制器,所以我们可以让NSTimer不再强引用控制器,即在创建NSTimer的时候传入的target为另一个对象,用来相应定时器。
具体步骤:
1.创建类YYTimerResponse类,在类中时间NSTimer的调用方法"- (void)timered"
#import "YYTimerResponse.h"
@implementation YYTimerResponse
- (void)timered{
NSLog(@"在YYTimerResponse中定时器被调用");
}
- (void)dealloc{
NSLog(@"YYTimerResponse被销毁");
}
@end
2.在控制器中添加定时器,并且在dealloc中销毁定时器
- (void)viewDidLoad {
[super viewDidLoad];
YYTimerResponse *timeResponse = [[YYTimerResponse alloc] init];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:timeResponse selector:@selector(timered) userInfo:nil repeats:YES];
}
- (void)dealloc{
NSLog(@"控制器被销毁");
[self.timer invalidate];
self.timer = nil;
}
运行之后定时器开始调用,控制器返回之后打印如下
2020-03-02 18:47:46.879168+0800 Test[47229:3134132] 在YYTimerResponse中定时器被调用
2020-03-02 18:47:47.879918+0800 Test[47229:3134132] 在YYTimerResponse中定时器被调用
2020-03-02 18:47:48.116563+0800 Test[47229:3134132] 控制器被销毁
2020-03-02 18:47:48.116796+0800 Test[47229:3134132] YYTimerResponse被销毁
因为此时不再强引用控制器,所以当控制器返回时,控制器的dealloc方法被调用,在dealloc方法中调用了[self.timer invalidate];,所以NSTimer被移除runloop,控制器被销毁所以控制器也不强引用NSTimer了,所以NSTimer也要被释放,所以YYTimerResponse也被销毁。
1.4在子线程启动定时器
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"当前线程%@", [NSThread currentThread]);
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timered) userInfo:nil repeats:YES];
});
- (void)timered{
NSLog(@"定时器被调用,当前线程:%@", [NSThread currentThread]);
}
运行结果:
2020-03-02 19:04:50.483133+0800 Test[47650:3162147] 当前线程{number = 3, name = (null)}
运行之后发现定时器没有执行
因为子线程的runloop默认没有开启
所以需要在创建完定时器之后调用“[[NSRunLoop currentRunLoop] run];”,开启定时器。
因为子线程也可以强引用NSTimer,所以此时控制器也不会被销毁,所以还是需要像1.3中一样解决循环引用的问题
所以代码修改如下:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"当前线程%@", [NSThread currentThread]);
YYTimerResponse *timeResponse = [[YYTimerResponse alloc] init];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:timeResponse selector:@selector(timered) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
});
运行结果:发现控制器pop之后,定时器还是无法停止
把target设置为其他对象后,控制器的dealloc方法仍然无法调用的原因:因为在开启异步操作的block中强引用了self,即子线程的runloop强引用了控制器,所以控制器无法被销毁,把self弱引用。
所以最终代码如下:
-(void)viewDidLoad{
[super viewDidLoad];
__weak __typeof__(self) weakSelf = self;
self.timeResponse = [[YYTimerResponse alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"当前线程%@", [NSThread currentThread]);
weakSelf.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf.timeResponse selector:@selector(timered) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
weakSelf.thread = [NSThread currentThread];
});
}
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
self.timeResponse = nil;
NSLog(@"控制器被销毁");
}
打印结果:
2020-03-03 16:03:48.477618+0800 Test[55881:3549309] 当前线程{number = 3, name = (null)}
2020-03-03 16:03:49.479146+0800 Test[55881:3549309] 在YYTimerResponse中定时器被调用,当前线程{number = 3, name = (null)}
2020-03-03 16:03:50.480857+0800 Test[55881:3549309] 在YYTimerResponse中定时器被调用,当前线程{number = 3, name = (null)}
2020-03-03 16:03:51.483406+0800 Test[55881:3549309] 在YYTimerResponse中定时器被调用,当前线程{number = 3, name = (null)}
2020-03-03 16:03:53.939806+0800 Test[55881:3549256] YYTimerResponse被销毁
2020-03-03 16:03:53.939990+0800 Test[55881:3549256] 控制器被销毁
在上面的代码中,除了把self弱引用之外,还手动把YYTimerResponse对象设置为nil,因为YYTimerResponse对象也被强引用了,无法销毁。
1.5定时器加入Runloop的模式(有时无法响应)
在进行UI交互的时候(如tableView的滑动时),runloop所在的模式是UITrackingRunLoopMode,而在把定时器默认加入runloop的时候会加入"NSDefaultRunLoopMode"
runloop执行任务时会在Mode间切换,所以在UI交互时无法响应定时器。
1.5.1解决方法1: 再加入runloop时把模式设置为NSRunLoopCommonModes
把定时器添加到runloop时模式设置为"NSRunLoopCommonModes",这个模式并不是一种Mode,而是一种特殊的标记,关联的有一个set(默认包含NSDefaultRunLoopMode、NSModalPanelRunLoopMode、NSEventTrackingRunLoopMode) 。
代码:
YYTimerResponse *timeResponse = [[YYTimerResponse alloc] init];
self.timer = [NSTimer timerWithTimeInterval:1 target:timeResponse selector:@selector(timered) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
1.5.3解决方法2:把定时器加入子线程
由于UI交互是在主线程,所以把定时器加入子线程的Runloop,就不用管加入的模式,因为是两个runloop,没有关联。
2.GCD中的定时器
GCD中的Dispatch Source其中的一种类型是DISPATCH_SOURCE_TYPE_TIMER,可以实现定时器的功能。
dispatch源监听系统内核对象并处理,其可以实现更加精准的定时效果。
GCD的定时器不是通过runloop实现的,所以不会被runloop强引用,需要当前对象强引用,否则会直接被释放。因此GCD的定时器也没有循环引用的问题。
使用步骤:
NSLog(@"当前时间");
//设置定时器回调执行所在的队列
dispatch_queue_t queue = dispatch_get_main_queue();
//创建dispatch_source_t类型的对象gcdTimer
self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//设置定时器开始时间,2秒后
dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
//时间间隔
uint64_t intervalTime = (int64_t)(1 * NSEC_PER_SEC);
//允许误差时间,设置为0即不允许有误差
uint64_t errorTime = 0;
//按照上面的参数设置定时器
dispatch_source_set_timer(self.gcdTimer, startTime, intervalTime, errorTime);
//设置定时器的回调
dispatch_source_set_event_handler(self.gcdTimer, ^{
NSLog(@"GCD定时器调用");
});
//启动定时器
dispatch_resume(self.gcdTimer);
启动定时器,然后返回控制器,打印结果如下
2020-03-03 17:24:33.348304+0800 Test[57422:3669949] 当前时间
2020-03-03 17:24:35.348971+0800 Test[57422:3669949] GCD定时器调用,当前线程{number = 1, name = main}
2020-03-03 17:24:36.348850+0800 Test[57422:3669949] GCD定时器调用,当前线程{number = 1, name = main}
2020-03-03 17:24:37.303353+0800 Test[57422:3669949] 控制器被销毁
由打印结果可以知道GCD的定时器不会引起循环引用。
暂停GCD定时器:
dispatch_suspend(self.gcdTimer);
取消GCD定时器:
dispatch_cancel(self.gcdTimer);
GCD定时器暂停之后仍然可以继续执行,NSTimer则不可以,NSTimer只能直接销毁,需要再次启动则需要重建创建NSTimer定时器。
GCD定时器调用了"dispatch_cancel"之后则无法继续执行,通过打印调用“dispatch_cancel”之前和之后gcdTimer对象,可以发现之后对象中会标识出“cancelled”。
注意点:
dispatch_suspend 状态下无法释放
如果调用 dispatch_suspend 后 timer 是无法被释放的。一般情况下会发生崩溃并报“EXC_BAD_INSTRUCTION”错误,看下 GCD 源码dispatch source release 的时候判断了当前是否是在暂停状态。
所以,dispatch_suspend 状态下直接释放当前控制器或者释放定时器,会导致定时器崩溃。
并且初始状态(未调用dispatch_resume)、挂起状态,都不能直接调用dispatch_source_cancel(timer),调用就会导致app闪退。
建议一:尽量不使用dispatch_suspend,在dealloc方法中,在dispatch_resume状态下直接使用dispatch_source_cancel来取消定时器。
建议二:使用懒加载创建定时器,并且记录当timer 处于dispatch_suspend的状态。这些时候,只要在 调用dealloc 时判断下,已经调用过 dispatch_suspend 则再调用下 dispatch_resume后再cancel,然后再释放timer。
参考:iOS中如何正确释放GCD定时器(dispatch_source_t)以及防止Crash?
3. CADisplayLink定时器
CADisplayLink是基于屏幕刷新的周期,所以其一般很准时,每秒刷新60次。其本质也是通过RunLoop,所以当RunLoop选择其他模式或被耗时操作过多时,仍旧会造成延迟。NSTimer中的循环引用问题他也存在。
使用步骤:
//创建接受定时器回调的对象
YYTimerResponse *timeresponse = [[YYTimerResponse alloc] init];
// 创建CADisplayLink
self.displayLink = [CADisplayLink displayLinkWithTarget:timeresponse selector:@selector(timered)];
//设置定时器周期,这个属性即将被废弃, 在iOS10新增了“preferredFramesPerSecond”代替他
// self.displayLink.frameInterval = 60;
self.displayLink.preferredFramesPerSecond = 1;
// 添加至RunLoop中
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
使用“frameInterval”属性设置定时器间隔时,因为屏幕一秒钟刷新60次,所以设置为60,表示一秒钟调用一次。
使用“preferredFramesPerSecond”时,设置的就是几秒钟刷新一次,该属性iOS10.0后才能用。