Block

Block 是 Objective-C 版本的 lambda 或者 closure(閉包)。

使用 block 定義異步接口:

- (void)downloadObjectsAtPath:(NSString *)path
                   completion:(void(^)(NSArray *objects, NSError *error))completion;

當你定義一個類似上面的接口的時候,盡量使用一個單獨的 block 作為接口的最后一個參數(shù)。把需要提供的數(shù)據(jù)和錯誤信息整合到一個單獨 block 中,比分別提供成功和失敗的 block 要好。

以下是你應該這樣做的原因:

通常這成功處理和失敗處理會共享一些代碼(比如讓一個進度條或者提示消失);
Apple 也是這樣做的,與平臺一致能夠帶來一些潛在的好處;
block 通常會有多行代碼,如果不作為最后一個參數(shù)放在后面的話,會打破調(diào)用點;
使用多個 block 作為參數(shù)可能會讓調(diào)用看起來顯得很笨拙,并且增加了復雜性。
看上面的方法,完成處理的 block 的參數(shù)很常見:第一個參數(shù)是調(diào)用者希望獲取的數(shù)據(jù),第二個是錯誤相關的信息。這里需要遵循以下兩點:

若 objects 不為 nil,則 error 必須為 nil
若 objects 為 nil,則 error 必須不為 nil
因為調(diào)用者更關心的是實際的數(shù)據(jù),就像這樣:

- (void)downloadObjectsAtPath:(NSString *)path
                   completion:(void(^)(NSArray *objects, NSError *error))completion {
    if (objects) {
        // do something with the data
    }
    else {
        // some error occurred, 'error' variable should not be nil by contract
    }
}

此外,Apple 提供的一些同步接口在成功狀態(tài)下向 error 參數(shù)(如果非 NULL) 寫入了垃圾值,所以檢查 error 的值可能出現(xiàn)問題。

深入 Block

一些關鍵點:

block 是在棧上創(chuàng)建的
block 可以復制到堆上
Block會捕獲棧上的變量(或指針),將其復制為自己私有的const(變量)。
(如果在Block中修改Block塊外的)棧上的變量和指針,那么這些變量和指針必須用__block關鍵字申明(譯者注:否則就會跟上面的情況一樣只是捕獲他們的瞬時值)。
如果 block 沒有在其他地方被保持,那么它會隨著棧生存并且當棧幀(stack frame)返回的時候消失。僅存在于棧上時,block對對象訪問的內(nèi)存管理和生命周期沒有任何影響。

如果 block 需要在棧幀返回的時候存在,它們需要明確地被復制到堆上,這樣,block 會像其他 Cocoa 對象一樣增加引用計數(shù)。當它們被復制的時候,它會帶著它們的捕獲作用域一起,retain 他們所有引用的對象。

如果一個 block引用了一個棧變量或指針,那么這個block初始化的時候會擁有這個變量或指針的const副本,所以(被捕獲之后再在棧中改變這個變量或指針的值)是不起作用的。(譯者注:所以這時候我們在block中對這種變量進行賦值會編譯報錯:Variable is not assignable(missing __block type specifier),因為他們是副本而且是const的.具體見下面的例程)。

當一個 block 被復制后,__block 聲明的棧變量的引用被復制到了堆里,復制完成之后,無論是棧上的block還是剛剛產(chǎn)生在堆上的block(棧上block的副本)都會引用該變量在堆上的副本。

(下面代碼是譯者加的)

   ...
   CGFloat blockInt = 10;
   void (^playblock)(void) = ^{
        NSLog(@"blockInt = %zd", blockInt);
    };
    blockInt ++;
    playblock();
    ...

    //結果為:blockInt = 10

最重要的事情是 __block 聲明的變量和指針在 block 里面是作為顯示操作真實值/對象的結構來對待的。

block 在 Objective-C 的 runtime(運行時) 里面被當作一等公民對待:他們有一個 isa 指針,一個類也是用 isa 指針在Objective-C 運行時來訪問方法和存儲數(shù)據(jù)的。在非 ARC 環(huán)境肯定會把它搞得很糟糕,并且懸掛指針會導致 crash。__block 僅僅對 block 內(nèi)的變量起作用,它只是簡單地告訴 block:
嗨,這個指針或者原始的類型依賴它們在的棧。請用一個棧上的新變量來引用它。我是說,請對它進行雙重解引用,不要 retain 它。 謝謝,哥們。
如果在定義之后但是 block 沒有被調(diào)用前,對象被釋放了,那么 block 的執(zhí)行會導致 crash。 __block 變量不會在 block 中被持有,最后... 指針、引用、解引用以及引用計數(shù)變得一團糟。

self 的循環(huán)引用

