NSTimer 常見疑問

這篇文章不科普原理,原理網(wǎng)上隨便搜,這里只聊常見的疑問。

  • runloop與內(nèi)存泄漏
  • target方式循環(huán)引用
  • block方式循環(huán)引用
  • 其它定時(shí)器
run loop與內(nèi)存泄漏

1、子線程使用NSTimer需要注意什么?

子線程需要?jiǎng)?chuàng)建run loop,model設(shè)為default,NSTimer添加到run loop中,否則不會(huì)運(yùn)行。NSTimer的Action更新UI注意回到主線程中。

2、NSTimer不添加到run loop可以運(yùn)行嗎?

可以,但是沒有實(shí)際意義。

dispatch_async(dispatch_get_global_queue(0, 0), ^{
     NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"run without run loop");
        }];
     [timer fire];
});

通過fire方法可以手動(dòng)觸發(fā)NSTimer的任務(wù),但是TimeInterval和repeats的設(shè)置都沒作用,相當(dāng)于直接寫這樣的代碼:

NSLog(@"run without run loop");

在實(shí)際應(yīng)用場(chǎng)景中意義不大。

3、NStimer的TimeInterval為什么不精準(zhǔn)?

一方面是因?yàn)榫€程阻塞的時(shí)候run loop會(huì)在下一次run loop循環(huán)中fire;一方面是因?yàn)镹STimer本身有Tolerance,允許出現(xiàn)容忍誤差,即使Tolerance設(shè)置為0,系統(tǒng)還是會(huì)給NSTimer設(shè)置Tolerance不為0的值。

4、invalidate的實(shí)際作用是什么?

  • 作用是移除run loop對(duì)NSTimer對(duì)象的強(qiáng)引用,移除NSTimer對(duì)target和userinfo的強(qiáng)引用。所以invalidate避免了run loop不釋放NSTimer對(duì)象的問題從而避免內(nèi)存泄漏,同時(shí)打破了和self可能存在的循環(huán)引用關(guān)系從而避免內(nèi)存泄漏。
  • invalidate必須調(diào)用,這是唯一在run loop中移除NStimer對(duì)象的方法。repeats為NO會(huì)自動(dòng)調(diào)用,為YES需要自己手動(dòng)調(diào)用。

5、invalidate不在NSTimer對(duì)象注冊(cè)的線程執(zhí)行會(huì)怎樣?

移除NSTimer對(duì)target和userinfo的強(qiáng)引用,可能不會(huì)移除run loop對(duì)NSTimer對(duì)象的強(qiáng)引用從而導(dǎo)致內(nèi)存泄漏,注意是可能

target方式循環(huán)引用

1、target設(shè)置為weak引用是否可以打破循環(huán)引用?

不可以,target不支持weak引用。

C++代碼可以看到這一點(diǎn):

/var/folders/27/wz97zmvn6fx_pw_kzbbrrm580000gn/T/BViewController-32f70f.mi:60696:20: error: 
cannot create __weak reference because the current deployment target does not support weak references
__attribute__((objc_ownership(weak))) typeof(self) weakself = self;

OC代碼沒報(bào)錯(cuò)是因?yàn)閣eak指針指向?qū)ο笾羔?,順著這條路,target依然可以強(qiáng)引用NSTimer對(duì)象。

2、怎么打破target方式產(chǎn)生的循環(huán)引用?

  • dealloc之前在適當(dāng)?shù)牡胤絠nvalidate
  • 繼承NSProxy,在NSProxy子類中weak引用self,并實(shí)現(xiàn)消息轉(zhuǎn)發(fā)self,NSTimer的target設(shè)置為NSProxy子類的對(duì)象,最后別忘了dealloc中invalidate。不然雖然解決了循環(huán)引用,但是run loop不釋放NSTimer對(duì)象還是會(huì)內(nèi)存泄漏。

3、UIControl和NSTimer的Target-Action有什么不同?

  • UIControl的target在內(nèi)部使用了weak引用,所以不會(huì)有循環(huán)引用問題。
  • NStimer的target在內(nèi)部如果也使用weak引用會(huì)造成比循環(huán)引用更難以發(fā)現(xiàn)的內(nèi)存泄漏問題。NSTimer的觸發(fā)是通過run loop向target發(fā)消息,如果target是weak引用,那么target釋放后,weak 指針對(duì)自動(dòng)置為nil,向nil發(fā)消息不會(huì)報(bào)錯(cuò),但也不會(huì)執(zhí)行action,這就給你造成了一個(gè)錯(cuò)覺,NSTimer對(duì)象似乎隨著target的釋放也釋放了,然而NSTimer對(duì)象其實(shí)還被run loop持有者永遠(yuǎn)不會(huì)釋放,也就造成了內(nèi)存泄漏。所以我們可以理解為NSTimer的循環(huán)引用問題是蘋果故意給我們?cè)O(shè)計(jì)的,蘋果希望我們可以通過invalidate方法顯示的打破循環(huán)引用,同時(shí)讓run loop釋放NSTimer對(duì)象。
  • iOS 10之后,NSTimer給我們提供了Block的方法,這個(gè)方法不存在target-action的循環(huán)引用問題,block實(shí)現(xiàn)不故意設(shè)計(jì)成循環(huán)引用的原因在于Block的持有者是NSTimer對(duì)象本身,只要run loop沒釋放NSTimer對(duì)象,block就可以一直執(zhí)行。所以即使NSTimer的持有者被釋放了,我們依然可以顯示的看到定時(shí)器還在運(yùn)行著,從而知道NSTimer對(duì)象還沒有釋放,發(fā)現(xiàn)內(nèi)存泄漏問題。
