timer,计时器,就是用来计时的,可以将它和要处理的动作绑定起来,让这个动作在某段时间之后执行,或者周期性地执行。
一、timer的工作原理
timer的工作和run loop密不可分,由于平常我们使用Application Kit和UIKit来新建的app,在app的主线程启动的时候就自动启动了一个runloop,因此在主线程中使用timer感觉不到runloop的存在。如果要在分线程中使用timer,就必须要了解timer和runloop的协同工作原理,自己去新建runloop,让它和timer一起配合工作。
简短概括runloop和timer的工作关系就是,timer只负责计时,timer所绑定的动作,是由runloop来执行的。下面就来详细地讲讲这种关系。
我们知道,runloop运行的时候需要指定运行模式(run loop mode),runloop的运行模式规定了它要监听的事件源,以及事件发生时它要通知的对象。因此可以给runloop定义多种运行模式,runloop运行时,可以同时启用多个运行模式。
一个timer同一时间只可以注册到一个runloop中,但是可以添加到这个runloop的多个运行模式中。(下图表示了这个关系)
初始化一个timer,就是给timer绑定一个要执行的动作;登记一个时间段,这个时间段用来告诉runloop,该动作需要在该时间段过去之后执行,或者每隔这个时间段执行一次。
在timer与runloop的协同工作中,timer只负责计时,而runloop负责监视其所启用的运行模式中添加的timer是否已经达到了其初始化时登记的时间,一旦发现某个timer已经到达了这个时间,就会去执行该timer所绑定的动作(这就是所谓的fire the timer)。
由此可以见得,timer所登记的这个时间段并不是动作的绝对执行时间。timer绑定的动作具体在什么时候执行,要看runloop是否在运行,且是否启用了timer所加入的运行模式,还要看runloop正在处理的东西多不多,能不能及时发现timer已经计时完毕。
二、timer的具体使用
1、创建timer
(1)使用当前的runloop来创建一个timer
方法:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
参数TimeInterval是timer计时的周期长度,每隔时长TimeInterval,计时器就会清零一次。
这两个方法,完成一系列动作:
- 创建timer;
- 将它加入到当前runloop的默认运行模式中;
- 让timer开始计时。
对于这种方式添加的timer,runloop就会等它的计时时长第一次达到TimeInterval时,才会执行它绑定的动作。
(2)自己新建一个timer,之后将它注册到某个runloop中
方法:
+ (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;
这两个方法可以返回一个timer。
之后要用runloop对象的方法将timer注册到这个runloop对象的某个运行模式中:
-(void)addTimer:(nonnull NSTimer *) forMode:(nonnull NSString *);
这样添加到runloop的timer会立即开始计时。
(3)初始化一个timer,并指明它绑定的动作被执行的时间点
方法:
- (instanceType)initWithFireDate:(nonnull NSDate *) interval:(NSTimeInterval) target:(nonnull id) selector:(nonnull SEL) userInfo:(nullable id) repeats:(BOOL);
这里的fireDate是个时间点,是相对于系统时间的,只要到达了这个时间点,runloop就会去执行timer绑定的动作。所以如果timer是一次性的,都不需要计时,interval这个参数就没有作用了。如果是重复的timer,才需要计时,参数interval才有用。
可以用setFireDate方法来给timer设定动作的执行时间。
2、停止timer
timer需要在runloop的管理下才会有效,停止timer,只要把它从它所在的runloop中清理掉就可以实现。(或者让runloop停止运行)。
(1)对于一次性的timer
一次性的timer都不用考虑这个问题,因为runloop执行完timer绑定的动作就会自动把这个timer从它所在的运行模式中清理掉。
(2)对于周期性的timer
方法:
-(void)invalidate;
对于周期性的timer才需要考虑这个问题。
要停止一个周期性的timer,要获取这个timer对象的引用,在需要清理timer的时候,主动调用该timer对象的invalidate方法即可。
当然,也可以对一次性的timer执行这个方法,在timer的动作被runloop执行之前调用invalidate方法,timer的动作就永远不会被执行了。