NSTimer 循環(huán)引用的原因和解決方案

NSTimer 循環(huán)引用的原因和解決方案

造成循環(huán)引用的原因就是兩個對象之間因為強(qiáng)引用無法釋放。本文將通過NSTimer來剖析強(qiáng)引用黄锤,以及解決方法搪缨。

1. 強(qiáng)引用

舉個例子,比如我們有兩個ViewController猜扮,分別為AB勉吻,從A可以pushB,從B可以popA旅赢,B中代碼如下:


static int num = 0;

@property (nonatomic, strong) NSTimer       *timer;

- (void)viewDidLoad {
    [super viewDidLoad];

     self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
     // 加runloop
     [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

- (void)fireHome{
    num++;
    NSLog(@"hello word - %d",num);
}

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

當(dāng)我們從B界面popA時齿桃,timer并不會停,那是為什么呢煮盼?顯然是沒有執(zhí)行B界面的dealloc方法短纵,導(dǎo)致B界面沒有被釋放。

既然沒釋放肯定是有循環(huán)引用僵控,那么這個循環(huán)引用產(chǎn)生的在哪里呢香到?乍一看,我們的BViewController強(qiáng)引用了timer报破,那么如果說造成循環(huán)引用就是timer強(qiáng)引用了self悠就,但是這里面沒有block怎么產(chǎn)生的循環(huán)引用呢?這里面在初始化timer的時候有個target充易,我們查看一下這個初始化方法shift+command+0梗脾,搜索一下timerWithTimeInterval:target:selector:userInfo:repeats:關(guān)于target的描述如下:

image

可以看到timertarget保持強(qiáng)引用,直到timer失效盹靴。

所以說循環(huán)引用就產(chǎn)生了炸茧,B強(qiáng)引用著timertimer強(qiáng)引用著target也就是self稿静,在這里self就是B的實例對象梭冠。此時就是:
self -> timer -> self構(gòu)成的循環(huán)引用。

我們在iOS Objective-C Block簡介這篇文章中介紹了使用weakSelf來解決循環(huán)引用改备,既然是這樣控漠,那么我們用weakSelf是否可以解決這層循環(huán)引用呢?

將代碼修改為如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    __weak typeof(self) weakSelf = self;
     self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
     // 加runloop
     [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

運行悬钳,依舊沒有打破循環(huán)引用润脸,timerpop后依舊運行柬脸。那么這是為什么呢?毙驯,在block中我們可以使用weakSelf來打破循環(huán)引用,那么在這里為什么不行呢灾测?

此時我們使用__weak雖然打破了self -> timer -> self這個循環(huán)引用爆价,使其變成了self -> timer -> weakSelf -> self

但是這里我們分析的并不全面媳搪,因為我們的timer需要加入到Runloop铭段,Runlooptimer是一個強(qiáng)持有,Runloop的生命周期比B界面更長秦爆,所以這才是導(dǎo)致timer無法釋放的真正原因序愚,timer無法釋放,自然self也就無法釋放等限。所以這個引用鏈最初應(yīng)該是這樣的:

self -> timer -> self
runloop -> timer -> self

畫個圖:

image

加上weakSelf之后爸吮,變成了這樣:

self -> timer -> weakSelf -> self
runloop -> timer -> weakSelf -> self

image

那么雖然是這樣weakSelf也是弱引用啊,為什么不能打破循環(huán)引用呢望门?在block中我們可以通過self -> block -> weakSelf -> self打破循環(huán)引用形娇?為什么這里就不可以了呢?

這里我們就要稍微研究一下這行代碼了:
__weak typeof(self) weakSelf = self;

我們想知道weakSelfself有什么區(qū)別筹误,其實主要是這三點:

  1. weakSelf會對self的引用計數(shù)+1嗎桐早?
  2. weakSelfself的指針地址相同嗎?
  3. weakSelfself是指向同一片內(nèi)存空間嗎厨剪?

下面我們驗證一下哄酝,添加這樣一段代碼:

    NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
    __weak typeof(self) weakSelf = self;
    NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));

運行并通過lldb調(diào)試得到如下結(jié)果:

image

我們可以看到:

  • weakSelf并沒有增加self的引用計數(shù)
  • weakSelfself指向同一內(nèi)存區(qū)域
  • weakSelfself的指針地址是不同的

其實分析完這里我們也看不出什么,這里的引用關(guān)系還是這幅圖:

image

下面我們在看看block中的weakSelf祷膳,添加如下代碼:

@property (nonatomic, copy)  void(^myBlock)(void);
@property (nonatomic, copy) NSString *name;

- (void)test1 {
    __weak typeof(self) weakSelf = self;
    self.name = @"test1";
    self.myBlock = ^{
        NSLog(@"%@",weakSelf.name);
    };
    
    self.myBlock();
}

調(diào)用test1陶衅,通過lldb調(diào)試:

image

此時就很清晰了,block中的weakSelf與外面的weakSelf根本不是同一個對象钾唬,雖然他們指向的都是同一片內(nèi)存區(qū)域万哪,在這里就是<LGTimerViewController: 0x7fc275604b10>,下面我們在看看libclosure中的_Block_object_assign函數(shù)抡秆。

image

在這里我們看到都是取的對象的地址**奕巍,或者是通過_Block_copy拷貝一份,也就是說在block中都是臨時變量儒士,一份新的變量的止,所以說在block中其引用鏈并不存在對weakSelf持有,而是持有的weakSelf的指針地址着撩,也就是*weakSelf诅福,跟self沒有任何關(guān)系匾委。

然而在timer這里,timerweakSelf也就是target是強(qiáng)持有氓润,所以不能打破循環(huán)引用赂乐。

所以對于blocktimer兩個模型之間循環(huán)引用的區(qū)別如下:

timerself -> timer -> weakSelf -> self
blockself -> block -> *weakSelf

2. 解決Timer強(qiáng)引用

2.1 不使用帶target的Timer

因為timer通過target強(qiáng)持有了self,那么我們不使用含有target的API不就就可以了咖气,修改代碼為如下:

self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"hello word - %d",num);
}];

