原文地址:http://matrixzk.github.io/blog/20150518/store_blocks_in_NSArray/
一直以來我都認為在 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ù)添加進來的兩個 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ù)傳進來的 Block 進行 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 下的局部變量和成員變量默認都是__strong
的御滩,只是作用域不同)
這里有一個有趣的小測試Objective-C Blocks Quiz鸥拧,可以測下自己對 Block 的理解。