一、创建定时器
(1)动态方法创建:
A:Target/Action方式
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;
//获取当前时间
NSDate *date = [NSDate date];//[[NSDate alloc]init];
NSTimer *timer = [[NSTimer alloc]initWithFireDate:date interval:2 target:self selector:@selector(nextPage) userInfo:nil repeats:YES];
//从当前时间开始,以后每隔2秒就调用一次当前类(self)的nextpage方法
(1)参数Date:从何时开始(立即调用?3秒以后?还是某个确定的时间点?),也是响应方法的首次调用时刻(添加进mode后是立即调用还是隔多少秒以后掉用);
(2)参数Repeat:是否重复调用响应方法。如果为NO,表示只调用一次响应方法;
B:Block方式
- (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));
NSTimer *timer = [[NSTimer alloc]initWithFireDate:[NSDate date] interval:2.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"我被调用了!");
}];
//从当前时刻开始(先调用一次),以后每隔2秒打印一次“我被调用了!”
(2)静态方法创建:
A:Target/Action方式
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(nextPage) userInfo:nil repeats:YES];
/*默认立即开始(立即调用相应方法),当然可以重设首次调用时刻,如:
timer.fireDate = [NSDate dateWithTimeIntervalSinceNow:3];
*/
B:Block方式
+ (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 *timer = [NSTimer timerWithTimeInterval:2.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"我被调用了!");
}];
C:Invocation方式
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
不常用,待补充。。。
二、开启定时器
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
注册timer
从这里可以看出,the receiver似乎指的是runloop。
(1)只有将在RunLoop注册过的NSTimer添加进指定mode中,timer才能被触发(也即你只需要将timer添加进runloop中即可)。所以以上两大类方式创建的timer是不能被触发的。
(2)为了将“安装”进modes中timer移除,我们需要向timer发送invalidate消息。
(3)mode:runloop的运行模式,runloop有多种运行模式,它需要知道timer处于哪种模式下才有效。
三、创建+开启一并执行
A:Target/Action方式
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(nextPage) userInfo:nil repeats:YES];
B:Block方式
+ (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 scheduledTimerWithTimeInterval:2.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"我被调用了!");
}];
C:Invocation方式
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
不常用,待补充。。。
注意:这三种方式创建的timer并没有指定fireDate,所以它们不是添加进runloop后就立即调用一次的,要想一进runloop就调用,需要立即调用fire方法。
四、启动与失效
//timer是否“有效”
@property (readonly, getter=isValid) BOOL valid;
//schedule方式创建的timer时不需要额外开启的,它是自动添加进runloop中并自动开启的,但注意:它不能保证一进runloop就立即调用,fire方法可以做到。
- (void)fire;
fire:使添加进NSRunLoop的timer立即执行timerSelector方法。
//让“有效”的timer变得失效
- (void)invalidate;
- (void)invalidate;
(1)Stops the timer from ever firing again and requests its removal from its run loop.(永远阻止timer重启,并且将其从RunLoop中移除)
(2)《1》This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.(它是从NSRunLoop对象中移除一个timer的仅有的方法。或者在invalidate方法返回前,或者其它时刻,该NSRunLoop对象会移除对这个timer的引用)《2》If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.(如果timer被绑定了target和info对象,the receiver也会移除对它们的强引用)(额,这个the receiver指的是谁呢?target?不像啊,自己对自己移除强引用?难道是runloop?哪位大神给解读下。。。从注册timer时的解读The timer to register with the receiver.
来看,the receiver似乎指的是RunLoop本身,register with :向...注册/登记)
所以,当timer执行完invalidate方法后,这个timer就永远不能用了,如果需要定时操作,只能重新创建timer对象。故,invalidate的意义不应该仅仅理解为“暂停”,应该理解为“注销”(从runloop对象中注销)
五、NSTimer与ViewController的引用
图片一图片二正常情况下,当控制器返回后就销毁了。
如图二,当加入NSTimer后,再返回第一界面时发现SecondViewController没有被销毁!那只能说明定时器的加入引用了SecondViewController,导致其不能被销毁;
那到底哪里出错了呢?
target: The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to this object until it (the timer) is invalidated.
很显然,创建定时器时指定的target(self)导致了定时器对SecondViewController的引用(我不关心我强引用了谁,大不了最后一块儿销毁你们。劳资关心的是你们谁TM强引用了我,你们引用了我让我怎么销毁。)
将self.timer置为nil会不会让SecondViewController的销毁恢复正常呢?
图片三
很显然嘛,我不能靠“放了你们”而去期待你们也能“放过我”
那如果让定时器失效呢?能否让SecondViewController的销毁恢复正常呢?
图片四
可以!(为了大义,你们自裁吧。。。)当然最好执行
self.timer = nil;
,将对象置为nil是一种规范和习惯,其实它才是导致timer被真正销毁timer的原因。
额。。。其实还有ViewController被销毁了还在执行任务的NSTimer,执行任务也是好性能的嘛。
图片五综上所述,当将timer添加至mode中时,它们的引用关系如下:
引用关系图
如理解有误还望大神指正,在此谢过。
其实,它能执行任务是因为自己没有被销毁;ViewController能被销毁,是因为该timer并没有引用ViewController(和控制器没有任何联系的timer又有什么用呢?)
六、NSTimer与NSRunLoopMode
线程的RunLoop有五种mode,但常用的NSRunLoopMode有两种:
(1)NSDefaultRunLoopMode:主线程RunLoop开启后的常驻mode
(2)UITrackingRunLoopMode:界面跟踪mode,主要用于处理UIScrollView及其子类的滚动事件。
(1)每个RunLoop有五种mode,它可以很快的在各个mode间完成切换以处理对应mode中的事件。
(2)如果NSTimer当前处于NSDefaultRunLoopMode中,此时界面上有滚动事件发生,则RunLoop会瞬间切换至UITrackingRunLoopMode模式。这意味着NSTimer会被暂停调用响应方法,直至滚动事件被处理完,当滚动事件被处理完后,RunLoop又会被瞬间切换至NSDefaultRunLoopMode模式,此时线程会查看该mode下是否有相应事件等待处理,有则继续,没有的话,runloop会进入休眠状态,直至被事件再次唤醒。
(3)占位mode:NSRunLoopCommonModes,它包含以上两种mode,处于该mode下的事件会在两种mode下都有效。也即mode切换过程中不会中断事件的处理。
(4)如果想让NSTimer同时在两种mode下都有效,该怎么办呢?
答:要么将该NSTimer单独添加至两种mode下,要么一次性加入两种mode下NSRunLoopCommonModes。
(5)Mode应用场景:轮播时,如果想拖拽查看某一页时暂停定时器;上下滑动UITableView时,不影响分页滚动界面的自动滚动。
(6)正常情况下,轮播时,定时器会自动暂停等待,不需要额外的暂停操作,但是当拖拽事件结束后,会发现恢复自动滚动瞬间,滚动的比较快,影响体验;所以正确的做法是先invalidate & nil,拖拽结束后再重新创建NSTimer。
总结:
(1)timer的作用域只是决定了timer被使用的范围,也即局部的timer并不会影响响应方法的重复调用(因为走出了方法,局部变量就被销毁了),只是其他方法无法使用到而已。
(2)timer只有被加入到runloop中才能被触发。
(3)NSTimer创建的实例timer,响应时刻并不会那么精确,只是大概准确。
(4)正常情况下,控制器的销毁是不受其他对象的,这时通常我们会在dealloc方法中做一些控制器被销毁前的“临终”操作,比如销毁一些我们自己创建的对象。如果发现控制器没有走dealloc方法,那只能说明控制器还在被强引用,所以平时我们需要清晰的知道对控制器的强引用都有哪些情况。
(5)定时器的加入,会影响当前控制器的正常销毁流程,因为runloop内部持有了当前控制器,有且仅有定时器被销毁才能结束对控制器的引用。
(6)当然,并非创建的所有的NSTimer都会强引用ViewController,比如repeats为NO的定时器就不会,还有block方式创建的定时器。
(7)当然,销毁定时器的操作所放位置也至关重要。如果放在dealloc方法里就完了,因为那样dealloc根本不会被执行!所以我们必须在dealloc方法执行之前要有invalidate操作。
网友评论