OC 循环引用(Retain Cycle)

0.408字数 1142阅读 877

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

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

@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
}

运行结果:

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。
方法一:

- (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已经执行
}

运行结果:

2017-09-04 23:16:46.918 demo1[22614:63060544] -[B dealloc]
2017-09-04 23:17:19.716 demo1[22614:63060544] b的dealloc 应该执行了吧
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的引用计数
}

运行结果:

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回调

//自定义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。代码如下:

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

@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

运行结果:

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系列,如下面也不会产生循环引用:

    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的内部处理逻辑如下:

//单步跟踪会发现在真正调用系统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对象的创建可能是单例或有限个,但依然需要关注内存泄漏的情况。

推荐阅读更多精彩内容