1. 程式人生 > >iOS中block的詳解weakSelf、strongSelf,以及block迴圈引用的避免。

iOS中block的詳解weakSelf、strongSelf,以及block迴圈引用的避免。

1

我們知道,在使用 block 的時候,為了避免產生迴圈引用,通常需要使用 weakSelf 與 strongSelf,寫下面這樣的程式碼:

__weak typeof(self) weakSelf = self;
[self doSomeBlockJob:^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) {
        ...
    }
}];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

那麼請問:什麼時候在 block 裡面用 self,不需要使用 weak self?

答案

當 block 本身不被 self 持有,而被別的物件持有,同時不產生迴圈引用的時候,就不需要使用 weak self 了。最常見的程式碼就是 UIView 的動畫程式碼,我們在使用 UIView 的 animateWithDuration:animations 方法 做動畫的時候,並不需要使用 weak self,因為引用持有關係是:

UIView 的某個負責動畫的物件持有了 block 
block 持有了 self 
因為 self 並不持有 block,所以就沒有迴圈引用產生,因為就不需要使用 weak self 了。

[UIView animateWithDuration:0.2 animations:^{
    self.alpha = 1;
}];
  • 1
  • 2
  • 3

當動畫結束時,UIView 會結束持有這個 block,如果沒有別的物件持有 block 的話,block 物件就會釋放掉,從而 block 會釋放掉對於 self 的持有。整個記憶體引用關係被解除。

思考題

如果覺得上面的問題太簡單,可以想想下面兩個題目:

為什麼 block 裡面還需要寫一個 strong self,如果不寫會怎麼樣? 
有沒有這樣一個需求場景,block會產生迴圈引用,但是業務又需要你不能使用 weak self? 如果有,請舉一個例子並且解釋這種情況下如何解決迴圈引用問題。

2

繼續回答昨天的問題第二問。

我們知道,在使用 block 的時候,為了避免產生迴圈引用,通常需要使用 weakSelf 與 strongSelf,寫下面這樣的程式碼:

__weak typeof(self) weakSelf = self;
[self doSomeBackgroundJob:^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) {
        ...
    }
}];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

那麼請問:為什麼 block 裡面還需要寫一個 strong self,如果不寫會怎麼樣?

答案

在 block 中先寫一個 strong self,其實是為了避免在 block 的執行過程中,突然出現 self 被釋放的尷尬情況。通常情況下,如果不這麼做的話,還是很容易出現一些奇怪的邏輯,甚至閃退。

我們以 AFNetworking 中 AFNetworkReachabilityManager.m 的一段程式碼舉例:

__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
    __strong __typeof(weakSelf)strongSelf = weakSelf;

    strongSelf.networkReachabilityStatus = status;
    if (strongSelf.networkReachabilityStatusBlock) {
        strongSelf.networkReachabilityStatusBlock(status);
    }

};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

如果沒有 strongSelf 的那行程式碼,那麼後面的每一行程式碼執行時,self 都可能被釋放掉了,這樣很可能造成邏輯異常。

特別是當我們正在執行 strongSelf.networkReachabilityStatusBlock(status); 這個 block 閉包時,如果這個 block 執行到一半時 self 釋放,那麼多半情況下會 Crash。

另外,還有讀者提了兩個有意思的問題,大家可以思考一下:

提問:“陣列” 和 “字典” 的 enumeratXXXUsingBlock: 是否要使用 weakSelf 和 strongSelf 呢?

提問:block 裡 strong self 後,block 不是也會持有 self 嗎?而 self 又持有 block ,那不是又迴圈引用了?

3

有沒有這樣一個需求場景,block 會產生迴圈引用,但是業務又需要你不能使用 weak self? 如果有,請舉一個例子並且解釋這種情況下如何解決迴圈引用問題。

答案

需要不使用 weak self 的場景是:你需要構造一個迴圈引用,以便保證引用雙方都存在。比如你有一個後臺的任務,希望任務執行完後,通知另外一個例項。在我們開源的 YTKNetwork 網路庫的原始碼中,就有這樣的場景。

在 YTKNetwork 庫中,我們的每一個網路請求 API 會持有回撥的 block,回撥的 block 會持有 self,而如果 self 也持有網路請求 API 的話,我們就構造了一個迴圈引用。雖然我們構造出了迴圈引用,但是因為在網路請求結束時,網路請求 API 會主動釋放對 block 的持有,因此,整個迴圈鏈條被解開,迴圈引用就被打破了,所以不會有記憶體洩漏問題。程式碼其實很簡單,如下所示:

- (void)clearCompletionBlock {
    // nil out to break the retain cycle.
    self.successCompletionBlock = nil;
    self.failureCompletionBlock = nil;
}
  • 1
  • 2
  • 3
  • 4
  • 5

總結來說,解決迴圈引用問題主要有兩個辦法:

第一個辦法是「事前避免」,我們在會產生迴圈引用的地方使用 weak 弱引用,以避免產生迴圈引用。 
第二個辦法是「事後補救」,我們明確知道會存在迴圈引用,但是我們在合理的位置主動斷開環中的一個引用,使得物件得以回收。 
思考題

下期的問題是:weak 變數在引用計數為 0 時,會被自動設定成 nil,這個特性是如何實現的?

4

weak 變數在引用計數為0時,會被自動設定成 nil,這個特性是如何實現的?

答案

《Objective-C高階程式設計》一書中也介紹了相關的內容。

簡單來說,系統有一個全域性的 CFMutableDictionary 例項,來儲存每個物件的 weak 指標列表,因為每個物件可能有多個 weak 指標,所以這個例項的值是 CFMutableSet 型別。

剩下我們要做的,就是在引用計數變成 0 的時候,去這個全域性的字典裡面,找到所有的 weak 指標,將其值設定成 nil。如何做到這一點呢?Friday QA 上介紹了一種類似 KVO 實現的方式。當物件存在 weak 指標時,我們可以將這個例項指向一個新建立的子類,然後修改這個子類的 release 方法,在 release 方法中,去從全域性的 CFMutableDictionary 字典中找到所有的 weak 物件,並且設定成 nil。我摘抄了 Friday QA 上的實現的核心程式碼,如下:

Class subclass = objc_allocateClassPair(class, newNameC, 0);
Method release = class_getInstanceMethod(class, @selector(release));
Method dealloc = class_getInstanceMethod(class, @selector(dealloc));
class_addMethod(subclass, @selector(release), (IMP)CustomSubclassRelease, method_getTypeEncoding(release));
class_addMethod(subclass, @selector(dealloc), (IMP)CustomSubclassDealloc, method_getTypeEncoding(dealloc));
objc_registerClassPair(subclass);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

當然,這並不代表蘋果官方是這麼實現的,因為蘋果的這部分程式碼並沒有開源。《Objective-C高階程式設計》一書中介紹了 GNUStep 專案中的開原始碼,思想也是類似的。所以我認為雖然實現細節會有差異,但是大致的實現思路應該差別不大。