什么是循環(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]

通過(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>

通過(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,如下:

例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)引用。

同樣類(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的講解

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

如上圖所示,通過(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);
}
}

通過(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,如下圖所示:

通過(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中,如下:
看圖中Important部分,
session對(duì)傳入的delegate對(duì)象保持一個(gè)強(qiáng)引用直到app退出,或調(diào)用invalidateAndCancel或finishTasksAndInvalidate方法使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這行代碼仍被注釋掉):

所以,上面將
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ì)象之間的引用情況如下:

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)存泄漏的情況。