[iOS-Foundation] NSTimer

參考資料

NSTimer
深入理解RunLoop
《編寫高質(zhì)量iOS與OS X代碼的52個(gè)有效方法》中第52條:別忘了 NSTimer 會(huì)保留其目標(biāo)對(duì)象

定時(shí)器

定時(shí)器是線程通知自己做某事的一種方法。iOS 中的定時(shí)器由 NSTimer 實(shí)現(xiàn),通過它可以在一段時(shí)間后執(zhí)行一次或循環(huán)執(zhí)行多次某一對(duì)象上的特定方法。NSTimer 對(duì)象必須作為定時(shí)源加入到線程的 runloop 中才可以工作,runloop 對(duì)象會(huì)強(qiáng)引用被加入的 NSTimer 對(duì)象。如果不想讓定時(shí)器繼續(xù)執(zhí)行任務(wù),則需要將定時(shí)器變?yōu)槭顟B(tài)。只執(zhí)行一次的定時(shí)器會(huì)在任務(wù)執(zhí)行后自動(dòng)變成失效狀態(tài),而循環(huán)執(zhí)行的定時(shí)器則需要通過調(diào)用方法[- invalidate]來(lái)將定時(shí)器對(duì)象變?yōu)槭顟B(tài)。runloop 不再引用失效的定時(shí)器對(duì)象。而且,失效的定時(shí)器對(duì)象不能再被重新使用。注意,只能在創(chuàng)建 NSTimer 對(duì)象的線程中調(diào)用[- invalidate]方法,否則,Runloop 可能無(wú)法正確移除定時(shí)源。

可以通過[- fire]方法讓定時(shí)任務(wù)直接執(zhí)行,如果定時(shí)器只執(zhí)行一次,那么定時(shí)器自動(dòng)失效。如果定時(shí)器是循環(huán)執(zhí)行,那么該方法并不影響定時(shí)器的定期執(zhí)行。

NSTimer 并不能保證定時(shí)任務(wù)一定會(huì)執(zhí)行,例如在觸發(fā)任務(wù)的時(shí)間點(diǎn),runloop 恰好在執(zhí)行一個(gè)非常耗時(shí)的任務(wù),或者 runloop 的模式中并不包括監(jiān)聽定時(shí)源,那么定時(shí)任務(wù)都無(wú)法執(zhí)行。對(duì)于循環(huán)執(zhí)行的定時(shí)器,如果錯(cuò)過了多個(gè)周期的執(zhí)行時(shí)間點(diǎn),timer 對(duì)象并不會(huì)彌補(bǔ)這些執(zhí)行次數(shù),而只是直到下一次的觸發(fā)時(shí)間點(diǎn)再進(jìn)行嘗試。為了提高系統(tǒng)的靈活性,通過設(shè)置 NSTimer 對(duì)象的tolerance屬性,可以讓觸發(fā)任務(wù)的時(shí)間點(diǎn)有一定的延遲誤差值,這樣任務(wù)可能會(huì)比設(shè)定的時(shí)間晚一點(diǎn)執(zhí)行,但一定不會(huì)早于設(shè)置的時(shí)間點(diǎn)。該屬性的默認(rèn)值是0,建議的值在間隔時(shí)間的10%以內(nèi)。對(duì)于循環(huán)執(zhí)行的定時(shí)器,下一次執(zhí)行任務(wù)的時(shí)間點(diǎn)會(huì)根據(jù)原始時(shí)間,而不是延遲后的執(zhí)行時(shí)間來(lái)增加時(shí)間間隔進(jìn)行設(shè)置。

通過下列方法可以創(chuàng)建一個(gè) timer 對(duì)象,并加入到當(dāng)前線程的 runloop 中。一旦將定時(shí)器加入到線程的 runloop 中,定時(shí)器就會(huì)在設(shè)置的時(shí)間點(diǎn)執(zhí)行任務(wù)。

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

通過下列方法可以創(chuàng)建一個(gè) NSTimer 對(duì)象,之后可通過 NSRunloop 對(duì)象的[- addTimer:forMode:]方法將 timer 加入到指定線程的 runloop 中。

