深入理解Objective-C的Block

最近時間少,也變得懶了,好久沒在這裏寫文章了,眼看就到8月末了,還是整理一篇醞釀已久的吧。之前的文章中整理過用ObjectiveC開發中常用到的Block代碼塊,其中也提到了一個和block使用不當的crash例子。接着這個問題,本篇文章將更深一步,對Block的內存使用相關的內容簡要整理一下,解釋其中的道理和使用Block需要注意的問題


0. 問題所在

下面給出一段代碼:

- (NSArray*) getBlockArray
{
    int num = 916;
    return [[NSArray alloc] initWithObjects:
            ^{ NSLog(@"this is block 0:%i", num); },
            ^{ NSLog(@"this is block 1:%i", num); },
            ^{ NSLog(@"this is block 2:%i", num); },
            nil];
}
 
- (void) forTest
{
    int a = 10;
    int b = 20;
}
 
- (void)test
{
    NSArray* obj = [self getBlockArray];
    [self forTest];
    void (^blockObject)(void);
    blockObject = [obj objectAtIndex:2];
    blockObject();
}

如上兩個方法實現的代碼並不難理解,其中第三個方法我們要去調用。它會調用第一個方法,並返回一個數組,數組中的元素是block代碼塊。那麼在特定的場景下,調用test會發生crash(閃退)。說明這樣的調用存在問題,恐怕能看到的應該就是EXC_BAD_ACCESS錯誤,通常這可以理解爲一個“野指針”錯誤,訪問了內存中不該訪問的內容。

問題在哪?從“野指針”錯誤,我們很直接能想到的就是block對象引用到的地址內容已經不是我們想要的了,簡單說就是block無效了。可block是對象類型的啊,爲什麼放在數組對象中回傳失效了呢,加入NSArray的對象本身就應該retain過啊。

問題就在這裏,下面我們先來看簡單下Block與對象的關係。

1. Block與對象

首先我們先反思幾個問題:

  • block到底是不是對象?
  • 如果是對象,和某個已定義的類的實例對象在使用上是不是一樣的?
  • 如果不一樣,主要的區別是什麼?

對於第一個問題,蘋果的Objective-C官方文檔中在“Working with Blocks”明確說明:

Blocks are Objective-C objects, which means they can be added to collections like NSArray or NSDictionary.  ”

可見,Block是Objective-C語言中的對象

蘋果在block的文檔中也提過這麼一句:

As an optimization, block storage starts out on the stack—just like blocks themselves do.

Clang的文檔中也有說明:

The initial allocation is done on the stack,but the runtime provides a Block_copy function” (Block_copy在下面我會說)

憑這一點,我們就可以回答剩下的兩個問題。Block對象與一般的類實例對象有所不同,一個主要的區別就是分配的位置不同,block默認在棧上分配,一般類的實例對象在堆上分配。

而這正是導致本文最初提到的那個問題發生的根本原因。Block對象在棧上分配,block的引用指向棧幀內存,而當方法調用過後,指針指向的內存上寫的是什麼數據就不確定了。但是到此,retain的疑問還是沒有解開。

我們想一想Objective-C引用計數的原理,retain是對一個在堆中分配內存的對象的引用計數做了增加,執行release操作的時候檢查計數是否爲1,如果是則釋放堆中內存。而對於在棧上分配的block對象,這一點顯然有所不同,如果方法調用返回,棧幀上的數據自然會作廢處理,不像堆上內存,需要單獨release,就算NSArray對block對象本身做了retain也無濟於事。

Clang文檔中提到:

Block pointers may be converted to type id; block objects are laid out in a way that makes them compatible with Objective-C objects. There is a builtin class that all block objects are considered to be objects of; this class implements retain by adjusting the reference count, not by calling Block_copy.

那麼要是想如本文開頭那樣,用一個方法對block數組做初始化是否有可行方案呢。答案是肯定的,不過需要真正瞭解block的使用,至少要會用Block_copy()和Block_release()。

2. Block的類型和使用

我這裏有對某個Block數組的一段Console Log顯示,如下:

