NSTimer是ios上比較常用的定時(shí)器組件,在使用了一段時(shí)間后,發(fā)現(xiàn)有些地方是需要注意一下的。
-
NSTimer 是需要配合NSRunLoop 才可以正常工作的。
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds invocation:(NSInvocation *)invocation repeats:(BOOL)repeats + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;使用這個(gè)類方法,會(huì)自動(dòng)添加到當(dāng)前的RunLoop里面。關(guān)于RunLoop的介紹網(wǎng)上有很多資料,推薦看看 深入理解RunLoop。
-
當(dāng)RunLoop處于UITrackingRunLoopMode模式的時(shí)候(滑動(dòng)UIScrollView的時(shí)候),使用
scheduledTimerWithTimeInterval:(NSTimeInterval)seconds invocation:(NSInvocation *)invocation repeats:(BOOL)repeats的類方法創(chuàng)建的Timer,是不會(huì)收到響應(yīng)事件。只有RunLoop切換到Default模式時(shí)才可以正常響應(yīng)。如果希望滑動(dòng)時(shí)也可以響應(yīng)Timer時(shí)間,需要把Timer加到RunLoop并指定模式為NSRunLoopCommonModes
NSTimer *timer = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(test) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
-
NSTimer 會(huì)強(qiáng)引用 target 對(duì)象,很容易造成內(nèi)存泄露或者其它因生命周期和預(yù)期不一至導(dǎo)致的問(wèn)題。
我們先看一段常見(jiàn)的事例代碼
@implementation TViewController { NSTimer *_timer; } - (void)dealloc { NSLog(@"%s", __func__); } - (void)viewDidLoad { [super viewDidLoad]; _timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(onTimeout) userInfo:nil repeats:YES]; } - (void)onTimeout { NSLog(@"%s", __func__); } @end大家可能會(huì)覺(jué)得,當(dāng)這個(gè)ViewController被 pop 掉后會(huì)正常釋放,timer 也會(huì)停掉。但實(shí)際的情況不是你想的那樣。以下log是Push這個(gè)ViewController后,然后點(diǎn)擊返回的過(guò)程。
2016-03-24 00:42:19.663 NSTimerDemo[14916:3982566] -[TViewController onTimeout]
2016-03-24 00:42:20.663 NSTimerDemo[14916:3982566] -[TViewController onTimeout]
2016-03-24 00:42:21.663 NSTimerDemo[14916:3982566] -[TViewController onTimeout]
2016-03-24 00:42:22.663 NSTimerDemo[14916:3982566] -[TViewController onTimeout]
2016-03-24 00:42:23.369 NSTimerDemo[14916:3982566] -[TViewController viewDidDisappear:]
2016-03-24 00:42:23.663 NSTimerDemo[14916:3982566] -[TViewController onTimeout]
2016-03-24 00:42:24.663 NSTimerDemo[14916:3982566] -[TViewController onTimeout]
2016-03-24 00:42:25.663 NSTimerDemo[14916:3982566] -[TViewController onTimeout]
從日志上來(lái)看,dealloc方法確實(shí)沒(méi)有執(zhí)行,而且timer事件還一直在觸發(fā)。
OK,既然Timer強(qiáng)引用了ViewController,那把ViewController改成__weak不就是可以解決問(wèn)題了?
于是我們把創(chuàng)建Timer的代碼改成
__weak typeof(self) weak_self = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:1
target:weak_self
selector:@selector(onTimeout)
userInfo:nil
repeats:YES];
發(fā)現(xiàn)輸出的log和之前的一樣,難道weak對(duì)象根本沒(méi)起作用?
用Instrement查看了一下內(nèi)存情況,發(fā)現(xiàn)真的是Timer強(qiáng)引用Target對(duì)象

查看了一下官方文檔關(guān)于target的一些說(shuō)明
target:
The object to which to send the message specified by aSelector when the timer fires. ***The timer maintains a strong reference to target *** until it (the timer) is invalidated.
目前主要是處于一個(gè)閉環(huán)(環(huán)形引用)的狀態(tài),我們要想辦法打破這種狀態(tài),而且__weak設(shè)置給Timer也不會(huì)破壞Timer強(qiáng)引用Target。
于是,我們引用一個(gè)包裝對(duì)象,讓Timer強(qiáng)引用這個(gè)包裝對(duì)象,包裝對(duì)象弱引用Target(ViewController)
ViewController ---> Timer --->Wrapper ...>ViewController 這樣就可以破壞環(huán)形引用。
@Interface Wrapper
@property (weak, nonatom) id target;
@end
那么創(chuàng)建Timer的類方法的Target對(duì)象不是傳self, 而是傳 wrapper 對(duì)象。
另外,wrapper對(duì)象還要把Timer的事件傳遞到真正的target上。
詳細(xì)的 Timer Wrapper 可以看完代碼 BSTimer
最后其實(shí)可以用dispatch_time解決強(qiáng)引用問(wèn)題,但是dispatch_time在暫停功能上處理起來(lái)比較麻煩。