// 創(chuàng)建一個(gè) timer 對(duì)象,
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep;

NSTimer 涉及的循環(huán)引用問題

由于計(jì)時(shí)器會(huì)保留其目標(biāo)對(duì)象,等到自身失效時(shí)再釋放此對(duì)象,所以設(shè)置成重復(fù)執(zhí)行模式的計(jì)時(shí)器,很容易出現(xiàn)循環(huán)引用的問題。如下列代碼:

#import "TimerTestClass.h"

@interface TimerTestClass ()

@property (nonatomic) NSTimer *timer;

@end

@implementation TimerTestClass

#pragma mark - Override

- (void)dealloc {
    [self.timer invalidate];
}

#pragma mark - Public

- (void)start {
    self.timer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
}

- (void)stop {
    [self.timer invalidate];
    self.timer = nil;
}

#pragma mark - Private

- (void)doSomething {
    // Do something
}

@end

如果創(chuàng)建了本類的實(shí)例,并調(diào)用了[- start]方法,那么實(shí)例通過屬性保留了計(jì)時(shí)器,而計(jì)時(shí)器的目標(biāo)對(duì)象又是實(shí)例本身,所以計(jì)時(shí)器也保留了目標(biāo)對(duì)象,此時(shí)就產(chǎn)生了保留環(huán)。

當(dāng)指向 TimerTestClass 類實(shí)例的所有外部引用都移走后,因?yàn)楸A舡h(huán),該實(shí)例仍然會(huì)繼續(xù)存活,且無(wú)法回收,于是就造成了內(nèi)存泄漏。這種內(nèi)存泄漏問題尤為嚴(yán)重,因?yàn)橛?jì)時(shí)器還將繼續(xù)反復(fù)地執(zhí)行輪詢?nèi)蝿?wù),造成更多不必要的資源消耗。

代碼里想在系統(tǒng)回收本類實(shí)例的過程中令計(jì)時(shí)器無(wú)效,從而打破保留環(huán)的方法實(shí)際上是不可能的,因?yàn)楸A舡h(huán)存在的情況下,實(shí)例的保留計(jì)數(shù)并不為0,系統(tǒng)無(wú)法將實(shí)例回收。要想打破保留環(huán),只有通過調(diào)用[- stop]方法,令計(jì)時(shí)器失效,從而不再引用本類的實(shí)例。但無(wú)法確保外界對(duì)象會(huì)在釋放最后一個(gè)指向本實(shí)例的引用之前,一定會(huì)調(diào)用[- stop]方法,所以這種方式也是不安全的。

這個(gè)問題可通過 block 來(lái)解決,為 NSTimer 添加分類如下:

#import "NSTimer+BlockSupport.h"

@implementation NSTimer (BlockSupport)

+ (NSTimer *)demo_scheduledTimerWithTimeInterval:(NSTimeInterval)ti block:(void (^)())block repeats:(BOOL)yesOrNo {
    return [self scheduledTimerWithTimeInterval:ti target:self selector:@selector(blockInvoke:) userInfo:[block copy] repeats:yesOrNo];
}

+ (void)blockInvoke:(NSTimer *)timer {
    void (^block) () = timer.userInfo;
    if (block) { block(); }
}

@end

這段代碼將計(jì)時(shí)器所應(yīng)執(zhí)行的任務(wù)封裝成 block,在調(diào)用計(jì)時(shí)器函數(shù)時(shí),把它作為 userInfo 參數(shù)傳入,只要計(jì)時(shí)器還有效,就會(huì)一直保留著它。計(jì)時(shí)器現(xiàn)在的 target 是 NSTimer 類對(duì)象,這是個(gè)單例,因此計(jì)時(shí)器是否會(huì)保留它,其實(shí)都無(wú)所謂。單純的將計(jì)時(shí)器任務(wù)封裝成塊還不能解決問題,修改[- start]方法如下:

- (void)start {
    self.timer = [NSTimer demo_scheduledTimerWithTimeInterval:5 block:^{
        [self doSomething];
    } repeats:YES];
}

