iOS的幾種定時器及區(qū)別

來自我的個人博客Minecode.link

在開發(fā)中我們經(jīng)常用到定時器辟汰,iOS為我們提供了多種定時器烛恤,包括NSTimer廓潜、CADisplayLink抵皱、GCD善榛、NSThread(performSelector:afterDelay:),其本質(zhì)都是通過RunLoop來實現(xiàn)叨叙,但GCD通過其調(diào)度機(jī)制大大提高了性能。定時器的使用中容易存在一些誤區(qū)堪澎,故寫本文總結(jié)擂错。

本文將介紹iOS的幾種定時器、定時器的立即執(zhí)行方法樱蛤、內(nèi)存泄露問題钮呀、不準(zhǔn)時問題

NSTimer

iOS中最基本的定時器,在Swift中稱為Timer昨凡。其通過RunLoop來實現(xiàn)爽醋,一般情況下較為準(zhǔn)確,但當(dāng)當(dāng)前循環(huán)耗時操作較多時便脊,會出現(xiàn)延遲問題蚂四。同時,也受所加入的RunLoop的RunLoopMode影響哪痰,具體可以參考RunLoop的特性遂赠。

創(chuàng)建

構(gòu)造方法主要分為自動啟動和手動啟動,手動啟動的構(gòu)造方法需要我們在創(chuàng)建NSTimer后手動啟動它:

/// 構(gòu)造并開啟(啟動NSTimer本質(zhì)上是將其加入RunLoop中)
// "scheduledTimer"前綴的為自動啟動NSTimer的晌杰,如:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block

/// 構(gòu)造但不開啟
// "timer"前綴的為只構(gòu)造不啟用的跷睦,如:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block

前面說到,定時器的本質(zhì)是加入到了RunLoop的Timer列表中肋演,從而隨著運(yùn)行循環(huán)來實現(xiàn)定時器的功能抑诸。所以NSTimer除了構(gòu)造,還需要加入RunLoop爹殊。關(guān)于RunLoop簡單實用可以見文末蜕乡。

釋放

定時器的釋放一定要先將其終止,而后才能銷毀對象梗夸。具體原因下文會說异希。

- (void)invalidate;

立即執(zhí)行(fire)

我們對定時器設(shè)置了延時之后,有時需要讓它立刻執(zhí)行绒瘦,可以使用fire方法:

- (void)fire;

但是該方法的使用需要注意: fire方法不會改變預(yù)定周期性調(diào)度称簿。什么意思呢?就是說惰帽,如果我們把Timer設(shè)置為循環(huán)調(diào)用憨降,那么我們?nèi)魏螘r候調(diào)用fire方法,下一次調(diào)度的時間仍舊是按照預(yù)定時間该酗,而非基于本次執(zhí)行的時間計算而得授药。這里需要特別注意士嚎,我們可以參考下面的??:

// Declare a timer with 10s interval
self.timer1 = [NSTimer timerWithTimeInterval:10.0 target:self selector:@selector(timerMethod1) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:self.timer1 forMode:NSRunLoopCommonModes];

self.timer2 = [NSTimer timerWithTimeInterval:10.0 target:self selector:@selector(timerMethod2) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer2 forMode:NSRunLoopCommonModes];

NSLog(@"Launch %@", [NSDate date]);

// Fire at 8.0s
__weak typeof(self) weakSelf = self;
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (ino64_t)(8.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^{
    __strong typeof(self) strongSelf = weakSelf;
    [strongSelf.timer1 fire];
    [strongSelf.timer2 fire];
});

// Log
- (void)timerMethod1 {
    static int timerIdx1 = 0;
    NSLog(@"Timer Method1: %@ %d", [NSDate date], timerIdx1++);
}
- (void)timerMethod2 {
    static int timerIdx2 = 0;
    NSLog(@"Timer Method2: %@ %d", [NSDate date], timerIdx2++);
}

我們定義了兩個NSTimer并加入到RUnLoop中,其目標(biāo)方法和其他屬性均相同悔叽,唯一區(qū)別是前者只運(yùn)行一次莱衩。

我們在第8秒時調(diào)用fire方法,結(jié)果如何呢娇澎? timer1立即執(zhí)行笨蚁,并且由于僅執(zhí)行一次,其任務(wù)結(jié)束趟庄。而timer2在第8秒執(zhí)行后括细,仍舊在第10秒執(zhí)行,這樣的結(jié)果說明了fire方法不會改變預(yù)定周期性調(diào)度戚啥。

