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 是否存在的問題。