iOS-block循环引用详解和应用
Block循环引用
什么情况下block会造成循环引用
ARC 情况下 block为了保证代码块内部对象不被提前释放,会对block中的对象进行强引用,就相当于持有了其中的对象,而如果此时block中的对象又持有了该block,就会造成循环引用。
常见误区
误区一.所有block都会造成循环引用
在block中,并不是所有的block都会循造成环引用,比如UIView动画block、Masonry添加约束block、AFN网络请求回调block等。
1. UIView动画block不会造成循环引用是因为这是类方法,不可能强引用一个类,所以不会造成循环引用。
2. Masonry约束block不会造成循环引用是因为self并没有持有block,所以我们使用Masonry的时候不需要担心循环引用。
- Masonry内部代码
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; //这里并不是self.block (self并没有持有block 所以不会引起循环引用) block(constraintMaker); return [constraintMaker install]; }
3.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]; }
误区二.block中只有self会造成循环引用
在block中并不只是self会造成循环引用,用下划线调用属性(如_name)也会出现循环引用,效果和使用self是一样的(内部会用self->name去查找)。
//会造成循环引用 _person1 = [[Person alloc] init]; _person2 = [[Person alloc] init]; _person2.name = @"张三"; [_person1 Block:^{ NSLog(@"%@",_person2.name) }];
误区三.通过__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); }); }];
误区四.用self调用带有block的方法会引起循环引用
并不是所有通过self调用带有block的方法会引起循环引用,需要看方法内部有没有持有self。
//不会引起循环引用 [self dismissViewControllerAnimated:YES completion:^{ NSLog(@"%@",self.string); }];
如何避免循环引用
方式一、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); }); }];
方式二、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); }]; }
方式三.将对象置为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是否会引起循环引用,如果会引起循环引用,再考虑采取哪种方式来避免循环引用。