block 循環引用問題的一點發散

開年第一餐,__weak 關鍵字用於防止 block 造成循環引用,關於它的用法,以及誤區,一起來品嚐吧。

關於 __weak

__weak 關鍵字是伴隨着 ARC 內存管理機制而來的一個變量修飾符,用於防止循環引用。 使用過 block 的朋友可能都會看到過類似這樣的建議:

__weak ViewController* weakSelf = self;

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    
    [weakSelf doSome];
    
});

如上所示, 爲什麼 block 裏面要使用 __weak 形式的引用呢? 大家常見的說法基本是這樣 —— 因爲 block 本身會對在它內部使用的所有引用進行 strong 類型的捕獲。 如果我們直接在 block 裏面直接引用 self, 它就會對 self 進行 strong 類型的引用。 而與此同時, self 也會對 block 進行 strong 引用。 這樣就會引起內存循環引用,這兩個對象所佔用的內存都無法被釋放掉。

實例驗證

這個說法是否完全正確呢? 我花時間實踐了一下,只能說它在理論上是對的,但實踐中也要根據具體情況而定。咱們來看一個例子, 我們定義一個 Reporter 類:

@interface Reporter : UIView

- (void)block: (void (^)(void)) doBlock;
- (void) foo;

@property (strong) void (^doBlock)(void);

@end

@implementation Reporter

- (instancetype)init {
    
    self = [super init];
    
    if(self) {
        
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification) name:@"Notification" object:nil];
        
    }
    
    return self;
    
}


- (void) handleNotification {
    
    NSLog(@"notification reveiced");
    
}

- (void) foo {
    //Just for demo.
}

- (void)block: (void (^)(void)) doBlock {

    self.doBlock = doBlock;
    self.doBlock();
    
}

- (void)dealloc {
    
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    NSLog(@"dealloc");
    
}

@end

這個類不算複雜,在 init 初始化的時候註冊了一個通知,只要這個對象還在內存中存在,就會處理這個通知。 我們用這個來測試對象當前是否被銷燬。

還有另外兩個方法, foo 用作示例, block 方法接受一個 block 類型的參數,並且會賦值給它的一個 strong 類型的屬性,然後執行這個傳遞進來的 block。

dealloc 方法取消了註冊通知,並且輸出一行消息。

實驗環境就創建完成了,現在我們在合適的地方使用 Reporter 這個類:


Reporter *_reporter = [[Reporter alloc] init];

[[NSNotificationCenter defaultCenter] postNotificationName:@"Notification" object:nil];
[_reporter block:^{
    
}];
_reporter = nil;

dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5);

dispatch_after(when, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    
    
    [[NSNotificationCenter defaultCenter] postNotificationName:@"Notification" object:nil];
    
});

我們這裏先初始化一個 Reporter 對象, 然後緊接着發送一個 Notification 消息, 這時候 Reporter 應該可以正確的處理這個消息,並打印控制檯輸出。

然後,我們調用 [_reporter block] 方法,傳入的 block 中保留空白。 緊接着把 _reporter 賦值爲 nil。

這時 _reporter 應該已經被釋放了。 我們來驗證一下, 使用 dispatch_after, 在 5秒鐘之後,再次發送 Notification 通知消息。果然這次沒有得到控制檯輸出, 因爲處理消息的 Reporter 對象已經不存在了。

一切都在邏輯之中,但如果這時我們對上面的代碼稍加改動,情況就會不一樣:

[_reporter block:^{

  [_reporter foo];

}];

這裏我們唯一做的改動就是在傳入的 block 中加了一行代碼 [_reporter foo]。 這時再次運行我們的程序,你會發現,雖然我們在第一次發送通知後,將 _reporter 設置爲 nil 了。 但 5秒鐘之後,依然輸出了處理通知的消息。 也就是說 Reporter 對象這次沒有被銷燬。

造成這個現象的原因就是循環引用。 還記得我們定義 Reporter 的時候嗎:

@property (strong) void (^doBlock)(void);

這個 doBlock 的屬性我們定義爲 strong 類型的強引用。 而我們剛剛做的改動,在 block 內部加入 [_reporter foo] 的調用,相當於在 block 中又反過來對 Reporter 也進行了強引用。

這樣他們兩個的實例都不能被正常銷燬,所以出現了我們第二次看到的現象。

如何解決

那麼如何解決這個問題呢,有兩種方法,第一種就是我們最開始提到的 __weak 引用:

__weak Reporter* weakReporter = _reporter;

[_reporter block:^{
    
    [weakReporter foo];
    
}];

這樣再運行程序,就恢復到正常結果了。 __weak 這個標記會告訴 block 不要對它引用的這個實例進行 strong 強引用。 兩個強引用只要斷掉其中一個,實例就可以被正常銷燬了。

就像我們提到的,兩個強引用只要斷掉一個就可以,也就是說除了使用 __weak 斷掉 block 中的強引用,我們還可以斷掉另一端:

@interface Reporter

@property (weak) void (^doBlock)(void);


@end

這次我們把 Reporter 的 doBlock 改成 weak 類型的。 這樣我們在調用出還可以這樣寫:

[_reporter block:^{
    
    [_reporter foo];
    
}];

這次運行結果也正常了。 雖然 block 會對 _reporter 進行強引用, 但 _reporter 對 block 是弱引用。

用在適合的場景

好了,上面說了這麼多後,這裏到了真正想和大家討論的地方了。 就是所有使用 block 引用外部變量的地方都必須使用 __weak 引用嗎?

答案顯而易見,肯定不是,我們剛剛的例子就是個證明。這取決於是否構成循環引用。如果使用引用 block 本身的類不是強引用,我們其實就不需要在調用的時候使用 __weak 了。

比如 GCD 和 View Animation 的 block:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        
});
[UIView animateWithDuration:0.2 animations:^{
        
}];

在我實際使用它們的時候,我發現不使用 __weak 並不會造成循環引用。 但在另外一些情況下,就需要注意這個問題, 比如 ASIHTTP 中:

ASIHTTPRequest *req = [[ASIHTTPRequest alloc] initWithURL:[NSURL URLWithString:@""]];
[req setCompletionBlock:^{
    
    [req setTag:1];
    
}];

ASIHTTP 的 setCompletionBlock 方法會對傳入的 block 進行強引用。 所以 block 內部在引用 reuqest 對象的時候,就需要加上 __weak 修飾符了。

通常在這種情況下,編譯器會給出警告:

1.png

按照這個提示,加上 __weak 引用即可。

結語

以上就是我對 weak 修飾符和它和 block 的使用問題跟大家進行的討論內容了。 雖然很多文章中會告訴我們在任何用到 block 的地方都要使用 weak。 但在我的實踐中,發現只要對那些可能造成循環引用的地方,纔有必要使用 weak。 並不是所有的 block 都會造成循環引用。 當然,這都是基於我目前對它的認知總結出來的內容,有可能還有不足,也歡迎大家在留言中展開和補充。

更多精彩內容可關注微信公衆號:
swift-cafe

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章