OC 循环引用(Retain Cycle)

什么是循环引用?就是两个或多个对象之间,都是强引用,且对象之间的引用形成了一个环状结构。导致对象最终无法释放,造成内存泄露。

为什么循环引用就会导致对象无法释放呢?先看一个小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@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对象造成循环引用
- (void)viewDidLoad {
[super viewDidLoad];

A *a = [[A alloc] init]; //创建对象a,a的引用计数为1
a.b = [[B alloc] init]; //对象a强引用对象b,b的引用计数为1
a.b.a = a; //对象b强引用对象a,a的引用计数为2
}

运行结果:

1
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循环引用

通过运行结果可以看到,对象a和对象b的dealloc都没有调用,说明a、b都没有释放。参见上图,表示了a、b的引用情况。代码中的注释表示了a、b的引用计数情况,当a离开作用域时,a的引用计数减1,但此时,a的引用计数并没有变为0,所以并不会释放

这个问题该怎样解决?

这个问题的关键在于让a离开作用域时,a的引用计数为1。
方法一:

1
2
3
4
5
6
7
8
9
10
11
- (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的引用计数减1

NSLog(@"b的dealloc 应该执行了吧"); //在此加断点,会发现b的dealloc已经执行
}

运行结果:

1
2
3
2017-09-04 23:16:46.918 demo1[22614:63060544] -[B dealloc]
2017-09-04 23:17:19.716 demo1[22614:63060544] bdealloc 应该执行了吧
2017-09-04 23:17:19.716 demo1[22614:63060544] -[A dealloc]

方法二:

1
2
3
4
5
6
7
8
9
10
11
@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的引用计数
}

运行结果:

1
2
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循环引用

循环引用通常是block导致的,如下面的例子:
例1:TableViewCell的block回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//自定义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的循环引用

通过运行结果可以看到,dealloc并没有被调用,说明发生了循环引用。上图中表示了对象之间的引用情况。要打破这个循环,则需要在cell里不强引用self。代码如下:

1
2
3
4
5
6
7
8
9
10
- (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 的循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@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.
//创建两个SecondViewController对象,VC1做观察者,VC2做通知发送者
SecondViewController *VC1 = [[SecondViewController alloc] init];
VC1.title = @"VC1";
[VC1 addObserver];

SecondViewController *VC2 = [[SecondViewController alloc] init];
VC2.title = @"VC2";
[VC2 postNotification];
}
@end

运行结果:

1
2
3
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方式,解决循环引用的问题。

But:这里我也没弄懂的是,addObserverForName方法中不需要传入self,是怎样持有的self呢?还请大家指点。
补充:这个问题我专门写了一篇文章:NSNotification引起的内存泄漏和循环引用,欢迎大家一起探讨


使用block的地方有很多,但并不是所以block都会产生循环引用,如以下情况:

例3:使用系统自带的UIView 的block,如下图所示,虽然在animation的block中打印了self,但由于是类方法,self并没有对block有强引用,所以不会形成循环引用。

UIViewAnimation的block不会造成循环引用

同样类似的还有GCD系列,如下面也不会产生循环引用:

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

例4:NSURLSession的block,我们可以直接适用sharedSession来进行HTTP请求,或自己创建session,但不管是不是自己持有的session,都不会造成循环引用
注意:session使用的是sharedSession,而不是通过sessionWithConfiguration: delegate: delegateQueue:创建的,其中区别,请看例5中AFN的讲解

self持有session但不会造成循环引用

例5:AFN的block,AFN的block比较特殊,让我们慢慢道来,首先看一下使用及结果。

AFN的使用,验证可见block中使用self不会造成循环引用

如上图所示,通过实例验证了AFN确实不会引起循环引用,VC得到了正常的释放,AFN的内部处理逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//单步跟踪会发现在真正调用系统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
{
//创建一个代理类,用来保存传入的block
AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] initWithTask:dataTask];
delegate.manager = self;
delegate.completionHandler = completionHandler;//completionHandler被强引用

dataTask.taskDescription = self.taskDescriptionForSessionTasks;
[self setDelegate:delegate forTask:dataTask];//将代理类保存起来,见下面代码实现

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会调用该代理函数
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
//根据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执行完之后已经移除了自身对block的引用,所以便打破了这个循环引用
[self removeDelegateForTask:task]; //如果将这行注释掉,会发现VC不会释放
}

if (self.taskDidComplete) {
self.taskDidComplete(session, task, error);
}
}

AFN如何打破的AFN和VC之间的循环引用

通过上面的代码和图示,清楚的表示了AFN如何打破的VC和AFN之间的循环引用。即AFN在调用完block之后,取消了对block的强引用,切断了这个环。


ps:可以猜想,例4中虽然VC持有NSURLSession对象,但并不会造成循环引用,可能也是通过这种方式来解决的。


乍看之下,这个问题得到了很好的解决,貌似已经没有任何问题。我们稍微改动下代码,让VC不持有AFN,并且注释AFN删除delegate这行代码,即让AFN单向持有VC,如下图所示:

AFN单向持有VC

通过上图运行结果可见,VC没有对AFN的强引用,但VC并没有得到释放。这是为什么呢?难道我们上面的分析有误?下面我们从AFN的创建来分析一下:

AFN创建对象时,对于NSURLSession的创建,使用了sessionWithConfiguration: delegate: delegateQueue:方法,并将AFURLSessionManager对象赋值到delegate中,如下:

AFN Init

看图中Important部分,session对传入的delegate对象保持一个强引用直到app退出,或调用invalidateAndCancelfinishTasksAndInvalidate方法使session失效。否则就会造成内存泄漏。

即AFN和session之间是存在循环引用的。所以,当创建一个临时的AFN对象发起请求时,发起方(假设为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之间的循环引用。使VC可以正常释放。但需要注意的是,AFN对象并没有得到释放,内存泄漏依然是存在的!!
在实际开发中,我们通常不会在VC中持有AFN对象,而是会将AFN封装,所以,AFN对象的创建可能是单例或有限个,但依然需要关注内存泄漏的情况。

原文链接:http://www.jianshu.com/p/c1cee4891c14

Bagikan Komentar