天天看點

Effective Objective-C 2.0 - 第三七條:了解“塊”這一概念

前言

  塊可以實作閉包,該項特性言語特性是作為“拓展”(extension)而加入GCC編譯器中的。Clang 10.4 和 iOS 4.0都含有塊正常執行所需的運作期元件,從技術層面江,這是位于C語言層面的特性,可以在C , C++, OC, OC++代碼中使用它。

塊基礎知識

  塊和函數相似,隻不過塊是直接定義在另一個函數内部,和定義它的函數共享同一個範圍的東西。塊其實就是一個值(對象),可以進行指派。塊的文法結構如下:

return_type (^block_name)(parameters)


//例子

int (^addBlock)(int a, int b) {
    return a + b;
}

//使用
int add = addBlock(2, 5);
           

  塊的優勢:在聲明它的範圍裡,所有變量都可被捕獲。

int additional = 5;
int (^addBlock) (int a, int b) {
    return a + b + additional;
};

int add = addBlock(2, 5);
           

  __block:預設情況下,塊所捕獲的變量是不可在塊内部修改值的。除非在變量前加上__block修飾(__block int additional = 3;),

  如果塊所捕獲的變量是對象類型,則自動保留它,系統在釋放塊的時候,同時會将其一并釋放。這就引出了:塊本身可視為對象,那麼它和其他對象一樣,同樣具有引用計數。當最後一個指向塊的引用移走之後,同時将塊所捕獲的變量給釋放了。

  如果将塊定義在OC執行個體方法中,那麼除了可以通路類的所有執行個體變量之外,還可以使用self變量。塊總能修改執行個體變量,是以在聲明時無需添加__block修飾。不過通過讀取或者寫入操作,捕獲了執行個體變量,那麼也會将self一同捕獲了,因為執行個體變量和self所指的執行個體是關聯在一起的。(類的變量呢?) 注意:一定要記住,self 也是個對象,因為塊将其捕獲時也會将其保留,如果self所指向的那個對象同樣也保留了塊,那麼這種情況通常會産生“保留環”(40)。

塊的類型

  定義塊時,其所占的記憶體是配置設定在棧中的,即:塊隻在定義它的那個範圍内有效,例如下面代碼存在問題:

void (^block) ();
if (something) {
    block = ^{
        NSLog();
    };
else {
    block = ^{
        NSLog();
    };
}
block();
           

  定義在if 及 else語句中的兩個塊都配置設定在棧記憶體中,出了相應的範圍,編譯器有可能把配置設定給塊的記憶體覆寫,于是兩個塊隻能保證在if else語句的範圍内有效。為了解決該問題,可給塊對象發送copy消息,這樣就可将block将棧區拷貝到堆區了,一旦複制到堆上,塊就成了帶引用計數的對象了。後續的copy操作都不會真正執行copy操作,隻是增加塊的引用計數,在ARC中,當不再使用該塊時,會自動釋放它,否則需要手動釋放。明白這一點隻需要添加copy即可:

void (^block) ();
if (something) {
    block = [^{
        NSLog();
    } copy];;
else {
    block = [^{
        NSLog();
    }copy];
}
block();
           

  還有一種塊:全局塊(global block),這種塊不會捕捉任何狀态(比如外圍的變量),運作時也無需有狀态來參與,塊的整個記憶體在編譯期已經完全确定了,全局塊是聲明在全局記憶體中,不需要每次使用都去棧中建立,另外,全局塊拷貝是個空操作。

塊的内部結構

塊本身是一個對象,每個OC對象都占據着某個記憶體區域,因為執行個體變量的個數以及對象所包含的關聯資料不同,每個對象所占用的記憶體大小也不盡相同。塊是對象,塊的記憶體區域中的首個變量是指向Class對象的指針,即isa,記憶體布局如下

invoke指針:這是個函數指針,指向塊的實作代碼

descriptor變量:指向結構體的指針,每個塊裡都包含此結構體,還聲明了copy 和 dispose這兩個輔助函數所對應的函數指針。

unsigned long int reserved

unsigned long int size

void (*)(void* ,void*) copy

void (*)(void* ,void*)dispose
           

其中塊會把它所捕獲的所有變量都拷貝一份,這些拷貝放在descriptor變量後面,捕獲多少變量就要占據多少的記憶體空間,請注意拷貝的不是對象本身,而是指向這些對象的指針變量。這也是invoke函數為何需要把塊對象作為參數傳進來:原因在于,執行塊時要從記憶體中把這些捕獲到的變量讀出來。

下一篇:塊的實戰