block方式循環(huán)引用

在block中weak引用self。注意invalidate,invalidate在block中或者在dealloc中都可以,否則run loop不釋放NSTimer對(duì)象還是會(huì)內(nèi)存泄漏。

1、低于iOS 10.0的版本,NSTimer沒有block的創(chuàng)建方法怎么辦?

添加NSTimer的分類,分類中定義block方式的創(chuàng)建方法(類方法),創(chuàng)建方法內(nèi)部實(shí)現(xiàn)用target方式,target設(shè)置為NSTimer類對(duì)象。
block內(nèi)部weak引用self:
self->timer->NSTimer類對(duì)象->weakself

  • 如果不weak引用self,即使在dealloc之前invalidate打破timer->NSTimer類對(duì)象進(jìn)而打破循環(huán)引用,但是由于NSTimer類對(duì)象不會(huì)釋放,進(jìn)而導(dǎo)致self也不會(huì)釋放。
  • weak引用self,不會(huì)循環(huán)引用,invalidate在block中或者在dealloc中都可以。如果不調(diào)用invalidate,run loop不釋放NSTimer對(duì)象還是會(huì)內(nèi)存泄漏。
其它定時(shí)器

1、GCD定時(shí)器
2、CADisplayLink
這兩個(gè)以后有時(shí)間詳細(xì)寫一下。

最后提供一個(gè)OC版的定時(shí)器庫(kù)吧。
https://github.com/clarkIsMe/CYTimer.git

提供了6個(gè)類方法,包含了NSTimer、CADisplayLink、GCD定時(shí)器的Target-Action調(diào)用方式和Block調(diào)用方式。
內(nèi)部解決了內(nèi)存泄漏的問題,使用這個(gè)6個(gè)類方法去創(chuàng)建定時(shí)器,可以完全忽略定時(shí)器給我們帶來的坑,讓我們更加專注在業(yè)務(wù)開發(fā)上。
同時(shí)提供了APP進(jìn)入后臺(tái),進(jìn)入前臺(tái),以及當(dāng)前控制器生命周期的AOP回調(diào),讓我們?cè)趯懴嚓P(guān)場(chǎng)景的業(yè)務(wù)時(shí)代碼不再到處飛了。

后期會(huì)更新哦,有興趣的小伙伴可以關(guān)注下,準(zhǔn)備加上一些自帶定時(shí)器的控件,定時(shí)器與控件的生命周期綁定,完全不再操心定時(shí)器的任何問題。

/// NSTimer的Target-Action實(shí)現(xiàn)方式,自動(dòng)檢測(cè)runloop并添加,內(nèi)部解決了循環(huán)引用問題,內(nèi)部自動(dòng)調(diào)用了invalidate,避免內(nèi)存泄漏
/// @param ti 調(diào)用間隔,單位 s
/// @param aTarget 目標(biāo)對(duì)象
/// @param aSelector 回調(diào)方法
/// @param userInfo 傳參
/// @param yesOrNo 是否重復(fù)執(zhí)行

+ (CYTimer *)scheduledNormalTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo API_AVAILABLE(ios(8.0));


/// NSTimer的Block實(shí)現(xiàn)方式,自動(dòng)檢測(cè)runloop并添加,內(nèi)部解決了循環(huán)引用問題,內(nèi)部自動(dòng)調(diào)用了invalidate,避免內(nèi)存泄漏
/// @param interval 調(diào)用間隔,單位 s
/// @param aTarget CYTimer的生命周期與aTarget綁定,aTarget不建議使用weak 引用,雖然weak引用不會(huì)導(dǎo)致任何問題。
/// @param repeats 是否重復(fù)執(zhí)行
/// @param block 回調(diào)

+ (CYTimer *)scheduledNormalTimerWithTimeInterval:(NSTimeInterval)interval bindTo:(id)aTarget repeats:(BOOL)repeats block:(void (^)(CYTimer *timer))block API_AVAILABLE(ios(8.0));


/// CADisplayLink的Target-Action實(shí)現(xiàn)方式,自動(dòng)檢測(cè)runloop并添加,內(nèi)部解決了循環(huán)引用問題,內(nèi)部自動(dòng)調(diào)用了invalidate,避免內(nèi)存泄漏
/// @param ti 調(diào)用間隔,單位 s
/// @param aTarget 目標(biāo)對(duì)象
/// @param aSelector 回調(diào)方法

+ (CYTimer *)scheduledFPSTimerWithFrameInterval:(NSUInteger)ti target:(id)aTarget selector:(SEL)aSelector API_AVAILABLE(ios(8.0));


