Block循环引用详解

作者: 季末微夏 | 来源:发表于2016-12-23 16:21 被阅读965次

    前言

    在项目中经常用到block,使用不当就很容易因为循环引用而造成内存泄漏。本文分析了block循环引用形成原因以及处理办法,如果有什么不对或者疑问请留言。

    什么情况下block会造成循环引用

    block为了保证代码块内部对象不被提前释放,会对block中的对象进行强引用,就相当于持有了其中的对象,而如果此时block中的对象又持有了该block,就会造成循环引用。

    常见误区

    1.所有block都会造成循环引用

    在block中,并不是所有的block都会循造成环引用,比如UIView动画block、Masonry添加约束block、AFN网络请求回调block等。
        UIView动画block不会造成循环引用是因为这是类方法,不可能强引用一个类,所以不会造成循环引用。
        Masonry约束block不会造成循环引用是因为内部使用的是局部变量,block并没有持有self,在超出它的作用域后,就会被销毁,所以我们使用Masonry的时候不需要担心循环引用。

    • Masonry内部代码
    - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
        self.translatesAutoresizingMaskIntoConstraints = NO;
        MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
     //这里并不是self.block
        block(constraintMaker);
        return [constraintMaker install];
    }
    

    AFN请求回调block不会造成循环引用是因为在内部做了处理。block先是被AFURLSessionManagerTaskDelegate对象持有。而AFURLSessionManagerTaskDelegate对象被mutableTaskDelegatesKeyedByTaskIdentifier字典持有,在block执行完成后,mutableTaskDelegatesKeyedByTaskIdentifier字典会移除AFURLSessionManagerTaskDelegate对象,这样对象就被释放了,所以不会造成循环引用。

    • AFN内部代码
    #pragma mark - 添加代理
    - (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
    {
        AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] init];
        delegate.manager = self;
         //block被代理引用
        delegate.completionHandler = completionHandler;
        dataTask.taskDescription = self.taskDescriptionForSessionTasks;
        //设置代理
        [self setDelegate:delegate forTask:dataTask];
    
        delegate.uploadProgressBlock = uploadProgressBlock;
        delegate.downloadProgressBlock = downloadProgressBlock;
    }
    #pragma mark - 设置代理
    - (void)setDelegate:(AFURLSessionManagerTaskDelegate *)delegate
                forTask:(NSURLSessionTask *)task
    {
        NSParameterAssert(task);
        NSParameterAssert(delegate);
    
        [self.lock lock];
        //代理被mutableTaskDelegatesKeyedByTaskIdentifier字典引用
        self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
        [delegate setupProgressForTask:task];
        [self addNotificationObserverForTask:task];
        [self.lock unlock];
    }
    #pragma mark - 任务完成
    - (void)URLSession:(NSURLSession *)session
              dataTask:(NSURLSessionDataTask *)dataTask
    didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask
    {
        AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:dataTask];
        if (delegate) {
            //任务完成,移除
            [self removeDelegateForTask:dataTask];
            [self setDelegate:delegate forTask:downloadTask];
        }
    
        if (self.dataTaskDidBecomeDownloadTask) {
            self.dataTaskDidBecomeDownloadTask(session, dataTask, downloadTask);
        }
    }
    #pragma mark - 移除任务代理
    - (void)removeDelegateForTask:(NSURLSessionTask *)task {
        NSParameterAssert(task);
    
        AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:task];
        [self.lock lock];
        [delegate cleanUpProgressForTask:task];
        [self removeNotificationObserverForTask:task];
        //移除
        [self.mutableTaskDelegatesKeyedByTaskIdentifier removeObjectForKey:@(task.taskIdentifier)];
        [self.lock unlock];
    }
    
    
    
    

    2.block中只有self会造成循环引用

    在block中并不只是self会造成循环引用,用下划线调用属性(如_name)也会出现循环引用,效果和使用self是一样的。

    //会造成循环引用
    _person1 = [[Person alloc] init];
    _person2 = [[Person alloc] init];
    _person2.name = @"张三";
        
    [_person1 Block:^{
        NSLog(@"%@",_person2.name)
    }];
    

    3.通过__weak __typeof(self) weakSelf = self;可以解决所有block造成的循环引用

    大部分情况下,这样使用是可以解决block循环引用,但是有些情况下这样使用会造成一些问题,比如在block中延迟执行一些代码,在还没有执行的时候,控制器被销毁了,这样控制器中的对象也会被释放,__weak对象就会变成null。所以会输出null。

    //在延迟执行期间,控制器被释放了,打印出来的会是**(null)**
    _person1 = [[Person alloc] init];
    _person2 = [[Person alloc] init];
    _person2.name = @"张三";
    __weak __typeof(self) weakSelf = self;
    [_person1 Block:^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",weakSelf.person2.name);
        });
    }];
    

    4.用self调用带有block的方法会引起循环引用

    并不是所有通过self调用带有block的方法会引起循环引用,需要看方法内部有没有持有self。

    //不会引起循环引用
    [self dismissViewControllerAnimated:YES completion:^{
        NSLog(@"%@",self.string);
    }];
    

    如何避免循环引用

    1.weakSelf、strongSelf结合使用

    使用weakSelf结合strongSelf的情况下,能够避免循环引用,也不会造成提前释放导致block内部代码无效。

    _person1 = [[Person alloc] init];
    _person2 = [[Person alloc] init];
    _person2.name = @"张三";
    __weak __typeof(self) weakSelf = self;
    [_person1 Block:^{
        __typeof(&*weakSelf) strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",strongSelf.person2.name);
        });
    }];
    

    2.block的外部对象使用week

    外部对象通过week修饰,使用全局弱指针指向一个局部强引用对象,这样局部变量在超出其作用域后也不会被销毁,因为是弱指针,所以不会造成循环引用。

    @interface CLViewController ()
    //弱引用指针
    @property (nonatomic,weak) Person *person1;
    @property (nonatomic,strong) Person *person2;
    
    @end
    
    @implementation CLViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor redColor];
        //局部强引用对象
        Person *person1 = [[Person alloc] init];
        _person1 = person1;
        
        
        _person2 = [[Person alloc] init];
        _person2.name = @"张三";
        [_person1 Block:^{
                NSLog(@"%@",self.person2.name);
        }];
    
    }
    

    3.将对象置为nil

    使用完对象之后就没有必要再保留该对象了,将对象置为nil。在ARC中,被置为nil的对象会被销毁。虽然这样也可以达到破除循环引用的效果,但是这样使用起来很不方便,如果一个对象有多个block,在前面将对象置空,后面的block就不会执行,所以使用这种方法来防止循环引用,需要注意在合适的位置将对象置空。

    _person1 = [[Person alloc] init];
    _person2 = [[Person alloc] init];
    _person2.name = @"张三";
    [_person1 Block:^{
        NSLog(@"%@",self.person2.name);
        //置空,避免循环引用
        _person1 = nil;
    }];
    //由于上面已经将对象置空,所以这里block里边的代码不会执行
    [_person1 Block:^{
        NSLog(@"%@",self.person2.name);
    }];
    
    

    虽然这种方式使用起来不是很友好,但是在封装block的时候,可以考虑使用完马上置空当前使用的block,这样使用的时候就不需要考虑循环引用的问题。

    - (void)back
    {
        if (self.BackBlock)
        {
            self.BackBlock(button);
        }
    //使用完,马上置空当前block
        self.BackBlock = nil;
    }
    
    

    总结

    使用block的时候,我们首先需要做的就是判断当前block是否会引起循环引用,如果会引起循环引用,再考虑采取哪种方式来避免循环引用。

    相关文章

      网友评论

      本文标题:Block循环引用详解

      本文链接:https://www.haomeiwen.com/subject/asthvttx.html