OC 循環(huán)引用(Retain Cycle)

什么是循環(huán)引用?就是兩個或多個對象之間,都是強引用愉老,且對象之間的引用形成了一個環(huán)狀結構。導致對象最終無法釋放铆铆,造成內存泄露。

為什么循環(huán)引用就會導致對象無法釋放呢丹喻?先看一個小例子:

@interface A : NSObject
@property (nonatomic, strong) B *b;
@end

@interface B : NSObject
@property (nonatomic, strong) A *a;
@end

@implementation A
- (instancetype) init {
    NSLog(@"%s", __FUNCTION__);
    return [super init];
}
- (void)dealloc {
    NSLog(@"%s", __FUNCTION__);
}
@end

@implementation B
- (instancetype) init {
    NSLog(@"%s", __FUNCTION__);
    return [super init];
}
- (void)dealloc {
    NSLog(@"%s", __FUNCTION__);
}
@end

//使用A薄货、B對象造成循環(huán)引用
- (void)viewDidLoad {
    [super viewDidLoad];
    
    A *a = [[A alloc] init];  //創(chuàng)建對象a,a的引用計數(shù)為1
    a.b = [[B alloc] init];  //對象a強引用對象b碍论,b的引用計數(shù)為1
    a.b.a = a;  //對象b強引用對象a谅猾,a的引用計數(shù)為2
}

運行結果:

2017-09-04 16:06:11.326 RetainCycleDemo[25202:24312629] -[A init]
2017-09-04 16:06:11.326 RetainCycleDemo[25202:24312629] -[B init]
對象a和對象b循環(huán)引用

通過運行結果可以看到,對象a和對象b的dealloc都沒有調用,說明a税娜、b都沒有釋放坐搔。參見上圖,表示了a敬矩、b的引用情況概行。代碼中的注釋表示了a、b的引用計數(shù)情況弧岳,當a離開作用域時凳忙,a的引用計數(shù)減1,但此時禽炬,a的引用計數(shù)并沒有變?yōu)?涧卵,所以并不會釋放

這個問題該怎樣解決腹尖?

這個問題的關鍵在于讓a離開作用域時柳恐,a的引用計數(shù)為1。
方法一:

- (void)viewDidLoad {
    [super viewDidLoad];

    A *a = [[A alloc] init];
    a.b = [[B alloc] init];
    a.b.a = a;
    
    a.b = nil;  //在a離開作用域前热幔,將b置為nil乐设,此時b會釋放,同時會將a的引用計數(shù)減1

    NSLog(@"b的dealloc 應該執(zhí)行了吧");  //在此加斷點绎巨,會發(fā)現(xiàn)b的dealloc已經(jīng)執(zhí)行
}

運行結果:

2017-09-04 23:16:46.918 demo1[22614:63060544] -[B dealloc]
2017-09-04 23:17:19.716 demo1[22614:63060544] b的dealloc 應該執(zhí)行了吧
2017-09-04 23:17:19.716 demo1[22614:63060544] -[A dealloc]

方法二:

@interface B : NSObject
@property (nonatomic, weak) A *a;  //將屬性設為weak近尚,弱引用對象a
@end

- (void)viewDidLoad {
    [super viewDidLoad];

    A *a = [[A alloc] init];
    a.b = [[B alloc] init];
    a.b.a = a;    //因為b對a是弱引用,所以不會增加a的引用計數(shù)
}

運行結果:

2017-09-04 23:21:52.524 demo1[23489:63076174] -[A dealloc]
2017-09-04 23:21:52.524 demo1[23489:63076174] -[B dealloc]

block循環(huán)引用

循環(huán)引用通常是block導致的认烁,如下面的例子:
例1:TableViewCell的block回調

//自定義cell,cell中有個按鈕介汹,當點擊按鈕時却嗡,通過block通知VC
//MyCell.h
@interface MyCell : UITableViewCell
@property (nonatomic, copy) void(^cellBtnClickBlock)();
@end
//MyCell.m
@implementation MyCell
- (IBAction)cellBtnClick:(id)sender {
    self.cellBtnClickBlock();
}
@end

//ViewController.m
//點擊cell的button,然后通過導航欄返回到上層控制器嘹承,看dealloc是否被調用
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MyCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([MyCell class]) forIndexPath:indexPath];
    
    cell.cellBtnClickBlock = ^{
        NSLog(@"%s, %@", __FUNCTION__, self);
    };
    
    return cell;
}
- (void)dealloc {
    NSLog(@"%s", __FUNCTION__);
}

運行結果:2017-09-06 09:31:28.365 RetainCycleDemo[28479:29994003] -[ViewController tableView:cellForRowAtIndexPath:]_block_invoke, <ViewController: 0x7fbd4ac29ca0>

tableView的循環(huán)引用

通過運行結果可以看到,dealloc并沒有被調用窗价,說明發(fā)生了循環(huán)引用。上圖中表示了對象之間的引用情況叹卷。要打破這個循環(huán)撼港,則需要在cell里不強引用self。代碼如下:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MyCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([MyCell class]) forIndexPath:indexPath];
    
    __weak typeof(self) weakSelf = self;
    cell.cellBtnClickBlock = ^{
        NSLog(@"%s, %@", __FUNCTION__, weakSelf);
    };
    
    return cell;
}

