NSTimer在時(shí)長(zhǎng)開發(fā)中使用頻率還是比較高的,但一個(gè)不注意可能就會(huì)造成了小問題异旧,日常使用中還需多注意才是。
NSTimer計(jì)時(shí)準(zhǔn)確嗎
問:NSTimer計(jì)時(shí)準(zhǔn)確嗎?
答:NSTimer 如果如下使用一般不準(zhǔn)確:
//在主線程中調(diào)用
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(logInfo) userInfo:nil repeats:YES];
//或者 在主線程中調(diào)用
_timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(logInfo) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
以上兩種方式直接調(diào)用NSTimer時(shí)很可能會(huì)計(jì)時(shí)不準(zhǔn)確右遭,原因如下:
NSTimer在每一次的runloop中會(huì)被處理,但當(dāng)runloop中有其他比較多的耗時(shí)操作缤削,且操作時(shí)間超過了NSTimer的間隔窘哈,那么這一次的NSTimer就會(huì)被延后處理。導(dǎo)致不準(zhǔn)確亭敢。
解決辦法:可以將NSTimer放入子線程滚婉,并手動(dòng)開啟子線程的runloop。當(dāng)前runloop中沒有其他耗時(shí)操作帅刀,所以也會(huì)相對(duì)準(zhǔn)確一些让腹。runloop model 模式的影響,當(dāng)沒有指定時(shí)扣溺,runloop默認(rèn)會(huì)添加到
RunLoopDefaultMode
中骇窍,當(dāng)頁(yè)面有tableview滑動(dòng)時(shí),主線程的runloop會(huì)切換到TrackingRunLoopMode
锥余,此模式下像鸡,NSTimer不會(huì)被觸發(fā),導(dǎo)致計(jì)時(shí)不準(zhǔn)確哈恰。
NSTimer 內(nèi)存泄漏
NSTimer涉及到內(nèi)存泄漏只估,主要是指在Timer中repeats
為YES
,即需要間隔指定時(shí)間着绷,重復(fù)調(diào)用方法蛔钙,此時(shí)需要手動(dòng)調(diào)用[self.timer invalidate]
使定時(shí)器失效。
如果repeats
為NO
荠医,則不存在內(nèi)存泄漏問題吁脱,調(diào)用完成后桑涎,定時(shí)器會(huì)自動(dòng)失效。
問題引發(fā):
一般使用NSTimer
時(shí)兼贡,用法如下:
self.timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(doSomeThing) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
上面代碼這樣寫攻冷,定時(shí)器會(huì)正常調(diào)用,但當(dāng)退出當(dāng)前頁(yè)面或者功能之后遍希,定時(shí)器依然在調(diào)用等曼,并沒有停止。想要停止凿蒜,需要手動(dòng)調(diào)用[self.timer invalidate]
方法禁谦。
可能想到下面這種寫法:
- (void)dealloc
{
[self.timer invalidate];
self.timer = nil;
}
這種寫法咋一看沒有問題,不是要手動(dòng)調(diào)用停止定時(shí)器的方法嘛废封,那在頁(yè)面釋放的時(shí)候州泊,調(diào)用就好了。
經(jīng)過測(cè)試漂洋,可以發(fā)現(xiàn)這樣寫的話遥皂,當(dāng)前對(duì)象self
不會(huì)釋放,定時(shí)器也沒有釋放刽漂,這就造成了內(nèi)存泄漏渴肉。
分析問題:
再看一下之前的代碼:
- (void)dealloc
{
[self.timer invalidate];
self.timer = nil;
}
self.timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(doSomeThing) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
self
持有了timer
, 而timer
在target
參數(shù)里持有了self
這就導(dǎo)致了相互引用。
可能有的同學(xué)立馬就有了疑惑:
-
self
不持有timer
也可以正常使用爽冕,為什么要持有 -
timer
為什么會(huì)持有self
針對(duì)以上兩個(gè)問題答疑:
-
self
持有timer
是因?yàn)樾枰谄渌牡胤绞謩?dòng)調(diào)用[self.timer invalidate]
仇祭,self
不持有timer
確實(shí)可以簡(jiǎn)單解決相互引用問題,但timer
卻無法手動(dòng)釋放颈畸。 -
runloop
需要對(duì)timer
的觀察者做保留操作乌奇,以便后續(xù)指定的時(shí)間點(diǎn)來到時(shí)做指定操作。
解決問題
解決上面的內(nèi)存泄漏問題眯娱,大致有下面幾種辦法:
-
viewWillDisappear:
手動(dòng)停止 - 重寫返回方法手動(dòng)停止
- iOS10之后礁苗,系統(tǒng)提供的
scheduledTimerWithTimeInterval:repeats:block
方法 - 去除
NSTimer
和當(dāng)前調(diào)用對(duì)象之間的循環(huán)引用。
詳細(xì)解釋:
手動(dòng)停止定時(shí)器徙缴,要有一個(gè)時(shí)機(jī)试伙。如果根據(jù)需求,剛好可以在用戶操作某項(xiàng)功能時(shí)可以主動(dòng)停止于样,算是一個(gè)比較好的辦法疏叨。但如果沒有這個(gè)時(shí)機(jī)則可以在1、2兩個(gè)方法中解決穿剖。
-
viewWillDisappear:
手動(dòng)停止- (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [self.timer invalidate]; self.timer = nil; }
需要注意在這個(gè)方法中停止蚤蔓,適用于不會(huì)從當(dāng)前頁(yè)面push出下一個(gè)頁(yè)面的情況,因?yàn)槿绻鹥ush出下一個(gè)頁(yè)面糊余,也會(huì)調(diào)用
viewWillDisappear:
秀又,可能就得不到正確的結(jié)果单寂。 -
重寫返回方法手動(dòng)停止
- (void)backButtonPressed:(id)sender { [self.timer invalidate]; self.timer = nil; }
-
蘋果估計(jì)也發(fā)現(xiàn)了timer容易導(dǎo)致內(nèi)存泄漏的問題,所以在iOS10之后吐辙,出了一個(gè)新的API宣决,使用新API是沒有內(nèi)存泄漏的。
if (@available(iOS 10.0, *)) { self.timer = [NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"do more thing"); }]; } - (void)dealloc { [self.timer invalidate]; self.timer = nil; }
調(diào)用這個(gè)新的API則不會(huì)相互強(qiáng)引用昏苏,
dealloc
方法會(huì)正常調(diào)用尊沸。 -
去除
NSTimer
和當(dāng)前調(diào)用對(duì)象之間的循環(huán)引用。
從3中的系統(tǒng)方法其實(shí)可以得到啟發(fā)捷雕,3中的方法里并沒有直接引用self
椒丧,而是讓timer引用了其他的對(duì)象壹甥,這樣就解除了相互引用救巷。
對(duì)于iOS10一下的系統(tǒng),可以模仿一下系統(tǒng)的實(shí)現(xiàn)句柠。#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface NSTimer (weak) + (NSTimer *)weak_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void(^)(NSTimer *timer))block; @end NS_ASSUME_NONNULL_END
#import "NSTimer+weak.h" @implementation NSTimer (weak) + (NSTimer *)weak_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block{ return [NSTimer scheduledTimerWithTimeInterval:inerval target:self selector:@selector(weak_blcokInvoke:) userInfo:[block copy] repeats:repeats]; } + (void)weak_blcokInvoke:(NSTimer *)timer { void (^block)(NSTimer *timer) = timer.userInfo; if (block) { block(timer); } } @end
在使用時(shí)浦译,可以如下:
self.timer = [NSTimer weak_scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) { //do some thing }]; - (void)dealloc { [self.timer invalidate]; self.timer = nil; }
可以看把target由之前原始的vc對(duì)象,轉(zhuǎn)換成了timer對(duì)象溯职,從而打破了雙向引用精盅。