當使用代碼塊和異步分發(fā)的時候,要注意避免引用循環(huán)。 總是使用 weak 來引用對象,避免引用循環(huán)。(譯者注:這里更為優(yōu)雅的方式是采用影子變量@weakify/@strongify 這里有更為詳細的說明) 此外,把持有 block 的屬性設置為 nil (比如 self.completionBlock = nil) 是一個好的實踐。它會打破 block 捕獲的作用域帶來的引用循環(huán)。

例子:

__weak __typeof(self) weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
    [weakSelf doSomethingWithData:data];
}];

不要這樣:

[self executeBlock:^(NSData *data, NSError *error) {
    [self doSomethingWithData:data];
}];

多個語句的例子:

__weak __typeof(self)weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
    __strong __typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) {
        [strongSelf doSomethingWithData:data];
        [strongSelf doSomethingWithData:data];
    }
}];

不要這樣:

__weak __typeof(self)weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
    [weakSelf doSomethingWithData:data];
    [weakSelf doSomethingWithData:data];
}];

你應該把這兩行代碼作為 snippet 加到 Xcode 里面并且總是這樣使用它們。

__weak __typeof(self)weakSelf = self;
__strong __typeof(weakSelf)strongSelf = weakSelf;

這里我們來討論下 block 里面的 self 的 __weak 和 __strong 限定詞的一些微妙的地方。簡而言之,我們可以參考 self 在 block 里面的三種不同情況。

直接在 block 里面使用關鍵詞 self
在 block 外定義一個 __weak 的 引用到 self,并且在 block 里面使用這個弱引用
在 block 外定義一個 __weak 的 引用到 self,并在在 block 內(nèi)部通過這個弱引用定義一個 __strong 的引用。
方案 1. 直接在 block 里面使用關鍵詞 self

如果我們直接在 block 里面用 self 關鍵字,對象會在 block 的定義時候被 retain,(實際上 block 是 copied 但是為了簡單我們可以忽略這個)。

一個 const 的對 self 的引用在 block 里面有自己的位置并且它會影響對象的引用計數(shù)。

如果這個block被其他的類使用并且(或者)彼此間傳來傳去,我們可能想要在 block 中保留 self,就像其他在 block 中使用的對象一樣. 因為他們是block執(zhí)行所需要的.

dispatch_block_t completionBlock = ^{
    NSLog(@"%@", self);
}

MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
                   animated:YES
                 completion:completionHandler];

沒啥大不了。但是如果通過一個屬性中的 self 保留 了這個 block(就像下面的例程一樣),對象( self )保留了 block 會怎么樣呢?

self.completionHandler = ^{
    NSLog(@"%@", self);
}

MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
                   animated:YES
                 completion:self.completionHandler];

這就是有名的 retain cycle, 并且我們通常應該避免它。這種情況下我們收到 CLANG 的警告:

Capturing 'self' strongly in this block is likely to lead to a retain cycle (在 block 里面發(fā)現(xiàn)了 `self` 的強引用,可能會導致循環(huán)引用)

所以 __weak 就有用武之地了。

方案 2. 在 block 外定義一個 __weak 的 引用到 self,并且在 block 里面使用這個弱引用

這樣會避免循壞引用,也是通常情況下我們的block作為類的屬性被self retain 的時候會做的。

__weak typeof(self) weakSelf = self;
self.completionHandler = ^{
    NSLog(@"%@", weakSelf);
};

MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
                   animated:YES
                 completion:self.completionHandler];

這個情況下 block 沒有 retain 對象并且對象在屬性里面 retain 了 block 。所以這樣我們能保證了安全的訪問 self。 不過糟糕的是,它可能被設置成 nil 的。問題是:如何讓 self 在 block 里面安全地被銷毀。

考慮這么個情況:block 作為屬性(property)賦值的結果,從一個對象被復制到另一個對象(如 myController),在這個復制的 block 執(zhí)行之前,前者(即之前的那個對象)已經(jīng)被解除分配。

下面的更有意思。

方案 3. 在 block 外定義一個 __weak 的 引用到 self,并在在 block 內(nèi)部通過這個弱引用定義一個 __strong 的引用

你可能會想,首先,這是避免 retain cycle 警告的一個技巧。這不是重點,這個 self 的強引用是在block 執(zhí)行時 被創(chuàng)建的,但是否使用 self 在 block 定義時就已經(jīng)定下來了, 因此self (在block執(zhí)行時) 會被 retain。

Apple 文檔 中表示 "為了 non-trivial cycles ,你應該這樣" :

MyViewController *myController = [[MyViewController alloc] init...];
// ...
MyViewController * __weak weakMyController = myController;
myController.completionHandler =  ^(NSInteger result) {
    MyViewController *strongMyController = weakMyController;
    if (strongMyController) {
        // ...
        [strongMyController dismissViewControllerAnimated:YES completion:nil];
        // ...
    }
    else {
        // Probably nothing...
    }
};

