iOS開發NSTimer的深入理解

weixin_33758863發表於2017-06-15

再一次面試中被問到nstimer的爭取使用方法,原理,我當時就說了[_timer invalidate],timer =nil;

但是原理沒說明,後來經過查詢資料查到了,引用原著作者http://www.jianshu.com/p/330d7310339d

發現ViewController的dealloc方法死活沒走到, 心裡咯噔一下, 不會又記憶體洩漏了? ?

一切都是很完美的節奏啊: ViewController初始化時, 建立Sub UIView, 建立資料結構, 建立NSTimer

然後在dealloc裡, 釋放NSTimer, 然後NSTimer = nil, 哪裡會有什麼問題?

不對! 移除NSTimer後dealloc就愉快滴走了起來, 難道NSTimer的用法一直都不對?

結果發現, 真的是不對! ?

好吧, 故事就講到這裡, 馬上開始今天的NSTimer之旅吧

建立NSTimer

建立NSTimer的常用方法是

+ (NSTimer*)scheduledTimerWithTimeInterval:(NSTimeInterval)titarget:(id)targetselector:(SEL)aSelectoruserInfo:(id)userInforepeats:(BOOL)repeats

建立NSTimer的不常用方法是

+ (NSTimer*)timerWithTimeInterval:(NSTimeInterval)titarget:(id)targetselector:(SEL)aSelectoruserInfo:(id)userInforepeats:(BOOL)repeats

- (instancetype)initWithFireDate:(NSDate*)date interval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats

這幾種方法除了建立方式不同(引數), 方法型別不同(類方法, 物件方法), 還有其他不同麼?

當然有, 不然Apple沒必要這麼作, 開這麼多介面, 作者(好像就是我?)也沒必要這麼作, 寫這麼長軟文

他們的區別很簡單:

2180450-523e7da5dd1bef97.png

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

2180450-f00f1ae2be9aadc8.png

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

2180450-fc0b2fdf26719777.png

how-to-user-nstimer-03.png

總之, 如果想要銷燬NSTimer, 那麼確定, 一定以及肯定要呼叫invalidate方法

invalidate與=nil

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

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

為什麼不可以? 其他強引用物件都可以, 為什麼NSTimer物件不可以? 你說不可以就可以? 憑什麼信你?

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

首先, 是建立NSTimer, 加入到runloop後, 除了ViewController之外iOS系統也會強引用NSTimer物件

2180450-a3a37c1cf9dbb6ae.png

how-to-user-nstimer-04.png

當呼叫invalidate方法時, 移除runloop後, iOS系統會解除對NSTimer物件的強引用, 當ViewController銷燬時, ViewController和NSTimer就都可以釋放了

2180450-d2005f131407a8bf.png

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((__bridgeCFTypeRef)self));_timer = [NSTimerscheduledTimerWithTimeInterval:TimerInterval                                          target:selfselector:@selector(timerSelector:)                                        userInfo:nilrepeats:TimerRepeats];NSLog(@"Retain count is %ld",CFGetRetainCount((__bridgeCFTypeRef)self));[_timer invalidate];NSLog(@"Retain count is %ld",CFGetRetainCount((__bridgeCFTypeRef)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

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

2180450-07535cdef298107f.png

how-to-user-nstimer-06.png

關於上圖, @JasonHan0991 有不同的解釋, 詳見評論區, 在此表示感謝!

結論

綜上所述, 銷燬NSTimer的正確姿勢應該是

[_timerinvalidate];// 真正銷燬NSTimer物件的地方_timer=nil;// 物件置nil是一種規範和習慣

慢著, 這個結論好像不妥吧?

這都被你發現了! 銷燬NSTimer的時機也是至關重要的!

如果將上述銷燬NSTimer的程式碼放到ViewController的dealloc方法裡, 你會發現dealloc還是永遠不會走的

所以我們要將上述程式碼放到ViewController的其他生命週期方法裡, 例如ViewWillDisappear中

綜上所述, 銷燬NSTimer的正確姿勢應該是 (這句話我怎麼看著這麼眼熟, 是的, 這次真的結論了)

- (void)viewWillDisappear:(BOOL)animated {    [superviewWillDisappear: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:_timerforMode:NSRunLoopCommonModes];

NSRunLoopCommonModes又是什麼鬼? 不是說好的只有兩種mode麼?

是滴, 請注意這裡的複數形式modes, 說明它不是一個mode, 它是mode的集合!

通常情況下NSDefaultRunLoopMode和UITrackingRunLoopMode都已經被加入到了common modes集合中, 所以不論runloop執行在哪種mode下, NSTimer都會被及時觸發

最後, 我們來做個小測驗, 來結束今天的NSTimer討論吧

測驗: 請問下面的NSTimer哪個更準時?

// 1[NSTimerscheduledTimerWithTimeInterval:TimerInterval                                target:selfselector:@selector(timerSelector:)                              userInfo:nilrepeats:TimerRepeats];

// 2[[NSRunLoopcurrentRunLoop] addTimer:_timer                            forMode:NSDefaultRunLoopMode];

// 3[[NSRunLoopcurrentRunLoop] addTimer:_timer                            forMode:NSRunLoopCommonModes];

答案, 第三種是最準確的,不受其他情況影響,第一種和第二種情況相同都是加入到NSDefaultRunLoopMode模式下,如果滑動scrollview的時候,時間計時器會停止,等滑動結束,再繼續,所以時間不準確


最後,如果本文章對你有幫助,希望點贊,評論666

本人github開源庫地址:https://github.com/lishiping,希望對廣大iOS開發者有幫助

Cocoapod公開庫:

1.SafeData---使用array,dictionary類別,主要是保護陣列防止插入空陣列,防止陣列越界,在開發中非常實用,字典的類別也非常實用,可以直接得到轉換後的型別SafeData

2.SPDebugBar ---A tool to help developers and testers quickly switch the server address, convenient to debug the program.一個小工具幫助開發人員和測試人員快速切換伺服器地址,方便除錯程式,可以在debug模式下或者測試包上方便切換地址SPDebugBar

3.SPFastPush---Use Macro Fast NavigationController push next VC,and Fast Pop anther VC.使用巨集幫助導航控制器快速push下一個Viewcontroller,並使用KVC快速給下一個VC賦值,也可以使用巨集定義快速返回上一層VC或者指定的VCSPFastPush

4.SPMacro---一些foundation層的一些巨集定義,列印巨集定義,型別判斷,通知使用,執行緒使用等,和一些UIKit層的巨集定義,螢幕方面的巨集定義,顏色方面的巨集定義,圖片巨集定義SPMacro

5.SPWebView---SPWebView是一個基於OC程式碼實現的WebView輕量級元件,將UIWebView和WKWebView的API封裝成統一的類去使用,並且在載入網頁的時候提供進度條,同時簡化JS與OC互相呼叫及傳遞資料的方式。仿微信介面SPWebView

6.SPCategory---SPWebView是一個常用類別庫,size,字串,hud等SPCategory

相關文章