天天看點

[轉] iOS --- 深入了解Objective-C的Block

本文轉載自深入了解Objective-C的Block.

在面向對象語言中,類封裝了資料和這些資料相關的行為。然而有些情況下,一個簡單的任務和已認證一段代碼塊和少數幾個變量來完成。在iOS中有了代碼塊block的概念,這篇文章就對block的使用做一個簡單的整理。

Block概述

Block是C語言級别和運作時方面的一個特征。Block封裝了一段代碼邏輯,也用{}括起,和标準C語言中的函數/函數指針很相似,此外就是blokc能夠對定義環境中的變量可以引用到。這一點和其它各種語言中所說的“閉包”是非常類似的概念。

在iOS中,block有很多應用場景,比如對代碼封裝作為參數傳遞。這在使用dispatch并發(Operation中也有BlockOperation)和completion異步回調等處都廣泛應用。

Block的基本使用

聲明:(傳回類型)(^聲明的block名稱)(參數清單);

實作:^(參數清單){代碼塊}

指派的例子:

double (^multiplyTwoValues)(double, double) =
           ^(double firstValue, double secondValue) {
               return firstValue * secondValue;
           };
           

有時為了友善,用typedef給出定義,在蘋果的官方文檔裡,建議出現多次的block使用typedef定義。

而block的調用,則十分簡單:block名稱(參數清單);

使用中的注意點

有如下一些注意點:

  1. 對定義環境的變量使用。預設是以const的方式使用,這有點像函數的const參數傳遞,如過需要block内修改可變,則使用__block,這樣做存儲就實作了共享,包括塊中的遞歸應用和定義環境上下文中的多個block使用。block通常定義在棧幀當中,而當所處的棧幀被銷毀的時候,block以及引用到的__block變量将會依然有效。
  2. 引用類型問題。block中的引用預設都是強引用,必要的時候需要使用__weak,同delegate使用的注意一樣,避免循環引用。此外,蘋果文檔中還給出了對instanceVariable和對localVariable引用不同的例子,注意體會下。
  3. copy。在類屬性中,要使用copy。此外,對block進行copy要使用Block_copy()/Block_release()。
  4. 蘋果文檔中幾種需要避免使用的方式。《Blocks Programming Topics》中Using Blocks中的例子,實際上就是要注意block定義的位置與其上下文的關系。
  5. 有關Block的位址/引用。注意這篇文章中最後例子中的問題:http://www.cnblogs.com/kesalin/archive/2013/04/30/ios_block.html 我的了解是Block的位址發生了變化,最終的問題是對Block引用位址釋放時的野指針錯誤。

聯想Java中匿名類使用

Java7以及之前的各個版本中,沒有“閉包”的概念(感興趣的可以看今年3月Oracle釋出的Java8),回調(callback)使用内部類實作。在方法定義中使用匿名内部類,需要注意的一點是匿名類中對外部方法參數的使用,要求參數隻能是final的。

其實在iOS中,對于block使用外部方法的參數,也隻能是const的,不能對參數進行__block要求。

更多内容,可以參考蘋果官方文檔:

Working with Blocks

Blocks Programming Topics

以上的内容整理了用ObjectiveC開發中常用到的Block代碼塊,其中也提到了一個和block使用不當的crash例子。接着這個問題,接下來将更深一步,對Block的記憶體使用相關的内容簡要整理一下,解釋其中的道理和使用Block需要注意的問題。

問題所在

下面給出一段代碼:

- (NSArray*) getBlockArray
{
    int num = ;
    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 = ;
    int b = ;
}

- (void)test
{
    NSArray* obj = [self getBlockArray];
    [self forTest];
    void (^blockObject)(void);
    blockObject = [obj objectAtIndex:];
    blockObject();
}
           

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

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

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

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()。

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。

其它

如果注釋掉其中看似無關的[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.”

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

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

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