<__NSArrayI 0x937f240>(
<__NSGlobalBlock__: 0x126750>,
<__NSStackBlock__: 0xbfffc788>,
<__NSMallocBlock__: 0x937f1c0>,
<__NSMallocBlock__: 0x937f1e0>,
<__NSMallocBlock__: 0x937f200>,
<__NSMallocBlock__: 0x937f220>,
<__NSGlobalBlock__: 0x126818>
)


可以看得出,這些對象都是block,而且還分了3種不同的類型。

其實在Clang的文檔中,只定義了兩個Block類型: _NSConcreteGlobalBlock 和 _NSConcreteStackBlock 。而在Console中的Log我們看到的3個類型應該是處理過的顯示,這些字樣在蘋果的文檔和Clang/LLVM的文檔中實難找到。通過字面上來看,可以認爲 _NSConcreteGlobalBlock對應於 __NSGlobalBlock__ ,_NSConcreteStackBlock對應於 __NSStackBlock__ ,而__NSMallocBlock__則是另一種情況。(實際上也正是如此)

NSGlobalBlock,我們只要實現一個沒有對周圍變量沒有引用的Block,就會顯示爲是它。而如果其中加入了對定義環境變量的引用,就是NSStackBlock。那麼NSMallocBlock又是哪來的呢?malloc一詞其實大家都熟悉,就是在堆上分配動態內存時。沒錯,如果你對一個NSStackBlock對象使用了Block_copy()或者發送了copy消息,就會得到NSMallocBlock。這一段中的幾項結論可從代碼實驗得出。

因此,也就得到了下面對block的使用注意點。

對於Global的Block,我們無需多處理,不需retain和copy,因爲即使你這樣做了,似乎也不會有什麼兩樣。對於Stack的Block,如果不做任何操作,就會向上面所說,隨棧幀自生自滅。而如果想讓它獲得比stack frame更久,那就調用Block_copy(),讓它搬家到堆內存上。而對於已經在堆上的block,也不要指望通過copy進行“真正的copy”,因爲其引用到的變量仍然會是同一份,在這個意義上看,這裏的copy和retain的作用已經非常類似。

“The runtime provides a Block_copy function which, given a block pointer, either copies the underlying block object to the heap, setting its reference count to 1 and returning the new block pointer, or (if the block object is already on the heap) increases its reference count by 1. The paired function is Block_release, which decreases the reference count by 1 and destroys the object if the count reaches zero and is on the heap.

在類中,如果有block對象作爲property,可以聲明爲copy。

3. 其它

如果註釋掉其中看似無關的[self forTest]調用,用當前的Xcode版本(我用的是5.1.1)build後,crash是不會發生的,這看起來很有意思。因爲forTest方法本身並沒有在邏輯上對數組的構建造成什麼影響。

實際上這是因爲上一個方法調用的棧幀沒有被新的數據覆蓋,仍然保留原來block數據的原因所致。這樣顯然是不安全的,是不能保證block數據可用的。

在ARC情況下,我們會發現一個有意思的情況,那就是返回的Block Array,只有元素0是執行過copy的。比如block數組中的第0個block是stack的,那麼返回之後在數組index爲0處取到的block變成了malloc的。與此同時,其它的block都如同沒有執行過copy一樣,如上述各段所述。這是一個現象,或者說是一個結論。至於爲什麼這樣,衆說紛紜,很多人認爲這是編譯器的一個bug,歡迎大家多多討論,給出見解。

在蘋果官方的《Transitioning to ARC Release Notes》文檔中,寫了這樣一段話,大家理解一下,尤其是其中的“just work”。

“How do blocks work in ARC?

Blocks ‘just work’ when you pass blocks up the stack in ARC mode, such as in a return. You don’t have to call Block Copy any more.”

4. 參考

以上整理了對Block的理解,在開發中注意到這些點足以解決block的特殊性帶來的各類問題。要想繼續深入,可參看LLVM文檔中對block的介紹:

http://clang.llvm.org/docs/Block-ABI-Apple.html

http://clang.llvm.org/docs/AutomaticReferenceCounting.html?highlight=class

補充:如下幾篇文章講解得也很細緻,可以參看。

《block沒那麼難(一)》

《block沒那麼難(二)》

《block沒那麼難(三)》

 本文轉自 三石道

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章