2.2 提前銷毀timer

因為timer通過target強(qiáng)持有了self挨措,當(dāng)我們需要pop的時候,提前銷毀timer就可以打破這層循環(huán)引用崩溪,所以我們可以通過didMoveToParentViewController浅役,但是無論是pop還是push都會調(diào)用該方法,所以我們加一層判斷伶唯,代碼如下:

- (void)didMoveToParentViewController:(UIViewController *)parent{
    // 無論push 進(jìn)來 還是 pop 出去 正常跑
    // 就算繼續(xù)push 到下一層 pop 回去還是繼續(xù)
    if (parent == nil) {
       [self.timer invalidate];
        self.timer = nil;
        NSLog(@"timer 走了");
    }
}

此時當(dāng)我們pop的時候就可以正常銷毀timer了觉既。

2.3 中介者模式

在這里我們關(guān)系的是fireHome能執(zhí)行,并不關(guān)心timer捕獲的target是誰乳幸,所以為了避免循環(huán)引用瞪讼,我們可以把target換成其他對象,將fireHome交給target執(zhí)行反惕。所以修改代碼為如下:

#import <objc/runtime.h>// 導(dǎo)入runtime

//* 定義一個id類型的對象屬性 */
@property (nonatomic, strong) id            target;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 初始化target
    self.target = [[NSObject alloc] init];
    // 給NSObject添加方法
    class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:");
    // 初始化timer
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(fireHome) userInfo:nil repeats:YES];
}

void fireHomeObjc(id obj){
    num++;
    NSLog(@"hello word - %d",num);
}

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

這里因為不在強(qiáng)引用self尝艘,self就可以正常dealloc,也就可以停掉timer姿染。從而解除對target的強(qiáng)引用背亥。

2.4 自定義封裝timer

上面的解決方式其實需要考慮的方面比較多,需要定義target對象悬赏,添加方法狡汉,停掉和置空timer,步驟還是蠻多的闽颇,稍不注意就可能出錯盾戴,所以我們自己封裝一個timer,作為中間層兵多,來解決調(diào)用者這些復(fù)雜的操作尖啡,來使調(diào)用顯得簡單、方便、安全。

首先我們提供兩個方法勘伺,分別是初始化方法和銷毀timer的方法,代碼如下:

@interface LGTimerWapper : NSObject

- (instancetype)lg_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- (void)lg_invalidate;

@end

然后我們提供了三個屬性畏梆,分別用于存儲targetselector以及自定義timer中的timer屬性,代碼如下:

#import <objc/message.h>

@interface LGTimerWapper()
// 定義一個target 用于存儲傳入的target 注意這里使用的是weak
@property (nonatomic, weak) id target;
// 存儲 sel
@property (nonatomic, assign) SEL aSelector;
// timer
@property (nonatomic, strong) NSTimer *timer;

@end

下面是初始化方法的實現(xiàn):

  1. 首先我們存儲了targetaSelector
  2. 然后判斷target能響應(yīng)aSelector的時候
    1. 為中介添加方法奠涌,這里面的中介就是當(dāng)前類
    2. 并把imp指向當(dāng)前類的fireHomeWapper方法
    3. 初始化timer
  3. return self
- (instancetype)lg_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
    if (self == [super init]) {
        self.target     = aTarget; // vc
        self.aSelector  = aSelector; // 方法 -- vc 釋放
        
        if ([self.target respondsToSelector:self.aSelector]) {
            // 將中介的處理添加到這里宪巨,不去外面再次添加,這里面的中介就是當(dāng)前類型
            // 通過Runtime 獲取到方法
            Method method    = class_getInstanceMethod([self.target class], aSelector);
            // 獲取方法的type
            const char *type = method_getTypeEncoding(method);
            // 為當(dāng)前類添加這個方法
            class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);

            // runloop&self -> timer -> lgtimerwarpper
            self.timer      = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
        }
    }
    return self;
}

