來自我的個人博客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