運行工程骤竹,結果OK帝牡,如下:

tableViewCircle.gif

例2:NSNotification 的循環(huán)引用

@implementation SecondViewController
- (void)addObserver {
     [[NSNotificationCenter defaultCenter] addObserverForName:@"noticycle" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        NSLog(@"%s, %@", __FUNCTION__, self);
    }];
}
- (void)postNotification {
    [[NSNotificationCenter defaultCenter] postNotificationName:@"noticycle" object:nil];
    NSLog(@"%s, %@", __FUNCTION__, self);
}
@end

@implementation FirstViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //創(chuàng)建兩個SecondViewController對象,VC1做觀察者蒙揣,VC2做通知發(fā)送者
    SecondViewController *VC1 = [[SecondViewController alloc] init];
    VC1.title = @"VC1";
    [VC1 addObserver];
    
    SecondViewController *VC2 = [[SecondViewController alloc] init];
    VC2.title = @"VC2";
    [VC2 postNotification];    
}
@end

運行結果:

2017-09-06 12:31:17.710 RetainCycleDemo[58071:30501179] -[SecondViewController addObserver]_block_invoke, VC1
2017-09-06 12:31:17.710 RetainCycleDemo[58071:30501179] -[SecondViewController postNotification], VC2
2017-09-06 12:31:17.712 RetainCycleDemo[58071:30501179] -[SecondViewController dealloc], VC2

從運行結果可以看到,VC1并沒有得到釋放靶溜。解決方式,同樣是在addObserverForName的block中使用weakSelf方式,解決循環(huán)引用的問題罩息。

But:這里我也沒弄懂的是嗤详,addObserverForName方法中不需要傳入self,是怎樣持有的self呢瓷炮?還請大家指點葱色。
補充:這個問題我專門寫了一篇文章:NSNotification引起的內存泄漏和循環(huán)引用,歡迎大家一起探討


使用block的地方有很多娘香,但并不是所以block都會產(chǎn)生循環(huán)引用苍狰,如以下情況:

例3:使用系統(tǒng)自帶的UIView 的block,如下圖所示茅主,雖然在animation的block中打印了self舞痰,但由于是類方法,self并沒有對block有強引用诀姚,所以不會形成循環(huán)引用响牛。

UIViewAnimation的block不會造成循環(huán)引用

同樣類似的還有GCD系列,如下面也不會產(chǎn)生循環(huán)引用:

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"dispatch_async:%@", self);
    });

例4:NSURLSession的block赫段,我們可以直接適用sharedSession來進行HTTP請求呀打,或自己創(chuàng)建session,但不管是不是自己持有的session糯笙,都不會造成循環(huán)引用
注意:session使用的是sharedSession贬丛,而不是通過sessionWithConfiguration: delegate: delegateQueue:創(chuàng)建的,其中區(qū)別给涕,請看例5中AFN的講解

self持有session但不會造成循環(huán)引用

例5:AFN的block豺憔,AFN的block比較特殊,讓我們慢慢道來够庙,首先看一下使用及結果恭应。

AFN的使用,驗證可見block中使用self不會造成循環(huán)引用

如上圖所示耘眨,通過實例驗證了AFN確實不會引起循環(huán)引用昼榛,VC得到了正常的釋放,AFN的內部處理邏輯如下:

//單步跟蹤會發(fā)現(xiàn)在真正調用系統(tǒng)NSURLSession的dataTaskWithRequest之后剔难,調用了下面方法
- (void)addDelegateForDataTask:(NSURLSessionDataTask *)dataTask
                uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock
              downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock
             completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler
{
  //創(chuàng)建一個代理類胆屿,用來保存?zhèn)魅氲腷lock
    AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] initWithTask:dataTask];
    delegate.manager = self;
    delegate.completionHandler = completionHandler;//completionHandler被強引用

    dataTask.taskDescription = self.taskDescriptionForSessionTasks;
    [self setDelegate:delegate forTask:dataTask];//將代理類保存起來,見下面代碼實現(xiàn)

    delegate.uploadProgressBlock = uploadProgressBlock;
    delegate.downloadProgressBlock = downloadProgressBlock;//download被強引用
}

- (void)setDelegate:(AFURLSessionManagerTaskDelegate *)delegate
            forTask:(NSURLSessionTask *)task
{
    NSParameterAssert(task);
    NSParameterAssert(delegate);

    [self.lock lock];
    //以task.taskIdentifier為key偶宫,將代理類保存起來非迹,即將上面的block強引用
    self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
    [self addNotificationObserverForTask:task];
    [self.lock unlock];
}

