在项目中我们经常会用到定时器(NSTimer)的定时执行任务功能,虽然它的用法很简单内部却暗藏玄机,如果不搞懂它的工作原理很容易埋下许多坑,在这里详细记录一下。
NSTimer & RunLoop
首先NSTimer之所以可以定时去执行任务,实质上是被当前
的Runloop驱动的,如果没有Runloop那它就不能运作,所以当我们让NSTimer定时处理事情的时候,要记得将NSTimer添加到了当前的Runloop中,Runloop会获取到NSTimer每一个时间间隔的时间,然后依次去执行我们的事件方法。如果不太明白RunLoop运行机制的童鞋,请戳这里。
有三种方式来初始化NSTimer:
- scheduledTimerWithTimeInterval:invocation:repeats: 和 scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
- timerWithTimeInterval:invocation:repeats: 和 timerWithTimeInterval:target:selector:userInfo:repeats:
- initWithFireDate:interval:target:selector:userInfo:repeats:
使用1方式去初始化:
NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"打印log");
}];
myTimer会被自动被添加到当前的RunLoop中,并默认让它在当前RunLoop中的运作模式(CFRunLoopModeRef)为NSDefaultRunLoopMode(空闲状态)
。
使用2,3方式去初始化,NSTimer不会被自动添加到当前的RunLoop中,也就无法执行任务了,所以我们要手动加入:
// NSTimer *myTimer = [[NSTimer alloc]initWithFireDate:[NSDate new] interval:1.0 target:self selector:@selector(logTime:) userInfo:@"打印log" repeats:YES];
NSTimer *myTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(logTime:) userInfo:@"打印log" repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:myTimer forMode:NSDefaultRunLoopMode];
这样做的话还会存在一个问题:当前页面如果有一个scrollView,在拖动scrollView的时候,只要不松开手,这个定时器就停止执行任务了,松开手后又恢复了正常。出现这个问题的原因还是与RunLoop有关,因为系统默认的RunLoop有两个模式:
- NSDefaultRunLoopMode:可以理解为空闲模式,就是当屏幕没有做任何操作时的状态。
- UITrackingRunLoopMode:可以理解为滑动模式,就是当处理手势或者滑动操作时的状态。
因为NSTimer默认是添加到NSDefaultRunLoopMode模式下的,所以当屏幕处于空闲期
时,当前线程的RunLoop在NSDefaultRunLoopMode模式下工作,当滑动屏幕
内容后,当前线程的RunLoop就会自动切换到UITrackingRunLoopMode模式下工作,那么NSTimer也就不执行了。
解决办法:
- 再添加一个UITrackingRunLoopMode
1 | [[NSRunLoop currentRunLoop] addTimer:myTimer [[NSRunLoop currentRunLoop] addTimer:myTimer forMode:NSDefaultRunLoopMode]; |
- 把NSTimer添加到NSRunLoopCommonModes模式(NSRunLoopCommonModes默认包含:NSDefaultRunLoopMode和UITrackingRunLoopMode)。
1 | [[NSRunLoop currentRunLoop] addTimer:myTimer forMode:NSRunLoopCommonModes]; |
以上都是在主线程
的操作的,如果我们让它在子线程
执行:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSTimer *myTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(logTime:) userInfo:@"打印log" repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:myTimer forMode:UITrackingRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:myTimer forMode:NSDefaultRunLoopMode];
});
发现又不执行了-_-||,这是为毛呢?还是和RunLoop有关!到底多大仇!!!
这里要说明一点:线程和RunLoop是一一对应的,但是各子线程中的RunLoop是要通过CoreFoundation框架中的CFRunLoopGetCurrent()函数来获取,主线程通过CFRunLoopGetMain()来获取,最后通过CFRunLoopRun()来开启RunLoop
。主线程默认就是开启的状态,所以我们只需要将NSTimer添加到主线程的RunLoop就可以起到效果,但是在子线程中我们还要通过调用CFRunLoopRun()函数使其run起来!
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSTimer *myTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(logTime:) userInfo:@"打印log" repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:myTimer forMode:UITrackingRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:myTimer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
});
另外文档中有这样一个提示:
Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
说的是NSTimer实例会被当前线程的RunLoop强引用
,所以在使用时不需要把NSTimer声明成强引用类型的属性或成员变量。
想了解线程与RunLoop之间关系的请看下面的源码整理:
1 | /// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef |
NSTimer触发时间
A timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed. Because of the various input sources a typical run loop manages, the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds. If a timer’s firing time occurs during a long callout or while the run loop is in a mode that is not monitoring the timer, the timer does not fire until the next time the run loop checks the timer. Therefore, the actual time at which the timer fires potentially can be a significant period of time after the scheduled firing time.
e.
通过官网文档描述可以了解到NSTimer并不是实时触发的,主要会受这两种情况影响:
- 当NSTimer处于一种runloopMode中,当Mode发生变化的时候(例如:NSDefaultRunLoopMode 切换到 UITrackingRunLoopMode模式)。
- 当NSTimer所在线程正在执行耗时任务的时候,定时任务会延时。
另外官方文档有引入Timer Tolerance(时间容差值)
这个概念:
In iOS 7 and later and macOS 10.9 and later, you can specify a tolerance for a timer (tolerance). Allowing the system flexibility in when a timer fires improves the ability of the system to optimize for increased power savings and responsiveness.
意思是说tolerance
是用来节省电量和优化系统响应速度的。
如果设置了这个容差值,那NSTimer的启动时间又是什么时候?请继续看官方文档的解释:
The timer may fire at any time between its scheduled fire date and the scheduled fire date plus the tolerance. The timer will not fire before the scheduled fire date.
NStimer真实启动时间
会在我们 设定的时间
和 设定的时间+容差值
之间(设定时间 <= NSTimer的启动时间 <= 设定时间 + tolerance)。
The default value is zero, which means no additional tolerance is applied. The system reserves the right to apply a small amount of tolerance to certain timers regardless of the value of the tolerance property.
Even a small amount of tolerance will have a significant positive impact on the power usage of your application.
tolerance
默认为0,但也不意味着我们的定时器能够按精准的时间来执行,系统会为它设置一个非常小的容差值,即使是极小的容差值也能大大节省你应用的耗电情况。
NSTimer销毁
Once scheduled on a run loop, the timer fires at the specified interval until it is invalidated. A non-repeating timer invalidates itself immediately after it fires. However, for a repeating timer, you must invalidate the timer object yourself by calling its invalidate method. Calling this method requests the removal of the timer from the current run loop; as a result, you should always call the invalidate method from the same thread on which the timer was installed. Invalidating the timer immediately disables it so that it no longer affects the run loop. The run loop then removes the timer (and the strong reference it had to the timer), either just before the invalidate method returns or at some later point. Once invalidated, timer objects cannot be reused.
官方文档里有说到:
- 当使用
non-repeating timer
的时候,它的销毁时间会在第一次定时执行任务后,无需我们手动调用invalidate方法。 - 当使用
repeating timer
的时候,我们在哪个线程创建的就要在哪个线程去销毁它。 - 当NSTimer被销毁后,无法再重新被使用。
总结
通过上面的代码示例说明和官方文档的描述,我们基本掌握了NSTimer的使用和它的运行机制,下面是对它的总结:
- NSTimer的创建和销毁都要保证在同一线程下操作(当repeat=YES时,必须要调用invalidate方法销毁)。
- NSTimer会被当前线程的RunLoop强引用。
- 在使用NSTimer时,要把实例添加到当前线程的RunLoop中,并启动RunLoop。
- NSTimer的创建和销毁尽量放到子线程去,因为主线程要处理的事情有很多,会影响到定时器的精准度,同时也会给主线程增加负荷。
参考文献: