定时器:A timer waits until a certain time interval has elapsed and then fires, sending a specified message to a target object.
翻译以下:在固定的时间间隔被触发,而后给指定目标发送消息。总结为三要素吧:时间间隔、被触发、发送消息(执行方法)html
按照官方的描述,咱们也确实是这么用的;可是里面有不少细节,你是否了解呢?app
呵呵。。。下面会解决这些问题async
控制器中添加定时器,例如:oop
- (void)viewDidLoad { NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; self.timer = timer; } - (void)timerFire { NSLog(@"timer fire"); }
上面的代码就是咱们使用定时器最经常使用的方式,能够总结为2个步骤:建立,添加到runloopui
系统提供了8个建立方法,6个类建立方法,2个实例初始化方法。atom
当前runloop default mode
,而不须要咱们本身操做,固然这样的代价是runloop只能是当前runloop,模式是default mode:+ (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;
addTimer:forMode:
:+ (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;
对上面全部方法参数作个说明:spa
- (void)timerFireMethod:(NSTimer *)timer
声明[timer userInfo]
获取,也能够为nil,那么[timer userInfo]
就为空添加到runloop,参数timer是不能为空的,不然抛出异常线程
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;
另外,系统提供了一个- (void)fire;
方法,调用它能够触发一次:翻译
如同引言中说的那样,timer必须添加到runloop才有效,很明显要保证两件事情,一是runloop存在(运行),另外一个才是添加。确保这两个前提后,还有runloop模式的问题。代理
一个timer能够被添加到runloop的多个模式,好比在主线程中runloop通常处于NSDefaultRunLoopMode
,而当滑动屏幕的时候,好比UIScrollView
或者它的子类UITableView、UICollectionView等
滑动时runloop处于UITrackingRunLoopMode
模式下,所以若是你想让timer在滑动的时候也可以触发,就能够分别添加到这两个模式下。或者直接用NSRunLoopCommonModes
一个模式集,包含了上面的两种模式。
可是一个timer只能添加到一个runloop(runloop与线程一一对应关系,也就是说一个timer只能添加到一个线程)。若是你非要添加到多个runloop,则只有一个有效
仍是常用到的代码
- (void)viewDidLoad { // 代码标记1 NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES]; // 代码标记2 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; // 代码标记3 self.timer = timer; } - (void)timerFire { NSLog(@"timer fire"); }
假设代码中的视图控制器由UINavigationController管理,且self.timer是strong类型,则强引用能够表示以下:
上面有四根强引用线,它们是如何产生的呢,这个也必须搞清楚?
代码标记3
的位置产生;代码标记1
的位置产生,至此L2与L3已经产生了循环引用,虽然timer尚未添加到runloop代码标记2
的位置产生根据上图就很清晰了,咱们常常说到timer与self会形成循环引用,并非由于runloop引发,而是timer自己会对self有强引用。
invalidate方法有2个功能:一是将timer从runloop中移除,那么图中的L4就消失,二是timer自己也会释放它持有资源,好比target、userinfo、block(关于block强引用self具体参考这里:http://www.cnblogs.com/mddblog/p/4754190.html),那么强引用L3就消失。若是self.timer是weak引用,也就是L2是弱引用,那么timer的引用计数就为0了,timer自己也就被释放了。若是你此时又调用addTimer:forMode:
则会抛异常,由于timer为nil,所以当控制器使用weak方式引用timer时,应注意这点
以后的timer也就永远无效了,调用它的getter方法isValid返回是NO,即便你再次将它正确的添加到runloop,也不会触发,由于timer已对target、block释放了。
timer只有这一个方法能够完成此操做,因此咱们取消一个timer必需要调用此方法。而在添加到runloop前,可使用它的getter方法isValid来判断,一个是防止为nil,另外一个是防止为无效。
然而就像引言中说的那个耸人听闻的问题同样,invalidate方法调用必须在timer添加到的runloop所在的线程,若是不在的话:虽然timer自己会释放掉它本身持有的资源好比target、userinfo、block,图中的L3会消失。可是runloop不会释放timer,即图中的L4不会消失,假设,self被pop了-->L1无效-->self引用计数为0,self释放-->L2也消失。此时就剩runloop、timer、L4,timer也就永远不会释放了,形成内存泄露。
这才真心是一个头疼的问题:是的,没错,runloop退出甚至自身释放后,L4消失,timer也就释放了。。。能够参考以前那篇关于runloop退出释放的问题NSRunLoop原理详解——再也不有盲点:http://www.jianshu.com/p/4263188ed940
这里补充一点,timer没有被释放,那么它会做为runloop的输入源,从而阻止runloop的退出(runloop的退出是会释放掉timer的)。
只关心runloop的退出就好,至于释放就别深究了,或者就当它不释放(个人理解是随着线程释放而释放)
重复的添加timer,例以下面的代码:
// 不管self.timer是strong仍是weak - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { self.timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:2 target:self selector:@selector(timerHandle) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode]; }
每点击一次屏幕就会添加一次,就会形成重复添加,你的timerHandle方法会被调用屡次,添加几回就调用几回。。。
假设点击了2次屏幕,即建立2了个timer,咱们标记为t1,t2。咱们分析一下:第二次的时候,self.timer引用t2,虽然不在引用t1可是,runloop还在引用它,因此不会释放,不用说t2也是不会释放的。
那么如何解决呢?setter方法里面调用invalidate便可:
- (void)setTimer:(NSTimer *)timer { [_timer invalidate]; _timer = timer; }
其实记住两条便可
不调用invalidate方法,target是不会被释放的,由于图中的L4,L3一直存在
不许时!
对于第一种状况咱们不该该在timer上下功夫,而是应该避免这个耗时的工做。那么第二种状况,做为开发者这也是最应该去关注的地方,要留意,而后视状况而定是否将timer添加到runloop多个模式
虽然跳过去,可是,接下来的执行不会依据被延迟的时间加上间隔时间,而是根据以前的时间来执行。好比:
定时时间间隔为2秒,t1秒添加成功,那么会在t二、t四、t六、t八、t10秒注册好事件,并在这些时间触发。假设第3秒时,执行了一个超时操做耗费了5.5秒,则触发时间是:t二、t8.五、t10,第4和第6秒就被跳过去了,虽然在t8.5秒触发了一次,可是下一次触发时间是t10,而不是t10.5。
好比上面说的t二、t四、t六、t八、t10,并不会在准确的时间触发,而是会延迟个很小的时间,缘由也能够归结为2点:
以我来说,历来没有特别准的时间,
iOS7之后,Timer 有个属性叫作 Tolerance (时间宽容度,默认是0),标示了当时间点到后,允许有多少最大偏差。
它只会在准确的触发时间到加上Tolerance时间内触发,而不会提早触发(是否是有点像咱们的火车,只会晚点。。。)。另外可重复定时器的触发时间点不受Tolerance影响,即相似上面说的t8.5触发后,下一个点不会是t10.5,而是t10 + Tolerance
,不让timer由于Tolerance而产生漂移(忽然想起嵌入式使人头疼的温漂)。
其实对于这种不许点,对咱们开发影响并不大(基本是毫秒妙级别如下的延迟),不多会用到很是准点的状况。
其实这种咱们平时也常常用(一次性定时):
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);
when接受两种类型参数:dispatch_time相对时间,相对系统的时间,好比上面相对于DISPATCH_TIME_NOW;dispatch_walltime是绝对时间,好比某年月日某时分秒。。。以后由GCD帮咱们计算一个相对时间。下面说下dispatch_time,支持纳秒级别
dispatch_time_t when = dispatch_time (DISPATCH_TIME_NOW, 1);// 还没这么用过1纳秒的延迟
应该很准确了,可是定时时间到后只是将block添加到指定的queue,去执行。这样的话,执行时间也是不保证的,首先执行线程要等待内核的调度,其次执行线程正好没有其它事情作。若是还须要建立线程的话,就更浪费时间了。因此这个也是不符合咱们指望的
when也支持DISPATCH_TIME_NOW,可是这样就没意义了,不如直接调用dispatch_async。而至于DISPATCH_TIME_FOREVER就更。。。
重复性定时,代码示例以下:
// 须要强引用 @property (nonatomic, strong)dispatch_source_t gcdTime; - (void)gcdTimerTest { // 这里须要强引用 self.gcdTime = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0)); // 开始时间支持纳秒级别 dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)2 * NSEC_PER_SEC); // 2秒执行一次 uint64_t dur = (uint64_t)(2.0 * NSEC_PER_SEC); // 最后一个参数是容许的偏差,即便设为零,系统也会有默认的偏差 dispatch_source_set_timer(self.gcdTime, start, dur, 0); // 设置回调 dispatch_source_set_event_handler(self.gcdTime, ^{ NSLog(@"---%@---%@",[NSThread currentThread],self); }); dispatch_resume(self.gcdTime); }
取消定时器:dispatch_cancel(self.gcdTimer);
,取消后再次调用dispatch_source_set_timer是没有用的。self.gcdTimer已不可用
虽然支持纳秒级别,可是定时也是不许的,上面的例子使用的是dispatch_get_global_queue
队列,执行线程也是不肯定的。因此在实际开发中这种不多用,好处是它不受runloop mode限制