#pragma mark - NSURLSessionTaskDelegate
//任務完成會NSURLSession會調用該代理函數(shù)
- (void)URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
    //根據(jù)task.taskIdentifier找到對應的代理類
    AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:task];

    // delegate may be nil when completing a task in the background
    if (delegate) {
        //代理類將本次http請求的結果,通過保存的block返回給調用者
        [delegate URLSession:session task:task didCompleteWithError:error];

      //移除對block的強引用纯趋,由于在block執(zhí)行完之后已經(jīng)移除了自身對block的引用彻秆,所以便打破了這個循環(huán)引用
        [self removeDelegateForTask:task];  //如果將這行注釋掉,會發(fā)現(xiàn)VC不會釋放
    }

    if (self.taskDidComplete) {
        self.taskDidComplete(session, task, error);
    }
}
AFN如何打破的AFN和VC之間的循環(huán)引用

通過上面的代碼和圖示,清楚的表示了AFN如何打破的VC和AFN之間的循環(huán)引用唇兑。即AFN在調用完block之后酒朵,取消了對block的強引用,切斷了這個環(huán)扎附。


ps:可以猜想蔫耽,例4中雖然VC持有NSURLSession對象,但并不會造成循環(huán)引用留夜,可能也是通過這種方式來解決的匙铡。


乍看之下,這個問題得到了很好的解決碍粥,貌似已經(jīng)沒有任何問題鳖眼。我們稍微改動下代碼,讓VC不持有AFN嚼摩,并且注釋AFN刪除delegate這行代碼钦讳,即讓AFN單向持有VC,如下圖所示:

AFN單向持有VC

通過上圖運行結果可見枕面,VC沒有對AFN的強引用愿卒,但VC并沒有得到釋放。這是為什么呢潮秘?難道我們上面的分析有誤琼开?下面我們從AFN的創(chuàng)建來分析一下:
AFN創(chuàng)建對象時,對于NSURLSession的創(chuàng)建枕荞,使用了sessionWithConfiguration: delegate: delegateQueue:方法柜候,并將AFURLSessionManager對象賦值到delegate中,如下:
AFN Init

看圖中Important部分躏精,session對傳入的delegate對象保持一個強引用直到app退出渣刷,或調用invalidateAndCancelfinishTasksAndInvalidate方法使session失效。否則就會造成內存泄漏玉控。
即AFN和session之間是存在循環(huán)引用的飞主。所以狮惜,當創(chuàng)建一個臨時的AFN對象發(fā)起請求時高诺,發(fā)起方(假設為VC)和AFN之間的引用關系為(此時AFN刪除delegate這行代碼仍被注釋掉):
VC-AFN-Session

所以,上面將removeDelegateForTask:注釋掉之后碾篡,是由于AFN對象得不到釋放虱而,導致AFN對block還保持有強引用,block又對VC有強引用开泽,才會導致VC釋放不掉牡拇。


*************下面恢復注釋掉的代碼,使AFN為標準未改動過的代碼*************


如果在VC中持有AFN的對象,像本例剛開始一樣惠呼,那么對象之間的引用情況如下:


VC強引用AFN导俘,但并不會導致VC釋放不了

AFN在調用block回調之后,清除了AFN對block的引用剔蹋,打破了VC和AFN之間的循環(huán)引用旅薄。使VC可以正常釋放。但需要注意的是泣崩,AFN對象并沒有得到釋放少梁,內存泄漏依然是存在的!矫付!
在實際開發(fā)中凯沪,我們通常不會在VC中持有AFN對象,而是會將AFN封裝买优,所以妨马,AFN對象的創(chuàng)建可能是單例或有限個,但依然需要關注內存泄漏的情況而叼。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末身笤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子葵陵,更是在濱河造成了極大的恐慌液荸,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脱篙,死亡現(xiàn)場離奇詭異娇钱,居然都是意外死亡,警方通過查閱死者的電腦和手機绊困,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門文搂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人秤朗,你說我怎么就攤上這事煤蹭。” “怎么了取视?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵硝皂,是天一觀的道長。 經(jīng)常有香客問我作谭,道長稽物,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任折欠,我火速辦了婚禮贝或,結果婚禮上吼过,老公的妹妹穿的比我還像新娘。我一直安慰自己咪奖,他們只是感情好盗忱,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著羊赵,像睡著了一般售淡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上慷垮,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天揖闸,我揣著相機與錄音,去河邊找鬼料身。 笑死汤纸,一個胖子當著我的面吹牛,可吹牛的內容都是我干的芹血。 我是一名探鬼主播贮泞,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼幔烛!你這毒婦竟也來了啃擦?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤饿悬,失蹤者是張志新(化名)和其女友劉穎令蛉,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體狡恬,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡珠叔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了弟劲。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片祷安。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖兔乞,靈堂內的尸體忽然破棺而出汇鞭,到底是詐尸還是另有隱情,我是刑警寧澤庸追,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布霍骄,位于F島的核電站,受9級特大地震影響锚国,放射性物質發(fā)生泄漏腕巡。R本人自食惡果不足惜玄坦,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一血筑、第九天 我趴在偏房一處隱蔽的房頂上張望绘沉。 院中可真熱鬧,春花似錦豺总、人聲如沸车伞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽另玖。三九已至,卻和暖如春表伦,著一層夾襖步出監(jiān)牢的瞬間谦去,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工蹦哼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鳄哭,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓纲熏,卻偏偏與公主長得像妆丘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子局劲,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內容