iOS開發 之 不要告訴我你會用NSTimer!

目錄

引言

為什么想起來要討論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沒必要這么作, 開這么多接口, 作者(好像就是我??)也沒必要這么作, 寫這么長軟文

他們的區別很簡單:

how-to-user-nstimer-01.png

scheduledTimerWithTimeInterval相比它的小伙伴們不僅僅是創建了NSTimer對象, 還把該對象加入到了當前的runloop中!

等等, runloop是什么鬼? 在此不解釋runloop的原理, 但是使用NSTimer你必須要知道的是

NSTimer只有被加入到runloop, 才會生效, 即NSTimer才會被真正執行

所以說, 如果你想使用timerWithTimeInterval或initWithFireDate的話, 需要使用NSRunloop的以下方法將NSTimer加入到runloop中

- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode
how-to-user-nstimer-02.png

銷毀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的區別了, 下面的示意圖更直觀

how-to-user-nstimer-03.png

總之, 如果想要銷毀NSTimer, 那么確定, 一定以及肯定要調用invalidate方法

invalidate與=nil

就像銷毀其他強應用(不用我解釋強引用了吧, 否則你還是別浪費時間往下看了)對象一樣, 我們是否可以將NSTimer置nil, 而讓iOS系統幫我們銷毀NSTimer呢?

答案是: 當然不可以! (詳見上述的結論, "總之, 巴拉巴拉...")

為什么不可以? 其他強引用對象都可以, 為什么NSTimer對象不可以? 你說不可以就可以? 憑什么信你?

好吧, 我們來看下使用NSTimer時, ARC是怎么工作的

  • 首先, 是創建NSTimer, 加入到runloop后, 除了ViewController之外iOS系統也會強引用NSTimer對象
how-to-user-nstimer-04.png
  • 當調用invalidate方法時, 移除runloop后, iOS系統會解除對NSTimer對象的強引用, 當ViewController銷毀時, ViewController和NSTimer就都可以釋放了
how-to-user-nstimer-05.png
  • 當將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

如果你還是不理解, 那只能用"殺手锏"了, 美圖伺候!

how-to-user-nstimer-06.png

關于上圖, @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, 計劃后續會有單獨的文章來詳細討論之

附錄

更多文章, 請支持我的個人博客

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容

  • 再一次面試中被問到nstimer的爭取使用方法,原理,我當時就說了[_timer invalidate],time...
    iOS開發小平哥閱讀 4,085評論 1 13
  • 創建NSTimer 創建NSTimer的常用方法是: + (NSTimer *)scheduledTimerWit...
    LanWor閱讀 1,398評論 0 2
  • NSTimer是iOS最常用的定時器工具之一,在使用的時候常常會遇到各種各樣的問題,最常見的是內存泄漏,通常我們使...
    bomo閱讀 1,241評論 0 7
  • 九宮格 時間:5分鐘 關鍵詞:小毛蟲 做好自己、泥土、小馬、小草、美麗、小羊、小螞蟻、小螳螂 關鍵詞:小毛蟲、美麗...
    李宇宙rourou閱讀 503評論 2 2
  • 原題 代碼庫的版本號是從 1 到 n 的整數。某一天,有人提交了錯誤版本的代碼,因此造成自身及之后版本的代碼在單元...
    Jason_Yuan閱讀 555評論 0 1