iOS-block迴圈引用詳解和應用

俊華的部落格發表於2021-08-20

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是否會引起迴圈引用,如果會引起迴圈引用,再考慮採取哪種方式來避免迴圈引用。


相關文章