NSTimer的建立一般有两种方式,一种是以 scheduledTimerWithTimeInterval 为开头的类方法 。这些方法在建立了 NSTimer 以后会将这个 NSTimer 以 NSDefaultRunLoopMode 模式放入当前线程的 RunLoop。html
+ ( NSTimer *) scheduledTimerWithTimeInterval:invocation:repeats: + ( NSTimer *) scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
另外一种是以 timerWithTimeInterval 为开头的类方法。这些方法建立的 NSTimer 并不能立刻使用,还须要调用 RunLoop 的 addTimer:forMode: 方法将 NSTimer 放入 RunLoop,这样 NSTimer 才能正常工做。git
+ ( NSTimer *) timerWithTimeInterval:invocation:repeats: + ( NSTimer *) timerWithTimeInterval:target:selector:userInfo:repeats:
Timers work in conjunction with run loops. 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.github
从 NSTimer 的官方文档能够得知,RunLoop 对加入其中的 NSTimer 会添加一个强引用。这里须要注意一个细节问题,以 timerWithTimeInterval 为开头的类方法建立出来的 NSTimer 须要手动加入 RunLoop, 这样 RunLoop 才会对这个 NSTimer 有强引用。如果咱们使用 weak 修饰 NSTimer 变量,在 NSTimer 建立以后加入 RunLoop 以前,将 NSTimer 对象赋值给 weak 修饰的变量,那么对致使 NSTimer 对象被释放。app
#import "TimerViewController.h" @interface TimerViewController () // 使用 weak @property (nonatomic,weak) NSTimer *timer; @end @implementation TimerViewController - (void)viewDidLoad { [super viewDidLoad]; // NSTimer 建立以后没有被自动加入 RunLoop self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(outputLog:) userInfo:nil repeats:YES]; if (self.timer == nil) { NSLog(@"timer 被释放了"); } } - (void)outputLog:(NSTimer *)timer{ NSLog(@"it is log!"); } @end
代码运行以后,log 输出 “ timer 被释放了 ”,说明 self.timer 为 nil,刚刚建立的 NSTimer 对象被释放了。解决这个问题的方法也很简单, NSTimer 对象建立以后先加入 RunLoop 再赋值给变量。oop
// ...... 省略代码 - (void)viewDidLoad { [super viewDidLoad]; // 建立 NSTimer NSTimer *doNotWorkTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(outputLog:) userInfo:nil repeats:YES]; // NSTimer 加入 NSRunLoop [[NSRunLoop currentRunLoop] addTimer:doNotWorkTimer forMode:NSDefaultRunLoopMode]; // 赋值给 weak 变量 self.timer = doNotWorkTimer; } // ...... 省略代码
对于 NSTimer 来讲,不管是重复执行的 NSTimer 仍是一次性的 NSTimer 只要调用 invalidate 方法则会变得无效,NSTimer 就会释放资源。一次性的 NSTimer 执行完操做后会自动调用 invalidate 方法.ui
举个例子,TimerViewController 强引用一个 NSTimer,NSTimer 的 target 设置为 TimerViewController,在 TimerViewController 的 dealloc 方法里面调用 NSTimer 的 invalidate 方法。atom
@interface TimerViewController () @property (nonatomic,strong) NSTimer *timer; @end @implementation TimerViewController - (void)viewDidLoad { [super viewDidLoad]; self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(outputLog:) userInfo:nil repeats:YES]; } -(void)outputLog:(NSTimer *)timer{ NSLog(@"it is log!"); } - (void)dealloc { [self.timer invalidate]; NSLog(@"TimerViewController dealloc!"); } @end
上面这段代码中的 TimerViewController 和 NSTimer 构成了循环引用,退出 TimerViewController 页面,TimerViewController 和 NSTimer 都没法释放,TimerViewController 的 dealloc 方法没有被调用,NSTimer 就没有被 invalidate ,outputLog 方法会被一直触发。.net
原来 TimerViewController 强引用一个 NSTimer,NSTimer 使用TimerViewController 为 target, 这样会构成循环引用,那若是 TimerViewController 弱引用一个 NSTimer,是否是可以解决这个问题呢?线程
@interface TimerViewController () // 使用 weak @property (nonatomic,weak) NSTimer *timer; @end // ...... 省略代码
运行结果和上面使用强引用的案例没有什么差异,到底是什么缘由呢?代理
如上图所示 TimerViewController 弱引用 NSTimer, NSTimer 强引用 TimerViewController。
TimerViewController 须要 NSTimer 同生共死。NSTimer 须要在 TimerViewController 的 dealloc 方法被 invalidate 。NSTimer 被 invalidate 的前提是 TimerViewController 被 dealloc。而 NSTimer 一直强引用着 TimerViewController 致使 TimerViewController 没法调用 dealloc 方法。
从 NSTimer 的角度来看解决方案,若是 NSTimer 不持有 TimerViewController 的引用,那么 TimerViewController 就能够正常销毁,dealloc 方法能够正常调用 NSTimer 的 invalidate 方法,那么 NSTimer 和 TimerViewController 均可以销毁,完美!
在 NSTimer 的使用过程,要避免循环引用问题。解决方案是 NSTimer 不持有 TimerViewController 的引用,也就是说 NSTimer 的 target 对象不要是 TimerViewController。 这里有 2 个方案能够来处理这个问题。
第一个方案:将 target 分离出来独立成一个 WeakProxy 代理对象, NSTimer 的 target 设置为 WeakProxy 代理对象,WeakProxy 是 TimerViewController 的代理对象,全部发送到 WeakProxy
的消息都会被转发到 TimerViewController 对象。使用代理对象能够达到 NSTimer 不直接持有 TimerViewController 的目的。
#import "TimerViewController.h" #import "YYWeakProxy.h" @interface TimerViewController () @property (nonatomic,weak) NSTimer *timer; @end @implementation TimerViewController - (void)viewDidLoad { [super viewDidLoad]; self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[YYWeakProxy proxyWithTarget:self] selector:@selector(outputLog:) userInfo:nil repeats:YES]; } - (void)outputLog:(NSTimer *)timer{ NSLog(@"it is log!"); } - (void)dealloc { [self.timer invalidate]; NSLog(@"TimerViewController dealloc!"); } @end
YYWeakProxy 来自 YYKit 开源项目 ,是一个代理类的实现。
第二个方案是经过 category 把 NSTimer 的 target 设置为 NSTimer 类,让 NSTimer 自身作为target, 把 selector 经过 block 传入给 NSTimer,在 NSTimer 的 category 里面触发 selector 。这样也能够达到 NSTimer 不直接持有 TimerViewController 的目的,实现更优雅 ( 若是是直接支持 iOS 10 以上的系统版本,那可使用 iOS 10新增的系统级 block 方案 )。
// NSTimer+BlocksSupport.h #import <Foundation/Foundation.h> @interface NSTimer (BlocksSupport) + (NSTimer *)xx_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void(^)())block; @end // NSTimer+BlocksSupport.m #import "NSTimer+BlocksSupport.h" @implementation NSTimer (BlocksSupport) + (NSTimer *)xx_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void(^)())block; { return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(xx_blockInvoke:) userInfo:[block copy] repeats:repeats]; } + (void)xx_blockInvoke:(NSTimer *)timer { void (^block)() = timer.userInfo; if(block) { block(); } } @end // TimerViewController.m #import "TimerViewController.h" #import "NSTimer+BlocksSupport.h" @interface TimerViewController () @property (nonatomic,weak) NSTimer *timer; @end @implementation TimerViewController - (void)viewDidLoad { [super viewDidLoad]; self.timer =[NSTimer xx_scheduledTimerWithTimeInterval:1.0 repeats:YES block:^{ NSLog(@"it is log!"); }]; } - (void)dealloc { [self.timer invalidate]; NSLog(@"TimerViewController dealloc!"); } @end
以上 2 个方案均可以达到目的,推荐使用第二个 NSTimer 的 category 方案。
从 RunLoop 的机制图中能够看到 CFRunLoopTimer 存在,CFRunLoopTimer 做为 RunLoop 的事件源之一,它的上层对应就是 NSTimer,NSTimer 的触发正是基于 RunLoop, 使用 NSTimer 以前必须注册到 RunLoop。一个NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 00:00, 00:02, 00:04,00:06 这几个时间点。RunLoop 为了节省资源,并不会在很是准确的时间点回调这个 NSTimer,NSTimer 有个属性叫作 Tolerance 表示回调 NSTimer 的时间点允许多少最大偏差。
tolerance : The amount of time after the scheduled fire date that the timer may fire.
若是 RunLoop 执行了一个很长时间的任务,错过了某个时间点,则那个时间点的回调也会跳过去,不会延后执行。好比 00:02 这个时间点被错过了,RunLoop 不会 那么就只能等待下一个时间点 00:04 。
RunLoop 的触发时间准确性也与 RunLoop 的 mode 相关。
主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你建立一个 Timer 并加到 DefaultMode 时,Timer 会获得重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,而且也不会影响到滑动操做。
有时你须要一个 Timer,在两个 Mode 中都能获得回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自动更新到全部具备”Common”属性的 Mode 里去。
// ......省略代码 - (void)viewDidLoad { [super viewDidLoad]; self.timer =[NSTimer xx_scheduledTimerWithTimeInterval:1.0 repeats:YES block:^{ NSLog(@"it is log!"); }]; [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; } - (void)dealloc { [self.timer invalidate]; NSLog(@"TimerViewController dealloc!"); } // ......省略代码