原文地址:http://matrixzk.github.io/blog/20150518/store_blocks_in_NSArray/
一直以來我都認(rèn)為在 ARC 下,給 Cocoa 框架的集合類,如 NSArray,添加 Block 類型的元素時,Block 是會被編譯器自動執(zhí)行 copy 操作的。而且一直以來的實踐也驗證了這一事實。但今天在測試如下一段代碼時出現(xiàn)了問題。
問題描述
先看下出問題的測試代碼:
id getBlockArray() {
int val = 12;
NSArray *arr = [[NSArray alloc] initWithObjects:^{NSLog(@"block1 val = %d", val);},
^{NSLog(@"block2 val = %d", val);},
nil];
return arr;
}
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[]) {
id blkArray = [obj getBlockArray];
MyBlock block1 = blkArray[0];
MyBlock block2 = blkArray[1]; // EXC_BAD_ACCESS, crash !!!
blcok1();
blcok2();
return 0;
}
如上所示,在獲取數(shù)組中第二個 Block 元素時,crash 了,原因是 EXC_BAD_ACCESS,即訪問了已被釋放的無效內(nèi)存。很奇怪,調(diào)試打印 arr,輸出如下:
<__NSArrayM 0x100300500>(
<__NSMallocBlock__: 0x100300410>,
<__NSStackBlock__: 0x7fff5fbff750>
)
居然一個在堆上,一個在棧上,這。。。有些挑戰(zhàn)三觀了。
測試驗證
確實很奇怪,那不妨來試試給數(shù)組填充元素的其他方式吧:
- (id)getBlockArray {
int val = 10;
NSMutableArray *arr = [[NSMutableArray alloc] init];
[arr addObject:^{NSLog(@"block1 val = %d", val);}];
[arr addObject:^{NSLog(@"block2 val = %d", val);}];
return arr;
}
以及:
int val = 10;
NSArray *arr = @[^{NSLog(@"block1 val = %d", val);}, ^{NSLog(@"block2 val = %d", val);}];
這兩種情況下調(diào)試打印 arr,輸出如下:
<__NSArrayM 0x100100250>(
<__NSMallocBlock__: 0x100200550>,
<__NSMallocBlock__: 0x1003007e0>
)
可以看到都沒問題,作為 addObject: 參數(shù)添加進(jìn)來的兩個 Block 元素,都被編譯器自動執(zhí)行了 copy 操作,這樣 Block 的類型就變成了 __NSMallocBlock,被拷貝到了堆上。好險,三觀稍微又正了點兒。但文章開頭的問題究竟是什么原因呢?
探尋原因
比較上邊的測試代碼和出問題的代碼,同樣都是 ARC 的測試環(huán)境,為什么問題代碼中數(shù)組的兩個 Block 元素,第一個在堆上,第二個在棧上呢?聯(lián)想到像測試代碼中這樣,將 Block 拷貝到堆上的操作是編譯器在編譯時完成的,那問題會不會出在初始化方法上呢?然后點開出問題的那個 API:
- (instancetype)initWithObjects:(id)firstObj, ... NS_REQUIRES_NIL_TERMINATION;
果然!這里的參數(shù)是可變個數(shù)的,而且只有第一個參數(shù)顯式的聲明為 id 類型。這下就能解釋問題代碼中,為什么第一個 Block 元素在堆上而第二個卻在棧上:因為只有第一個參數(shù)顯式的聲明為 id 類型,所以編譯器在編譯階段只能意識到需要對第一個作為參數(shù)傳進(jìn)來的 Block 進(jìn)行 copy 處理。為了驗證這一猜測,下面顯式得把后邊的 Block 傳參強制轉(zhuǎn)換為 id 類型,讓編譯器看到它:
NSArray *arr = [[NSArray alloc] initWithObjects:^{NSLog(@"block1 val = %d", val);},
(id)^{NSLog(@"block2 val = %d", val);},
nil];
代碼順利運行通過,沒有 crash,猜測得到了驗證。這真算是一個坑點兒。在 stackoverflow 上看到了一個對類似問題的討論,可以參考下:Storing Blocks in an Array。
擴展
另外需要注意的一點是,在 MRC 下,向方法或函數(shù)的參數(shù)中傳遞 Block 時,除了以下兩種情況,都需要手動 copy 一下:
- Cocoa 框架中的方法名含有 usingBlock 的方法。例如 NSArray 的
enumerateObjectsUsingBlock實例方法。 - GCD 的 API。例如,
dispatch_async函數(shù)。
在 ARC 下,除了上述兩種情況外,在如下兩種情況,編譯器也幫我們自動做了 copy 操作:
- Block 作為函數(shù)或方法的返回值返回時。(此場景和 ARC 下普通的對象作為函數(shù)或方法返回值返回時的場景一致)
- 將 Block 賦值給附有
__strong修飾符的變量時。(ARC 下的局部變量和成員變量默認(rèn)都是__strong的,只是作用域不同)
這里有一個有趣的小測試Objective-C Blocks Quiz,可以測下自己對 Block 的理解。