深入淺出了解NSTimer循環(huán)引用的原因

NSTimer產(chǎn)生循環(huán)引用的原因

我們首先看下NSTimer的初始化方法

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
  • timerWithTimeInterval創(chuàng)建出來的timer無法立刻使用,需要添加到NSRunloop中才可以正常工作
    「After creating it肪笋, you must add the timer to a run loop manually by calling the addTimer:forMode: method of the corresponding NSRunLoop object图张●常」
  • scheduledTimerWithTimeInterval創(chuàng)建出來的runloop已經(jīng)被添加到當前線程的currentRunloop中來了崔梗。
    「Schedules it on the current run loop in the default mode佛嬉÷甙模」

NSTimer與runloop的關(guān)系暫時不在本文詳談。我們先關(guān)注下timer為何會容易產(chǎn)生循環(huán)引用暖呕。 NSTimer會強引用target斜做,等到自身'失效'時再釋放此對象。
我們先假設(shè)開發(fā)中一個最常見的場景

#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController

- (void)viewDidLoad
{
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

- (void)doSomething
{
    NSLog(@"%s", __func__);
}

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

控制器中有一個timer屬性湾揽,且timer的target是該控制器瓤逼。如圖兩者之間會形成一個retain cycle。


retain cycle

在不主動釋放timer的前提下库物,那么控制器會一直強引用著timer霸旗,timer內(nèi)部的target也強引用著控制器,控制器的引用計數(shù)永遠不會為0戚揭。這種內(nèi)存泄漏問題尤其嚴重诱告,因為timer還在不斷的執(zhí)行著輪詢?nèi)蝿?wù),很容易導(dǎo)致其它的內(nèi)存泄漏問題民晒。


幾種不太好的解決方案

1.在dealloc中對timer進行釋放

很多人都會在控制器的dealloc方法中寫如下代碼

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

認為在控制器銷毀的時候順便銷毀timer精居,這樣一來就萬無一失了,殊不知因為循環(huán)引用dealloc方法根本沒有執(zhí)行镀虐。

2.在- (void)viewDidDisappear:(BOOL)animated中對timer進行釋放

這種方式在平時開發(fā)中是比較是比較常見的且有效的箱蟆,但是我認為有幾點不好

  • 如果當前控制器是在導(dǎo)航控制器的棧中,那么無論push/pop都會調(diào)用- (void)viewDidDisappear:(BOOL)animated 需要在該方法中判斷
- (void)viewDidDisappear:(BOOL)animated
{
    if (self.navigationController == nil) {
        [self.timer invalidate];
        self.timer = nil;
    }
}
  • 如果當前類的跳轉(zhuǎn)方式是modal呢刮便?或者說當前類并不是ViewController的子類,那么該如何判斷呢空猜?

最好的辦法是讓timer跟當前類的生命周期綁定在一起,自動化的進行釋放恨旱,減少非必要的代碼書寫辈毯。

3.使用weakSelf

很多人回想如果把傳入target的引用改為弱引用,這樣一來引用線在timer指向當前類就斷掉了搜贤,引用換就無法形成閉環(huán),那么就不會形成循環(huán)引用了谆沃。

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1.0 target:weakSelf selector:@selector(doSomething) userInfo:nil repeats:YES];

其實這是一個非常容易出錯的想法,傳參跟使用block是兩個完全不同的概念!!
weakSelf最多使用的場景是在block內(nèi)部中使用仪芒,block內(nèi)部的機制會根據(jù)捕獲的對象變量的指針類型(__weak, __strong)進行強引用或弱引用.
但是參數(shù)傳遞的本質(zhì)是將參數(shù)的地址傳過去唁影。無論是self或者是weakSelf耕陷,本質(zhì)都是一個地址。所以該方法無效据沈。

4.不使用屬性

你可能覺得不使用屬性或成員變量可切斷當前類對timer的強引用哟沫,但是當前類仍然會一直在內(nèi)存中。原因如圖
image.png

主線程的runloop在程序運行期間是不會銷毀的锌介, runloop引用著timer,timer就不會自動銷毀嗜诀。timer引用著target,target也不會銷毀。


