目錄
引言
為什么想起來要討論NSTimer? 源自這兩天工作中的遇到的一個問題:
專職iOS開發(fā)也一年有余了, 但是在跟蹤自己寫的ViewController釋放時, 發(fā)現(xiàn)ViewController的dealloc方法死活沒走到, 心里咯噔一下, 不會又內(nèi)存泄漏了? ??
一切都是很完美的節(jié)奏啊: ViewController初始化時, 創(chuàng)建Sub UIView, 創(chuàng)建數(shù)據(jù)結(jié)構(gòu), 創(chuàng)建NSTimer
然后在dealloc里, 釋放NSTimer, 然后NSTimer = nil, 哪里會有什么問題?
不對! 移除NSTimer后dealloc就愉快滴走了起來, 難道NSTimer的用法一直都不對?
結(jié)果發(fā)現(xiàn), 真的是不對! ??
好吧, 故事就講到這里, 馬上開始今天的NSTimer之旅吧
創(chuàng)建NSTimer
創(chuàng)建NSTimer的常用方法是
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
創(chuàng)建NSTimer的不常用方法是
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
和
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
這幾種方法除了創(chuàng)建方式不同(參數(shù)), 方法類型不同(類方法, 對象方法), 還有其他不同么?
當然有, 不然Apple沒必要這么作, 開這么多接口, 作者(好像就是我??)也沒必要這么作, 寫這么長軟文
他們的區(qū)別很簡單:
scheduledTimerWithTimeInterval相比它的小伙伴們不僅僅是創(chuàng)建了NSTimer對象, 還把該對象加入到了當前的runloop中!
等等, runloop是什么鬼? 在此不解釋runloop的原理, 但是使用NSTimer你必須要知道的是
NSTimer只有被加入到runloop, 才會生效, 即NSTimer才會被真正執(zhí)行
所以說, 如果你想使用timerWithTimeInterval或initWithFireDate的話, 需要使用NSRunloop的以下方法將NSTimer加入到runloop中
- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode
銷毀NSTimer
知道了如何創(chuàng)建NSTimer后, 我們來說說如何銷毀NSTimer, 銷毀NSTimer不是很簡單的么?
用invalidate方法啊, 好像還有個fire方法, 實在不行直接將NSTimer對象置nil, 這樣iOS系統(tǒng)就幫我們銷毀了
是的, 曾經(jīng)的我也是如此混沌滴這么認為著, 那么這幾種方法是不是真的都可以銷毀NSTimer呢?
invalidate與fire
我們來看看Apple Documentation對這兩個方法的權(quán)威解釋吧
- invalidate
Stops the receiver from ever firing again and requests its removal from its run loop
This method is the only way to remove a timer from an NSRunLoop object
- fire
Causes the receiver’s message to be sent to its target
If the timer is non-repeating, it is automatically invalidated after firing
理解了上面的幾句話, 你就完完全全理解了invalidate和fire的區(qū)別了, 下面的示意圖更直觀
總之, 如果想要銷毀NSTimer, 那么確定, 一定以及肯定要調(diào)用invalidate方法
invalidate與=nil
就像銷毀其他強應用(不用我解釋強引用了吧, 否則你還是別浪費時間往下看了)對象一樣, 我們是否可以將NSTimer置nil, 而讓iOS系統(tǒng)幫我們銷毀NSTimer呢?
答案是: 當然不可以! (詳見上述的結(jié)論, "總之, 巴拉巴拉...")
為什么不可以? 其他強引用對象都可以, 為什么NSTimer對象不可以? 你說不可以就可以? 憑什么信你?
好吧, 我們來看下使用NSTimer時, ARC是怎么工作的
- 首先, 是創(chuàng)建NSTimer, 加入到runloop后, 除了ViewController之外iOS系統(tǒng)也會強引用NSTimer對象
- 當調(diào)用invalidate方法時, 移除runloop后, iOS系統(tǒng)會解除對NSTimer對象的強引用, 當ViewController銷毀時, ViewController和NSTimer就都可以釋放了
- 當將NSTimer對象置nil時, 雖然解除了ViewController對NSTimer的強引用, 但是iOS系統(tǒng)仍然對NSTimer和ViewController存在著強引用關系
神馬? iOS系統(tǒng)對NSTimer有強引用很好理解, 對ViewController本來不就是強引用么?
這里所說的iOS系統(tǒng)對ViewController的強引用, 不是指為了實現(xiàn)View顯示的強引用, 而是指iOS為了實現(xiàn)NSTimer而對ViewController進行的額外強引用 (我去, 能不能不要這么拗口, 欺負我語文不好)
不瞞您說, 我的語文其實也是一般般, 所以show me the code
NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));
_timer = [NSTimer scheduledTimerWithTimeInterval:TimerInterval
target:self
selector:@selector(timerSelector:)
userInfo:nil
repeats:TimerRepeats];
NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));
[_timer invalidate];
NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));
各位請注意, 創(chuàng)建NSTimer和銷毀NSTimer后, ViewController(就是這里的self)引用計數(shù)的變化
2016-07-06 13:53:21.950 NSTimerAndDeallocDemo[2028:697020] Retain count is 7
2016-07-06 13:53:21.950 NSTimerAndDeallocDemo[2028:697020] Retain count is 8
2016-07-06 13:53:21.950 NSTimerAndDeallocDemo[2028:697020] Retain count is 7
如果你還是不理解, 那只能用"殺手锏"了, 美圖伺候!
關于上圖, @JasonHan0991 有不同的解釋, 詳見評論區(qū), 在此表示感謝!
結(jié)論
綜上所述, 銷毀NSTimer的正確姿勢應該是
[_timer invalidate]; // 真正銷毀NSTimer對象的地方
_timer = nil; // 對象置nil是一種規(guī)范和習慣
慢著, 這個結(jié)論好像不妥吧?
這都被你發(fā)現(xiàn)了! 銷毀NSTimer的時機也是至關重要的!
如果將上述銷毀NSTimer的代碼放到ViewController的dealloc方法里, 你會發(fā)現(xiàn)dealloc還是永遠不會走的
所以我們要將上述代碼放到ViewController的其他生命周期方法里, 例如ViewWillDisappear中
綜上所述, 銷毀NSTimer的正確姿勢應該是 (這句話我怎么看著這么眼熟, 是的, 這次真的結(jié)論了)
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[_timer invalidate];
_timer = nil;
}
NSTimer與runloop
上面說到scheduledTimerWithTimeInterval方法時, 有這么一句
schedules it on the current run loop in the default mode
加到runloop這件事就不必再解釋了, 而這個default mode應該如何理解呢?
其實我是不想談runloop的(因為理解不深, 所以怕誤導人民群眾), 但是這里不得不解釋下了
runloop會運行在不同的mode, 簡單來說有以下兩種mode
NSDefaultRunLoopMode, 默認的mode
UITrackingRunLoopMode, 當處理UI滾動操作時的mode
所以scheduledTimerWithTimeInterval創(chuàng)建的NSTimer在UI滾動時, 是不會被及時觸發(fā)的, 因為此時NSTimer被加到了default mode
如果想要runloop運行在UITrackingRunLoopMode時, 仍然及時觸發(fā)NSTimer那應該怎么辦呢?
應該使用timerWithTimeInterval或initWithFireDate, 在創(chuàng)建完NSTimer后, 自己加入到指定的runloop mode
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
NSRunLoopCommonModes又是什么鬼? 不是說好的只有兩種mode么?
是滴, 請注意這里的復數(shù)形式modes, 說明它不是一個mode, 它是mode的集合!
通常情況下NSDefaultRunLoopMode和UITrackingRunLoopMode都已經(jīng)被加入到了common modes集合中, 所以不論runloop運行在哪種mode下, NSTimer都會被及時觸發(fā)
最后, 我們來做個小測驗, 來結(jié)束今天的NSTimer討論吧
測驗: 請問下面的NSTimer哪個更準時?
// 1
[NSTimer scheduledTimerWithTimeInterval:TimerInterval
target:self
selector:@selector(timerSelector:)
userInfo:nil
repeats:TimerRepeats];
// 2
[[NSRunLoop currentRunLoop] addTimer:_timer
forMode:NSDefaultRunLoopMode];
// 3
[[NSRunLoop currentRunLoop] addTimer:_timer
forMode:NSRunLoopCommonModes];
答案, 就不貼了, 相信你肯定知道的; 另外, 關于runloop, 計劃后續(xù)會有單獨的文章來詳細討論之
附錄
更多文章, 請支持我的個人博客