首先,我覺得這個例子看起來是錯誤的。如果 block 本身在 completionHandler 屬性中被 retain 了,那么 self 如何被 delloc 和在 block 之外賦值為 nil 呢? completionHandler 屬性可以被聲明為 assign 或者 unsafe_unretained 的,來允許對象在 block 被傳遞之后被銷毀。

我不能理解這樣做的理由,如果其他對象需要這個對象(self),block 被傳遞的時候應該 retain 對象,所以 block 應該不被作為屬性存儲。這種情況下不應該用 __weak/__strong

總之,其他情況下,希望 weakSelf 變成 nil 的話,就像第二種情況解釋那么寫(在 block 之外定義一個弱應用并且在 block 里面使用)。

還有,Apple的 "trivial block" 是什么呢。我們的理解是 trivial block 是一個不被傳送的 block ,它在一個良好定義和控制的作用域里面,weak 修飾只是為了避免循環(huán)引用。

雖然有 Kazuki Sakamoto 和 Tomohiko Furumoto) 討論的 一 些 的 在線 參考, Matt Galloway 的 (Effective Objective-C 2.0 和 Pro Multithreading and Memory Management for iOS and OS X ,大多數(shù)開發(fā)者始終沒有弄清楚概念。

在 block 內(nèi)用強引用的優(yōu)點是,搶占執(zhí)行的時候的魯棒性。在 block 執(zhí)行的時候, 再次溫故下上面的三個例子:

方案 1. 直接在 block 里面使用關鍵詞 self

如果 block 被屬性 retain,self 和 block 之間會有一個循環(huán)引用并且它們不會再被釋放。如果 block 被傳送并且被其他的對象 copy 了,self 在每一個 copy 里面被 retain

方案 2. 在 block 外定義一個 __weak 的 引用到 self,并且在 block 里面使用這個弱引用

不管 block 是否通過屬性被 retain ,這里都不會發(fā)生循環(huán)引用。如果 block 被傳遞或者 copy 了,在執(zhí)行的時候,weakSelf 可能已經(jīng)變成 nil。

block 的執(zhí)行可以搶占,而且對 weakSelf 指針的調(diào)用時序不同可以導致不同的結果(如:在一個特定的時序下 weakSelf 可能會變成 nil )。

__weak typeof(self) weakSelf = self;
dispatch_block_t block =  ^{
    [weakSelf doSomething]; // weakSelf != nil
    // preemption, weakSelf turned nil
    [weakSelf doSomethingElse]; // weakSelf == nil
};

方案 3. 在 block 外定義一個 __weak 的 引用到 self,并在在 block 內(nèi)部通過這個弱引用定義一個 __strong 的引用。

不管 block 是否通過屬性被 retain ,這里也不會發(fā)生循環(huán)引用。如果 block 被傳遞到其他對象并且被復制了,執(zhí)行的時候,weakSelf 可能被nil,因為強引用被賦值并且不會變成nil的時候,我們確保對象 在 block 調(diào)用的完整周期里面被 retain了,如果搶占發(fā)生了,隨后的對 strongSelf 的執(zhí)行會繼續(xù)并且會產(chǎn)生一樣的值。如果 strongSelf 的執(zhí)行到 nil,那么在 block 不能正確執(zhí)行前已經(jīng)返回了。

__weak typeof(self) weakSelf = self;
myObj.myBlock =  ^{
    __strong typeof(self) strongSelf = weakSelf;
    if (strongSelf) {
      [strongSelf doSomething]; // strongSelf != nil
      // preemption, strongSelf still not nil(搶占的時候,strongSelf 還是非 nil 的)
      [strongSelf doSomethingElse]; // strongSelf != nil
    }
    else {
        // Probably nothing...
        return;
    }
};

在ARC條件中,如果嘗試用 -> 符號訪問一個實例變量,編譯器會給出非常清晰的錯誤信息:

Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to a strong variable first. (對一個 __weak 指針的解引用不允許的,因為可能在競態(tài)條件里面變成 null, 所以先把他定義成 strong 的屬性)

可以用下面的代碼展示

__weak typeof(self) weakSelf = self;
myObj.myBlock =  ^{
    id localVal = weakSelf->someIVar;
};

在最后

方案 1: 只能在 block 不是作為一個 property 的時候使用,否則會導致 retain cycle。

方案 2: 當 block 被聲明為一個 property 的時候使用。

方案 3: 和并發(fā)執(zhí)行有關。當涉及異步的服務的時候,block 可以在之后被執(zhí)行,并且不會發(fā)生關于 self 是否存在的問題。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容