ARC 下向 NSArray 添加 Block 元素的一個小坑

原文地址: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 的理解。

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

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

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