目錄
引言
為什么想起來要討論NSTimer? 源自這兩天工作中的遇到的一個問題:
專職iOS開發也一年有余了, 但是在跟蹤自己寫的ViewController釋放時, 發現ViewController的dealloc方法死活沒走到, 心里咯噔一下, 不會又內存泄漏了? ??
一切都是很完美的節奏啊: ViewController初始化時, 創建Sub UIView, 創建數據結構, 創建NSTimer
然后在dealloc里, 釋放NSTimer, 然后NSTimer = nil, 哪里會有什么問題?
不對! 移除NSTimer后dealloc就愉快滴走了起來, 難道NSTimer的用法一直都不對?
結果發現, 真的是不對! ??
好吧, 故事就講到這里, 馬上開始今天的NSTimer之旅吧
創建NSTimer
創建NSTimer的常用方法是
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
創建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
這幾種方法除了創建方式不同(參數), 方法類型不同(類方法, 對象方法), 還有其他不同么?
當然有, 不然Apple沒必要這么作, 開這么多接口, 作者(好像就是我??)也沒必要這么作, 寫這么長軟文
他們的區別很簡單:
scheduledTimerWithTimeInterval相比它的小伙伴們不僅僅是創建了NSTimer對象, 還把該對象加入到了當前的runloop中!
等等, runloop是什么鬼? 在此不解釋runloop的原理, 但是使用NSTimer你必須要知道的是
NSTimer只有被加入到runloop, 才會生效, 即NSTimer才會被真正執行
所以說, 如果你想使用timerWithTimeInterval或initWithFireDate的話, 需要使用NSRunloop的以下方法將NSTimer加入到runloop中
- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode
銷毀NSTimer
知道了如何創建NSTimer后, 我們來說說如何銷毀NSTimer, 銷毀NSTimer不是很簡單的么?
用invalidate方法啊, 好像還有個fire方法, 實在不行直接將NSTimer對象置nil, 這樣iOS系統就幫我們銷毀了
是的, 曾經的我也是如此混沌滴這么認為著, 那么這幾種方法是不是真的都可以銷毀NSTimer呢?
invalidate與fire
我們來看看Apple Documentation對這兩個方法的權威解釋吧
- 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的區別了, 下面的示意圖更直觀
總之, 如果想要銷毀NSTimer, 那么確定, 一定以及肯定要調用invalidate方法
invalidate與=nil
就像銷毀其他強應用(不用我解釋強引用了吧, 否則你還是別浪費時間往下看了)對象一樣, 我們是否可以將NSTimer置nil, 而讓iOS系統幫我們銷毀NSTimer呢?
答案是: 當然不可以! (詳見上述的結論, "總之, 巴拉巴拉...")
為什么不可以? 其他強引用對象都可以, 為什么NSTimer對象不可以? 你說不可以就可以? 憑什么信你?
好吧, 我們來看下使用NSTimer時, ARC是怎么工作的
- 首先, 是創建NSTimer, 加入到runloop后, 除了ViewController之外iOS系統也會強引用NSTimer對象
- 當調用invalidate方法時, 移除runloop后, iOS系統會解除對NSTimer對象的強引用, 當ViewController銷毀時, ViewController和NSTimer就都可以釋放了
- 當將NSTimer對象置nil時, 雖然解除了ViewController對NSTimer的強引用, 但是iOS系統仍然對NSTimer和ViewController存在著強引用關系
神馬? iOS系統對NSTimer有強引用很好理解, 對ViewController本來不就是強引用么?
這里所說的iOS系統對ViewController的強引用, 不是指為了實現View顯示的強引用, 而是指iOS為了實現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));
各位請注意, 創建NSTimer和銷毀NSTimer后, ViewController(就是這里的self)引用計數的變化
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 有不同的解釋, 詳見評論區, 在此表示感謝!
結論
綜上所述, 銷毀NSTimer的正確姿勢應該是
[_timer invalidate]; // 真正銷毀NSTimer對象的地方
_timer = nil; // 對象置nil是一種規范和習慣
慢著, 這個結論好像不妥吧?
這都被你發現了! 銷毀NSTimer的時機也是至關重要的!
如果將上述銷毀NSTimer的代碼放到ViewController的dealloc方法里, 你會發現dealloc還是永遠不會走的
所以我們要將上述代碼放到ViewController的其他生命周期方法里, 例如ViewWillDisappear中
綜上所述, 銷毀NSTimer的正確姿勢應該是 (這句話我怎么看著這么眼熟, 是的, 這次真的結論了)
- (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創建的NSTimer在UI滾動時, 是不會被及時觸發的, 因為此時NSTimer被加到了default mode
如果想要runloop運行在UITrackingRunLoopMode時, 仍然及時觸發NSTimer那應該怎么辦呢?
應該使用timerWithTimeInterval或initWithFireDate, 在創建完NSTimer后, 自己加入到指定的runloop mode
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
NSRunLoopCommonModes又是什么鬼? 不是說好的只有兩種mode么?
是滴, 請注意這里的復數形式modes, 說明它不是一個mode, 它是mode的集合!
通常情況下NSDefaultRunLoopMode和UITrackingRunLoopMode都已經被加入到了common modes集合中, 所以不論runloop運行在哪種mode下, NSTimer都會被及時觸發
最后, 我們來做個小測驗, 來結束今天的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, 計劃后續會有單獨的文章來詳細討論之
附錄
更多文章, 請支持我的個人博客