因?yàn)閴K捕獲了 self 變量,所以塊要保留實(shí)例。而計(jì)時(shí)器又通過 userInfo 參數(shù)保留了塊。最后,實(shí)例本身還要保留計(jì)時(shí)器。這時(shí),循環(huán)引用的問題依然存在。不過,只要改用弱引用,即可打破保留環(huán):

- (void)start {
    __weak TimerTestClass *weakSelf = self;
    
    self.timer = [NSTimer demo_scheduledTimerWithTimeInterval:5 block:^{
        TimerTestClass *strongSelf = weakSelf;
        [strongSelf doSomething];
    } repeats:YES];
}

因?yàn)閴K捕獲的是實(shí)例的弱引用,所以 self 不會(huì)為計(jì)時(shí)器所保留。當(dāng)塊開始執(zhí)行時(shí),立刻生成 strong 引用,以保證實(shí)例在執(zhí)行期間持續(xù)存活。這樣,當(dāng)外界指向類實(shí)例的最后一個(gè)引用將其釋放,則該實(shí)例就可為系統(tǒng)所回收了,回收過程中還會(huì)調(diào)用計(jì)時(shí)器的[- invalidate]方法。

值得注意的是,從 iOS10 開始,NSTimer 類本身就已經(jīng)支持了通過 Block 傳入任務(wù)的方式:

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

Runloop

RunLoop 是線程中的事件處理的循環(huán)。使用 RunLoop 的目的是讓線程在有任務(wù)時(shí)處理任務(wù),沒有任務(wù)時(shí)處于休眠狀態(tài)。

RunLoop 接收輸入事件來(lái)自兩種不同的來(lái)源:輸入源(input source)和定時(shí)源(timer source)。輸入源傳遞異步事件,通常消息來(lái)自于其他線程或程序。定時(shí)源則傳遞同步事件,發(fā)生在特定時(shí)間或者重復(fù)的時(shí)間間隔。兩種源都使用程序的某一特定的處理例程來(lái)處理到達(dá)的事件。

runloop.png

正常情況下,一個(gè)線程啟動(dòng)后,開始執(zhí)行一個(gè)任務(wù),當(dāng)任務(wù)執(zhí)行完便退出線程。如果要讓線程可以一直處于監(jiān)聽狀態(tài),隨時(shí)響應(yīng)事件,就需要在線程內(nèi)執(zhí)行一個(gè)循環(huán),直到收到退出的信號(hào)時(shí),才結(jié)束循環(huán)。這種模型通常稱為 Event Loop,在蘋果的開發(fā)體系中的實(shí)現(xiàn)就是 RunLoop。

所以,RunLoop 實(shí)際上就是一個(gè)對(duì)象,這個(gè)對(duì)象管理了其需要處理的事件和消息,并提供了一個(gè)入口函數(shù)來(lái)執(zhí)行上面 Event Loop 的邏輯。線程執(zhí)行了這個(gè)函數(shù)后,就會(huì)一直處于這個(gè)函數(shù)內(nèi)部 "接受消息->等待->處理" 的循環(huán)中,直到這個(gè)循環(huán)結(jié)束(比如傳入 quit 的消息),函數(shù)返回。

蘋果分別在 Foundation 層和 Core Foundation 層提供了 NSRunLoop 和 CFRunLoopRef 兩個(gè)API。

RunLoop 和線程是一一對(duì)應(yīng)的,可以通過 NSRunLoop 的+ mainRunLoop+ currentRunLoop獲取主線程和當(dāng)前線程的 Run Loop。線程默認(rèn)是沒有 Run Loop
的,當(dāng)?shù)谝淮潍@取時(shí),會(huì)創(chuàng)建 RunLoop。

