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

什么是循環(huán)引用?就是兩個(gè)或多個(gè)對(duì)象之間,都是強(qiáng)引用,且對(duì)象之間的引用形成了一個(gè)環(huán)狀結(jié)構(gòu)。導(dǎo)致對(duì)象最終無(wú)法釋放,造成內(nèi)存泄露。

為什么循環(huán)引用就會(huì)導(dǎo)致對(duì)象無(wú)法釋放呢?先看一個(gè)小例子:

@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對(duì)象造成循環(huán)引用
- (void)viewDidLoad {
    [super viewDidLoad];
    
    A *a = [[A alloc] init];  //創(chuàng)建對(duì)象a,a的引用計(jì)數(shù)為1
    a.b = [[B alloc] init];  //對(duì)象a強(qiáng)引用對(duì)象b,b的引用計(jì)數(shù)為1
    a.b.a = a;  //對(duì)象b強(qiáng)引用對(duì)象a,a的引用計(jì)數(shù)為2
}

運(yùn)行結(jié)果:

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

通過(guò)運(yùn)行結(jié)果可以看到,對(duì)象a和對(duì)象b的dealloc都沒(méi)有調(diào)用,說(shuō)明a、b都沒(méi)有釋放。參見(jiàn)上圖,表示了a、b的引用情況。代碼中的注釋表示了a、b的引用計(jì)數(shù)情況,當(dāng)a離開(kāi)作用域時(shí),a的引用計(jì)數(shù)減1,但此時(shí),a的引用計(jì)數(shù)并沒(méi)有變?yōu)?,所以并不會(huì)釋放。

這個(gè)問(wèn)題該怎樣解決?

這個(gè)問(wèn)題的關(guān)鍵在于讓a離開(kāi)作用域時(shí),a的引用計(jì)數(shù)為1。
方法一:

- (void)viewDidLoad {
    [super viewDidLoad];

    A *a = [[A alloc] init];
    a.b = [[B alloc] init];
    a.b.a = a;
    
    a.b = nil;  //在a離開(kāi)作用域前,將b置為nil,此時(shí)b會(huì)釋放,同時(shí)會(huì)將a的引用計(jì)數(shù)減1

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

運(yùn)行結(jié)果:

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

方法二:

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

- (void)viewDidLoad {
    [super viewDidLoad];

    A *a = [[A alloc] init];
    a.b = [[B alloc] init];
    a.b.a = a;    //因?yàn)閎對(duì)a是弱引用,所以不會(huì)增加a的引用計(jì)數(shù)
}

運(yùn)行結(jié)果:

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導(dǎo)致的,如下面的例子:
例1:TableViewCell的block回調(diào)

//自定義cell,cell中有個(gè)按鈕,當(dāng)點(diǎn)擊按鈕時(shí),通過(guò)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
//點(diǎn)擊cell的button,然后通過(guò)導(dǎo)航欄返回到上層控制器,看dealloc是否被調(diào)用
- (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__);
}

運(yùn)行結(jié)果:2017-09-06 09:31:28.365 RetainCycleDemo[28479:29994003] -[ViewController tableView:cellForRowAtIndexPath:]_block_invoke, <ViewController: 0x7fbd4ac29ca0>

tableView的循環(huán)引用

通過(guò)運(yùn)行結(jié)果可以看到,dealloc并沒(méi)有被調(diào)用,說(shuō)明發(fā)生了循環(huán)引用。上圖中表示了對(duì)象之間的引用情況。要打破這個(gè)循環(huán),則需要在cell里不強(qiáng)引用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;
}

運(yùn)行工程,結(jié)果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)建兩個(gè)SecondViewController對(duì)象,VC1做觀察者,VC2做通知發(fā)送者
    SecondViewController *VC1 = [[SecondViewController alloc] init];
    VC1.title = @"VC1";
    [VC1 addObserver];
    
    SecondViewController *VC2 = [[SecondViewController alloc] init];
    VC2.title = @"VC2";
    [VC2 postNotification];    
}
@end

運(yùn)行結(jié)果:

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

從運(yùn)行結(jié)果可以看到,VC1并沒(méi)有得到釋放。解決方式,同樣是在addObserverForName的block中使用weakSelf方式,解決循環(huán)引用的問(wèn)題。

But:這里我也沒(méi)弄懂的是,addObserverForName方法中不需要傳入self,是怎樣持有的self呢?還請(qǐng)大家指點(diǎn)。
補(bǔ)充:這個(gè)問(wèn)題我專(zhuān)門(mén)寫(xiě)了一篇文章:NSNotification引起的內(nèi)存泄漏和循環(huán)引用,歡迎大家一起探討


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

例3:使用系統(tǒng)自帶的UIView 的block,如下圖所示,雖然在animation的block中打印了self,但由于是類(lèi)方法,self并沒(méi)有對(duì)block有強(qiáng)引用,所以不會(huì)形成循環(huán)引用。

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

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

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

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

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

