什么是循環(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的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>
通過運行結果可以看到,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帝牡,如下:
例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)引用响牛。
同樣類似的還有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的講解
例5:AFN的block豺憔,AFN的block比較特殊,讓我們慢慢道來够庙,首先看一下使用及結果恭应。
如上圖所示耘眨,通過實例驗證了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如何打破的VC和AFN之間的循環(huán)引用唇兑。即AFN在調用完block之后酒朵,取消了對block的強引用,切斷了這個環(huán)扎附。
ps:可以猜想蔫耽,例4中雖然VC持有NSURLSession對象,但并不會造成循環(huán)引用留夜,可能也是通過這種方式來解決的匙铡。
乍看之下,這個問題得到了很好的解決碍粥,貌似已經(jīng)沒有任何問題鳖眼。我們稍微改動下代碼,讓VC不持有AFN嚼摩,并且注釋AFN刪除delegate這行代碼钦讳,即讓AFN單向持有VC,如下圖所示:
通過上圖運行結果可見枕面,VC沒有對AFN的強引用愿卒,但VC并沒有得到釋放。這是為什么呢潮秘?難道我們上面的分析有誤琼开?下面我們從AFN的創(chuàng)建來分析一下:
AFN創(chuàng)建對象時,對于
NSURLSession
的創(chuàng)建枕荞,使用了sessionWithConfiguration: delegate: delegateQueue:
方法柜候,并將AFURLSessionManager
對象賦值到delegate中,如下:看圖中Important部分躏精,
session
對傳入的delegate
對象保持一個強引用直到app退出渣刷,或調用invalidateAndCancel
或finishTasksAndInvalidate
方法使session
失效。否則就會造成內存泄漏玉控。即AFN和session之間是存在循環(huán)引用的飞主。所以狮惜,當創(chuàng)建一個臨時的AFN對象發(fā)起請求時高诺,發(fā)起方(假設為VC)和AFN之間的引用關系為(此時AFN刪除delegate這行代碼仍被注釋掉):
所以,上面將
removeDelegateForTask:
注釋掉之后碾篡,是由于AFN對象得不到釋放虱而,導致AFN對block還保持有強引用,block又對VC有強引用开泽,才會導致VC釋放不掉牡拇。
*************下面恢復注釋掉的代碼,使AFN為標準未改動過的代碼*************
如果在VC中持有AFN的對象,像本例剛開始一樣惠呼,那么對象之間的引用情況如下:
AFN在調用block回調之后,清除了AFN對block的引用剔蹋,打破了VC和AFN之間的循環(huán)引用旅薄。使VC可以正常釋放。但需要注意的是泣崩,AFN對象并沒有得到釋放少梁,內存泄漏依然是存在的!矫付!
在實際開發(fā)中凯沪,我們通常不會在VC中持有AFN對象,而是會將AFN封裝买优,所以妨马,AFN對象的創(chuàng)建可能是單例或有限個,但依然需要關注內存泄漏的情況而叼。