下面我們看看fireHomeWapper方法的實現(xiàn)溜畅,這里是重點也是難點:

  1. 首先判斷target屬性是否有值捏卓,因為這個屬性是weak的,如果有值說明能響應(yīng)
    1. 這里通過objc_msgSend來調(diào)用存儲的aSelector
  2. 如果不存在慈格,說明不能響應(yīng)了天吓,停掉timer并置空就好了

關(guān)于lg_invalidate方法的實現(xiàn)就更簡單了,在本示例中沒有用到該方法峦椰,但是如果想要主動銷毀可以調(diào)用,代碼如下:

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

這樣編寫后調(diào)用的時候就非常簡單了汰规,減少了很多需要處理的地方:

#import "LGTimerWapper.h"

@property (nonatomic, strong) LGTimerWapper *timerWapper;
//* 定義一個id類型的對象屬性 */

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.timerWapper = [[LGTimerWapper alloc] lg_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
}

2.5 使用NSProxy虛基類的子類

上面的代碼雖然使用起來比較簡單汤功,但是代碼寫起來少多了些,有時候也存在維護(hù)問題溜哮,對于調(diào)用者沒有真正的去調(diào)用invalidate和置空timer滔金,總是有些別扭的,其實解決timer循環(huán)引用的最好的方式還是使用NSProxy茂嗓。下面我們來看看怎么實現(xiàn):

首先我們定義一個NSProxy的子類餐茵,這個類里面通過一個weak屬性,持有著target中需要強(qiáng)引用的實例對象述吸。代碼如下:

#import "LGProxy.h"

@interface LGProxy()
@property (nonatomic, weak) id object;
@end

@implementation LGProxy
+ (instancetype)proxyWithTransformObject:(id)object{
    LGProxy *proxy = [LGProxy alloc];
    proxy.object = object;
    return proxy;
}

但是僅僅是這樣還是不行的忿族,還需要讓實際的target響應(yīng)消息,畢竟LGProxy不能真正響應(yīng)timer中的消息蝌矛。

/*
    僅僅添加了weak類型的屬性還不夠道批,為了保證中間件能夠響應(yīng)外部self的事件
    需要通過消息轉(zhuǎn)發(fā)機(jī)制,讓實際的響應(yīng)target還是外部self入撒,
    這一步至關(guān)重要隆豹,主要涉及到runtime的消息機(jī)制。
*/
-(id)forwardingTargetForSelector:(SEL)aSelector {
    return self.object;
}

下面我們看看怎么使用:

#import "LGProxy.h"

@property (nonatomic, strong) LGProxy       *proxy;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.proxy = [LGProxy proxyWithTransformObject:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
}

- (void)fireHome{
    num++;
    NSLog(@"hello word - %d",num);
}

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

此時使用起來還是直接使用NSTimer茅逮,只是對target的強(qiáng)引用的修改成了Proxy璃赡。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市献雅,隨后出現(xiàn)的幾起案子碉考,更是在濱河造成了極大的恐慌,老刑警劉巖惩琉,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件豆励,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)良蒸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門技扼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人嫩痰,你說我怎么就攤上這事剿吻。” “怎么了串纺?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵丽旅,是天一觀的道長。 經(jīng)常有香客問我纺棺,道長榄笙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任祷蝌,我火速辦了婚禮茅撞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘巨朦。我一直安慰自己米丘,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布糊啡。 她就那樣靜靜地躺著拄查,像睡著了一般。 火紅的嫁衣襯著肌膚如雪棚蓄。 梳的紋絲不亂的頭發(fā)上堕扶,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機(jī)與錄音癣疟,去河邊找鬼挣柬。 笑死,一個胖子當(dāng)著我的面吹牛睛挚,可吹牛的內(nèi)容都是我干的邪蛔。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼扎狱,長吁一口氣:“原來是場噩夢啊……” “哼侧到!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起淤击,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤匠抗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后污抬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體汞贸,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡绳军,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了矢腻。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片门驾。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖多柑,靈堂內(nèi)的尸體忽然破棺而出奶是,到底是詐尸還是另有隱情,我是刑警寧澤竣灌,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布聂沙,位于F島的核電站,受9級特大地震影響初嘹,放射性物質(zhì)發(fā)生泄漏及汉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一屯烦、第九天 我趴在偏房一處隱蔽的房頂上張望豁生。 院中可真熱鬧,春花似錦漫贞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至豪嗽,卻和暖如春谴蔑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背龟梦。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工隐锭, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人计贰。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓钦睡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親躁倒。 傳聞我的和親對象是個殘疾皇子荞怒,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

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