例5:AFN的block,AFN的block比較特殊,讓我們慢慢道來(lái),首先看一下使用及結(jié)果。

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

如上圖所示,通過(guò)實(shí)例驗(yàn)證了AFN確實(shí)不會(huì)引起循環(huán)引用,VC得到了正常的釋放,AFN的內(nèi)部處理邏輯如下:

//單步跟蹤會(huì)發(fā)現(xiàn)在真正調(diào)用系統(tǒng)NSURLSession的dataTaskWithRequest之后,調(diào)用了下面方法
- (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)建一個(gè)代理類(lèi),用來(lái)保存?zhèn)魅氲腷lock
    AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] initWithTask:dataTask];
    delegate.manager = self;
    delegate.completionHandler = completionHandler;//completionHandler被強(qiáng)引用

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

    delegate.uploadProgressBlock = uploadProgressBlock;
    delegate.downloadProgressBlock = downloadProgressBlock;//download被強(qiáng)引用
}

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

    [self.lock lock];
    //以task.taskIdentifier為key,將代理類(lèi)保存起來(lái),即將上面的block強(qiáng)引用
    self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
    [self addNotificationObserverForTask:task];
    [self.lock unlock];
}

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

    // delegate may be nil when completing a task in the background
    if (delegate) {
        //代理類(lèi)將本次http請(qǐng)求的結(jié)果,通過(guò)保存的block返回給調(diào)用者
        [delegate URLSession:session task:task didCompleteWithError:error];

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

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

通過(guò)上面的代碼和圖示,清楚的表示了AFN如何打破的VC和AFN之間的循環(huán)引用。即AFN在調(diào)用完block之后,取消了對(duì)block的強(qiáng)引用,切斷了這個(gè)環(huán)。


ps:可以猜想,例4中雖然VC持有NSURLSession對(duì)象,但并不會(huì)造成循環(huán)引用,可能也是通過(guò)這種方式來(lái)解決的。


乍看之下,這個(gè)問(wèn)題得到了很好的解決,貌似已經(jīng)沒(méi)有任何問(wèn)題。我們稍微改動(dòng)下代碼,讓VC不持有AFN,并且注釋AFN刪除delegate這行代碼,即讓AFN單向持有VC,如下圖所示:

AFN單向持有VC

通過(guò)上圖運(yùn)行結(jié)果可見(jiàn),VC沒(méi)有對(duì)AFN的強(qiáng)引用,但VC并沒(méi)有得到釋放。這是為什么呢?難道我們上面的分析有誤?下面我們從AFN的創(chuàng)建來(lái)分析一下:
AFN創(chuàng)建對(duì)象時(shí),對(duì)于NSURLSession的創(chuàng)建,使用了sessionWithConfiguration: delegate: delegateQueue:方法,并將AFURLSessionManager對(duì)象賦值到delegate中,如下:
AFN Init

看圖中Important部分,session對(duì)傳入的delegate對(duì)象保持一個(gè)強(qiáng)引用直到app退出,或調(diào)用invalidateAndCancelfinishTasksAndInvalidate方法使session失效。否則就會(huì)造成內(nèi)存泄漏。
即AFN和session之間是存在循環(huán)引用的。所以,當(dāng)創(chuàng)建一個(gè)臨時(shí)的AFN對(duì)象發(fā)起請(qǐng)求時(shí),發(fā)起方(假設(shè)為VC)和AFN之間的引用關(guān)系為(此時(shí)AFN刪除delegate這行代碼仍被注釋掉):
VC-AFN-Session

所以,上面將removeDelegateForTask:注釋掉之后,是由于AFN對(duì)象得不到釋放,導(dǎo)致AFN對(duì)block還保持有強(qiáng)引用,block又對(duì)VC有強(qiáng)引用,才會(huì)導(dǎo)致VC釋放不掉。


*************下面恢復(fù)注釋掉的代碼,使AFN為標(biāo)準(zhǔn)未改動(dòng)過(guò)的代碼*************


如果在VC中持有AFN的對(duì)象,像本例剛開(kāi)始一樣,那么對(duì)象之間的引用情況如下:


VC強(qiáng)引用AFN,但并不會(huì)導(dǎo)致VC釋放不了

AFN在調(diào)用block回調(diào)之后,清除了AFN對(duì)block的引用,打破了VC和AFN之間的循環(huán)引用。使VC可以正常釋放。但需要注意的是,AFN對(duì)象并沒(méi)有得到釋放,內(nèi)存泄漏依然是存在的??!
在實(shí)際開(kāi)發(fā)中,我們通常不會(huì)在VC中持有AFN對(duì)象,而是會(huì)將AFN封裝,所以,AFN對(duì)象的創(chuàng)建可能是單例或有限個(gè),但依然需要關(guān)注內(nèi)存泄漏的情況。

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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