一個(gè) RunLoop 中可以有多個(gè) Mode,RunLoop 啟動(dòng)時(shí)會(huì)應(yīng)用某一個(gè) Mode。Mode 內(nèi)包含了 Source/Timer/Observer 三類集合,分別對(duì)應(yīng)了事件的響應(yīng),固定時(shí)間點(diǎn)的響應(yīng)和 RunLoop 本身變化的響應(yīng)。如果需要切換 Mode,只能退出 Loop,再重新指定一個(gè) Mode 進(jìn)入。Mode 主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響。這里有一個(gè) CommonModes 的概念,可以把 Source/Timer/Observer 這些響應(yīng)加入到某一個(gè) Mode 中,也可以加入到 CommonModes 中,而一個(gè) Mode 可以標(biāo)記為是否是 Common 的,當(dāng) Run Loop 使用一個(gè) Mode 時(shí),如果這個(gè) Mode 是 Common 的,所有在 CommonModes 里的響應(yīng)都會(huì)加入其中,使用 CommonModes 可以避免將同一響應(yīng)分別加入不同的 Mode 中。

蘋果公開提供的 Mode 有 NSDefaultRunLoopMode 和 UITrackingRunLoopMode,NSDefaultRunLoopMode 是 App 平時(shí)所處的狀態(tài),UITrackingRunLoopMode 是追蹤 ScrollView 滑動(dòng)時(shí)的狀態(tài)。

通過 RunLoop 實(shí)現(xiàn) AutoReleasePool,蘋果在主線程 RunLoop 里注冊(cè)O(shè)bserver,進(jìn)入 RunLoop 時(shí),創(chuàng)建自動(dòng)釋放池,Loop 準(zhǔn)備進(jìn)入休眠時(shí),釋放舊的池并創(chuàng)建新池,Loop 退出時(shí),釋放自動(dòng)釋放池。

通過 RunLoop 實(shí)現(xiàn)事件響應(yīng),蘋果注冊(cè)響應(yīng)系統(tǒng)事件的 source,當(dāng)一個(gè)硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由 IOKit.framework 生成一個(gè) IOHIDEvent 事件并由 SpringBoard (iOS 的界面)接收。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨后用 mach port 轉(zhuǎn)發(fā)給需要的 App 進(jìn)程。接著蘋果注冊(cè)的那個(gè) Source 就會(huì)觸發(fā)回調(diào),把 IOHIDEvent 處理并包裝成 UIEvent 進(jìn)行處理或分發(fā),其中包括識(shí)別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow 等。當(dāng)識(shí)別了一個(gè)手勢(shì)時(shí),首先會(huì)將響應(yīng)鏈打斷,隨后系統(tǒng)將對(duì)應(yīng)的 UIGestureRecognizer 標(biāo)記為待處理,蘋果注冊(cè)了一個(gè) Observer 監(jiān)測(cè) Loop 即將進(jìn)入休眠的事件,這個(gè) Observer 的回調(diào)函數(shù)會(huì)獲取所有剛被標(biāo)記為待處理的 GestureRecognizer,并執(zhí)行GestureRecognizer的回調(diào)。

通過 RunLoop 實(shí)現(xiàn)界面更新,當(dāng)在操作 UI 時(shí),比如改變了 Frame、更新了 UIView/CALayer 的層次時(shí),或者手動(dòng)調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個(gè) UIView/CALayer 就被標(biāo)記為待處理,并被提交到一個(gè)全局的容器去。蘋果注冊(cè)了一個(gè) Observer 監(jiān)聽即將進(jìn)入休眠和即將退出 Loop 的事件,回調(diào)的函數(shù)里會(huì)遍歷所有待處理的 UIView/CAlayer 以執(zhí)行實(shí)際的繪制和調(diào)整,并更新 UI 界面。

NSTimer 就是向 Run Loop 注冊(cè) Timer 類的響應(yīng),當(dāng)?shù)竭_(dá)固定的時(shí)間點(diǎn)時(shí),Loop 就會(huì)喚醒并執(zhí)行響應(yīng)。

通過 RunLoop 實(shí)現(xiàn) PerformSelecter,當(dāng)調(diào)用 NSObject 的 performSelecter:afterDelay: 后,實(shí)際上其內(nèi)部會(huì)創(chuàng)建一個(gè) Timer 并添加到當(dāng)前線程的 RunLoop 中。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容