CADisplayLink

CADisplayLink是基于屏幕刷新的周期奋单,所以其一般很準(zhǔn)時,每秒刷新60次猫十。其本質(zhì)也是通過RunLoop览濒,所以不難看出,當(dāng)RunLoop選擇其他模式或被耗時操作過多時拖云,仍舊會造成延遲匾七。

其使用步驟為 創(chuàng)建CADisplayLink->添加至RunLoop中->終止->銷毀。代碼如下:

// 創(chuàng)建CADisplayLink
CADisplayLink *disLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkMethod)];
// 添加至RunLoop中
[disLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
// 終止定時器
[disLink invalidate];
// 銷毀對象
disLink = nil;

由于其并非NSTimer的子類江兢,直接使用NSRunLoop的添加Timer方法無法加入昨忆,應(yīng)使用CADisplayLink自己的addToRunLoop:forMode:方法。

同時杉允,由于其是基于屏幕刷新的邑贴,所以也度量單位是每幀,其提供了根據(jù)屏幕刷新來設(shè)置間隔的frameInterval屬性叔磷,其決定于屏幕刷新多少幀時調(diào)用一次該方法拢驾,默認(rèn)為1,即1/60秒調(diào)用一次改基。

如果我們想要計算出每次調(diào)用的時間間隔繁疤,可以通過frameInterval * duration求出,后者為屏幕每幀間隔的只讀屬性秕狰。

在日常開發(fā)中稠腊,適當(dāng)使用CADisplayLink甚至有優(yōu)化作用。比如對于需要動態(tài)計算進(jìn)度的進(jìn)度條鸣哀,由于起進(jìn)度反饋主要是為了UI更新架忌,那么當(dāng)計算進(jìn)度的頻率超過幀數(shù)時,就造成了很多無謂的計算我衬。如果將計算進(jìn)度的方法綁定到CADisplayLink上來調(diào)用叹放,則只在每次屏幕刷新時計算進(jìn)度饰恕,優(yōu)化了性能。MBProcessHUB則是利用了這一特性井仰。

GCD

GCD定時器是dispatch_source_t類型的變量埋嵌,其可以實現(xiàn)更加精準(zhǔn)的定時效果。我們來看看如何使用:

/** 創(chuàng)建定時器對象
 * para1: DISPATCH_SOURCE_TYPE_TIMER 為定時器類型
 * para2-3: 中間兩個參數(shù)對定時器無用
 * para4: 最后為在什么調(diào)度隊列中使用
 */
_gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
/** 設(shè)置定時器
 * para2: 任務(wù)開始時間
 * para3: 任務(wù)的間隔
 * para4: 可接受的誤差時間俱恶,設(shè)置0即不允許出現(xiàn)誤差
 * Tips: 單位均為納秒
 */
dispatch_source_set_timer(_gcdTimer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
/** 設(shè)置定時器任務(wù)
 * 可以通過block方式
 * 也可以通過C函數(shù)方式
 */
dispatch_source_set_event_handler(_gcdTimer, ^{
    static int gcdIdx = 0;
    NSLog(@"GCD Method: %d", gcdIdx++);
    NSLog(@"%@", [NSThread currentThread]);
    
    if(gcdIdx == 5) {
        // 終止定時器
        dispatch_suspend(_gcdTimer);
    }
});
// 啟動任務(wù)雹嗦,GCD計時器創(chuàng)建后需要手動啟動
dispatch_resume(_gcdTimer);

GCD更準(zhǔn)時的原因

通過觀察代碼,我們可以發(fā)現(xiàn)GCD定時器實際上是使用了dispatch源(dispatch source)速那,dispatch源監(jiān)聽系統(tǒng)內(nèi)核對象并處理俐银。dispatch類似生產(chǎn)者消費(fèi)者模式尿背,通過監(jiān)聽系統(tǒng)內(nèi)核對象端仰,在生產(chǎn)者生產(chǎn)數(shù)據(jù)后自動通知相應(yīng)的dispatch隊列執(zhí)行,后者充當(dāng)消費(fèi)者田藐。通過系統(tǒng)級調(diào)用荔烧,更加精準(zhǔn)。

定時器不準(zhǔn)時的問題及解決

通過上文的敘述汽久,我們大致了解了定時器不準(zhǔn)時的原因鹤竭,總結(jié)一下主要是

  • 當(dāng)前RunLoop過于繁忙
  • RunLoop模式與定時器所在模式不同

上面解釋了GCD更加準(zhǔn)時的原因,所以解決方案也不難得出:

  • 避免過多耗時操作并發(fā)
  • 采用GCD定時器
  • 創(chuàng)建新線程并開啟RunLoop景醇,將定時器加入其中(適度使用)
  • 將定時器添加到NSRunLoopCommonModes(使用不當(dāng)會阻塞UI響應(yīng))

其中后兩者在使用前應(yīng)確保合理使用臀稚,否則會產(chǎn)生負(fù)面影響。

定時器的內(nèi)存泄露問題

定時器在使用時應(yīng)格外注意內(nèi)存管理三痰,常見情況時定時器對象無法釋放造成內(nèi)存泄露吧寺,而嚴(yán)重情況會造成控制器也無法釋放,危害更大散劫。其內(nèi)存泄露有兩部分問題稚机,我們先來看第一部分:

問題1: NSTimer無法釋放

我們知道,NSTimer實際上是加入到RunLoop中的获搏,那么在其啟動時其被RunLoop強(qiáng)引用赖条,那么即使我們在后面將定時器設(shè)為nil,也只是引用計數(shù)減少了1常熙,其仍因為被RunLoop引用而無法釋放纬乍,造成內(nèi)存泄露。

問題2: 控制器無法釋放

這是NSTimer無法釋放所造成的更嚴(yán)重問題裸卫,由于為定時器設(shè)置了target蕾额,控制器就會得到一個來自定時器的引用。我們來分析一下這個情況彼城,首先定時器必須被強(qiáng)引用诅蝶,否則將在autoreleasepool之后被釋放掉造成野指針退个。而定時器通過target屬性對控制器產(chǎn)生一個強(qiáng)引用,造成了循環(huán)引用调炬。

那么如何解決這兩個問題呢语盈?答案就是使用invalidate方法。

蘋果文檔介紹如下:

This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.
If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.

即缰泡,invalidate方法會將定時器從RunLoop中移除刀荒,同時解除對target等對象的強(qiáng)引用。

CADisplayLink同理棘钞,而GCD定時器則使用dispatch_suspend()

更多技術(shù)文章缠借,歡迎訪問
本人博客 - Minecode's Blog
Github - Minecodecraft

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市宜猜,隨后出現(xiàn)的幾起案子泼返,更是在濱河造成了極大的恐慌,老刑警劉巖姨拥,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绅喉,死亡現(xiàn)場離奇詭異,居然都是意外死亡叫乌,警方通過查閱死者的電腦和手機(jī)柴罐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來憨奸,“玉大人革屠,你說我怎么就攤上這事∨旁祝” “怎么了似芝?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長额各。 經(jīng)常有香客問我国觉,道長,這世上最難降的妖魔是什么虾啦? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任麻诀,我火速辦了婚禮,結(jié)果婚禮上傲醉,老公的妹妹穿的比我還像新娘蝇闭。我一直安慰自己,他們只是感情好硬毕,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布呻引。 她就那樣靜靜地躺著,像睡著了一般吐咳。 火紅的嫁衣襯著肌膚如雪逻悠。 梳的紋絲不亂的頭發(fā)上元践,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天,我揣著相機(jī)與錄音童谒,去河邊找鬼单旁。 笑死,一個胖子當(dāng)著我的面吹牛饥伊,可吹牛的內(nèi)容都是我干的象浑。 我是一名探鬼主播,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼琅豆,長吁一口氣:“原來是場噩夢啊……” “哼愉豺!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起茫因,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蚪拦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后节腐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體外盯,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡摘盆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年翼雀,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片孩擂。...
    茶點(diǎn)故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡狼渊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出类垦,到底是詐尸還是另有隱情狈邑,我是刑警寧澤,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布蚤认,位于F島的核電站米苹,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏砰琢。R本人自食惡果不足惜蘸嘶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望陪汽。 院中可真熱鬧训唱,春花似錦、人聲如沸挚冤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽训挡。三九已至澳骤,卻和暖如春歧强,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背为肮。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工誊锭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人弥锄。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓丧靡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親籽暇。 傳聞我的和親對象是個殘疾皇子温治,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評論 2 359

推薦閱讀更多精彩內(nèi)容