解決方案

1.使用中間代理方法

既然循環(huán)引用的原因是因為timer和控制器之間的強引用,那么是否可以使用一個中間代理得接觸這個閉環(huán)呢孔祸?答案是可以的隆敢。整體構(gòu)思如下圖


Proxy

可以在timer與控制器之間使用一個proxy來解除兩者之間的相互強引用。

首先聲明一個.h文件
#import <Foundation/Foundation.h>
@interface LLTimerProxy1 : NSObject
+ (instancetype)proxyWithTarget:(id)target withSelector:(SEL)selector;
- (void)__execute;
@end
實現(xiàn).m文件
#import "LLTimerProxy1.h"
@interface LLTimerProxy1()
/** target */
@property (nonatomic, weak) id target;
/** SEL */
@property (nonatomic, assign) SEL selector;
@end

@implementation LLTimerProxy1


+ (instancetype)proxyWithTarget:(id)target withSelector:(SEL)selector
{
    LLTimerProxy1 *proxy = [[LLTimerProxy1 alloc] init];
    proxy.target = target;
    proxy.selector = selector;
    return proxy;
}

- (void)__execute
{
    if (_target && _selector) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [_target performSelector:_selector withObject:nil];
#pragma clang diagnostic pop
    }
}

@end
使用方式
- (void)viewDidLoad
{
    LLTimerProxy1 *proxy = [LLTimerProxy1 proxyWithTarget:self withSelector:@selector(doSomething)];
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:proxy selector:@selector(__execute) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

- (void)doSomething
{
    NSLog(@"%s", __func__);
}

- (void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;
    
    NSLog(@"%s", __func__);
}

首先梳理一下引用流程
NSTimer target -> 強引用著proxy
proxy的target -> 弱引用著控制器
這樣一來當控制器的引用計數(shù)為0的時候崔慧,會調(diào)用dealloc方法拂蝎,在dealloc方法中對timer進行釋放,timer釋放的時候也會對proxy進行釋放尊浪。這樣一來就可以讓timer的聲明周期與控制器同步了

Log

補充

  • 在proxy的__execute方法中匣屡,我做了一個if判斷,是因為有可能在target的dealloc方法中并沒有對timer進行釋放拇涤。這樣就會導(dǎo)致timer仍然runloop中運行捣作,不斷的調(diào)用__execute方法.此時的target因為釋放了,所以target為nil鹅士。像空指針發(fā)送消息并不會引起崩潰券躁,但是最好還是在該方法里添加一個判斷target是否為空的if語句來告訴開發(fā)人員某個類已經(jīng)釋放掉了,但是該類的timer沒有被釋放。

2.使用NSProxy

NSProxy是一個基類掉盅,是蘋果創(chuàng)建出來專門做代理轉(zhuǎn)發(fā)事件的基類也拜,負責(zé)將消息轉(zhuǎn)發(fā)到真正target的類

An abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet.

該類有兩個方法,有runtime儲備的同學(xué)應(yīng)該會對這兩個方法比較熟悉

- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel

NSProxy收到消息之后會在自己的方法列表中查找,如果沒有則直接會進入消息轉(zhuǎn)發(fā)趾痘。比NSObject類少了在父類的方法列表和動態(tài)解析的步驟慢哈,性能會更好。
因為Proxy可以實現(xiàn)消息轉(zhuǎn)發(fā)永票,那么本身也不用持有選擇子卵贱,這樣代碼會寫的會更明確。

.h文件
#import <Foundation/Foundation.h>

@interface LLTimerProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@end
.m實現(xiàn)文件
#import "LLTimerProxy.h"

@interface LLTimerProxy ()
/** tatget */
@property (nonatomic, weak) id target;
@end

@implementation LLTimerProxy
+ (instancetype)proxyWithTarget:(id)target
{
    LLTimerProxy *proxy = [LLTimerProxy alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    if (!self.target || ![self.target respondsToSelector:sel]) {
        return [NSMethodSignature signatureWithObjCTypes:"v:@"];
    }
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    if (!self.target) {
        NSLog(@"target已經(jīng)從內(nèi)存中死掉了");
        return;
    }
    [invocation invokeWithTarget:self.target];
}

@end
使用方式
- (void)viewDidLoad
{
    LLTimerProxy *proxy = [LLTimerProxy proxyWithTarget:self];
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:proxy selector:@selector(doSomething) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

- (void)doSomething
{
    NSLog(@"%s", __func__);
}

- (void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;
    
    NSLog(@"%s", __func__);
}

可以發(fā)現(xiàn)Proxy類并沒有引用著selector侣集,因為proxy類并沒有doSomething方法键俱,所有直接進入了消息轉(zhuǎn)發(fā)步驟.在消息轉(zhuǎn)發(fā)中將消息接受者轉(zhuǎn)發(fā)給自身持有的target,這樣就可以完成調(diào)用了。

補充

  • 使用繼承NSProxy的類的優(yōu)勢是在代碼書寫上比使用繼承自NSObject的類更加直觀世分,因為NSTimer的選擇子可以直接填寫本類的方法,而不用寫__execute方法
  • 但是使用NSProxy會‘更’容易造成崩潰编振,當然這種崩潰的原因是因為開發(fā)者沒有規(guī)范的處理timer的聲明周期。設(shè)想一種這樣的場景臭埋,類已經(jīng)釋放掉了踪央,但是timer仍然不斷的調(diào)用方法臀玄,那么在methodSignatureForSelector方法中,[self.target methodSignatureForSelector:sel];因為self.target已經(jīng)是nil了,就會導(dǎo)致return nil.methodSignatureForSelector方法中返回為空代表消息轉(zhuǎn)發(fā)失敗,會導(dǎo)致[NSProxy doesNotRecognizeSelector:doSomething崩潰.當然崩潰不一定是壞事杯瞻,容錯性是雙刃劍镐牺。有時候別人犯錯了就應(yīng)該讓它崩潰炫掐,讓別人發(fā)現(xiàn)問題魁莉,必須去解決。如果不提醒它募胃,那這個問題就越來越嚴重旗唁,比如內(nèi)存泄露問題。各位在開發(fā)中靈活選擇使用痹束。

3.使用block+weakSelf

NSTimer在iOS10開放了兩個API

/// Creates and returns a new NSTimer object initialized with the specified block object. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
/// - parameter:  timeInterval  The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter:  repeats  If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter:  block  The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

/// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
/// - parameter:  ti    The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter:  repeats  If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter:  block  The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
使用方式
- (void)viewDidLoad
{
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf doSomething];
    }];
}

- (void)doSomething
{
    NSLog(@"%s", __func__);
}

該方法比較簡單检疫,就不多贅述了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末祷嘶,一起剝皮案震驚了整個濱河市屎媳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌论巍,老刑警劉巖烛谊,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異嘉汰,居然都是意外死亡丹禀,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門鞋怀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來双泪,“玉大人,你說我怎么就攤上這事密似”好” “怎么了?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵残腌,是天一觀的道長村斟。 經(jīng)常有香客問我,道長废累,這世上最難降的妖魔是什么邓梅? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮邑滨,結(jié)果婚禮上日缨,老公的妹妹穿的比我還像新娘。我一直安慰自己掖看,他們只是感情好匣距,可當我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布面哥。 她就那樣靜靜地躺著,像睡著了一般毅待。 火紅的嫁衣襯著肌膚如雪尚卫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天尸红,我揣著相機與錄音吱涉,去河邊找鬼。 笑死外里,一個胖子當著我的面吹牛怎爵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播盅蝗,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼鳖链,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了墩莫?” 一聲冷哼從身側(cè)響起芙委,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎狂秦,沒想到半個月后灌侣,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡故痊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年顶瞳,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片愕秫。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡慨菱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出戴甩,到底是詐尸還是另有隱情符喝,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布甜孤,位于F島的核電站协饲,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏缴川。R本人自食惡果不足惜茉稠,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望把夸。 院中可真熱鬧而线,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至誓竿,卻和暖如春磅网,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背筷屡。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工涧偷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人速蕊。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓嫂丙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親规哲。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,515評論 2 359

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