/// CADisplayLink的Block實(shí)現(xiàn)方式,自動(dòng)檢測(cè)runloop并添加,內(nèi)部解決了循環(huán)引用問題,內(nèi)部自動(dòng)調(diào)用了invalidate,避免內(nèi)存泄漏
/// @param interval  iOS 10以后該參數(shù)代表每秒執(zhí)行的次數(shù),0 為代表每一幀都調(diào)用;iOS 10以前每 frameInterval  幀的調(diào)用一次,1 為每一幀都調(diào)用,不可以小于1
/// @param aTarget CYTimer的生命周期與aTarget綁定,aTarget不建議使用weak 引用,雖然weak引用不會(huì)導(dǎo)致任何問題。
/// @param block 回調(diào)

+ (CYTimer *)scheduledFPSTimerWithFrameInterval:(NSUInteger)interval bindTo:(id)aTarget block:(void (^)(CYTimer *timer))block API_AVAILABLE(ios(8.0));


/// GCD定時(shí)器的Target-Action實(shí)現(xiàn)方式,自動(dòng)檢測(cè)runloop并添加,內(nèi)部解決了循環(huán)引用問題,內(nèi)部自動(dòng)調(diào)用了invalidate,避免內(nèi)存泄漏
/// @param ti  iOS 10以后該參數(shù)代表每秒執(zhí)行的次數(shù),0 為代表每一幀都調(diào)用;iOS 10以前每 frameInterval  幀的調(diào)用一次,1 為每一幀都調(diào)用,不可以小于1
/// @param aTarget 目標(biāo)對(duì)象
/// @param aSelector 回調(diào)方法

+ (CYTimer *)scheduledGCDTimerWithTimeInterval:(NSUInteger)ti target:(id)aTarget selector:(SEL)aSelector API_AVAILABLE(ios(8.0));


/// GCD定時(shí)器的Block實(shí)現(xiàn)方式,自動(dòng)檢測(cè)runloop并添加,內(nèi)部解決了循環(huán)引用問題,內(nèi)部自動(dòng)調(diào)用了invalidate,避免內(nèi)存泄漏
/// @param interval 調(diào)用間隔,單位 s
/// @param aTarget CYTimer的生命周期與aTarget綁定,aTarget不建議使用weak 引用,雖然weak引用不會(huì)導(dǎo)致任何問題。
/// @param block 回調(diào)

+ (CYTimer *)scheduledGCDTimerWithTimeInterval:(NSUInteger)interval bindTo:(id)aTarget block:(void (^)(CYTimer *timer))block API_AVAILABLE(ios(8.0));

做個(gè)這個(gè)組件的初衷:
1、提供干凈的定時(shí)器調(diào)用方式,不用考慮循環(huán)引用、內(nèi)存泄漏等等問題,不用時(shí)刻想著銷毀定時(shí)器,讓我們更加專注在業(yè)務(wù)上。
2、提供不同原理實(shí)現(xiàn)的定時(shí)器來更好的適應(yīng)業(yè)務(wù)場(chǎng)景。
3、提供適當(dāng)?shù)腁OP。

需要知道的地方:
1、如果你聲明了CYTimer類型的成員變量,然后直接調(diào)用CYTimer的類方法去執(zhí)行任務(wù),沒有用 = 給成員變量賦值,那么這個(gè)賦值過程會(huì)自動(dòng)發(fā)生;如果CYTimer類型的成員變量個(gè)數(shù)超過一個(gè),這個(gè)自動(dòng)賦值的過程就不會(huì)發(fā)生了。
2、如果你采用CYTimer的Block方式調(diào)用,那么你仍然要注意Block內(nèi)部弱引用self,這個(gè)組件是解決定時(shí)器的問題,不是block。
3、除了block內(nèi)部你自己寫的代碼里注意循環(huán)引用,其它地方你將不再需要關(guān)心self是否需要弱引用,怎么樣都可以
4、dealloc里不要求調(diào)用 invalidate 方法,當(dāng)然你要調(diào)用也可以。
5、使用normal定時(shí)器,你不用關(guān)心runloop是否會(huì)釋放NSTimer,這個(gè)釋放過程是自動(dòng)發(fā)生的。
6、normal和FPS的定時(shí)器都是在當(dāng)前線程的runloop中,模式是 NSRunLoopCommonModes,如果你需要自己靈活設(shè)置模式,請(qǐng)告訴我。
7、block回調(diào)已經(jīng)自動(dòng)切回了主線程,你沒必要在自己的block代碼再切一次。

使用建議:
1、非動(dòng)畫類推薦使用用GCD的方法。
2、動(dòng)畫類的推薦使用FPS的方法。
3、如果你偏愛用NSTimer,那你也可以選擇normal的方法,而且讓你使用中不再有坑。但是它不準(zhǔn)呀大兄弟,為啥你非得用。

如果有大神對(duì)我這篇文章的某些地方有不同看法,請(qǐng)一定指出,最后感謝您花費(fèi)寶貴的時(shí)間看完這篇文章。

最后編輯于
?著作權(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ù)。

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