Block

要了解Block需要先了解什么是“閉包性”

2.3 閉包性

上文說過,block實際是Objc對閉包的實現。

我們來看看下面代碼:

#import?void?logBlock(?int?(?^?theBlock?)(?void?)?)

{

NSLog(?@"Closure?var?X:?%i",?theBlock()?);

}

int?main(?void?)

{

NSAutoreleasePool?*?pool;

int?(?^?myBlock?)(?void?);

int?x;

pool?=?[?[?NSAutoreleasePool?alloc?]?init?];

x?=?42;

myBlock?=?^(?void?)

{

returnx;

};

logBlock(?myBlock?);

[?pool?release?];

returnEXIT_SUCCESS;

}

上面的代碼在main函數中聲明了一個整型,并賦值42,另外還聲明了一個block,該block會將42返回。然后將block傳遞給 logBlock函數,該函數會顯示出返回的值42。即使是在函數logBlock中執行block,而block又聲明在main函數中,但是 block仍然可以訪問到x變量,并將這個值返回。

注意:block同樣可以訪問全局變量,即使是static。

一,明確兩點

1,Block可以訪問Block函數以及語法作用域以內的外部變量。也就是說:一個函數里定義了個block,這個block可以訪問該函數的內部變量(當然還包括靜態,全局變量)-即block可以使用和本身定義范圍相同的變量。

2,Block其實是特殊的Objective-C對象,可以使用copy,release等來管理內存,但和一般的NSObject的管理方式有些不同,稍后會說明。

二,Block語法

Block很像函數指針,這從Block的語法上就可以看出。Block的原型:

返回值 (^名稱)(參數列表)

Block的定義

^ 返回值類型 (參數列表) { 表達式 }

其中返回值類型和參數列表都可以省略,最簡單的Block就是:

^{ ; };

一般的定義就是:

返回值 (^名稱)(參數列表) = ^(參數列表){代碼段};

為了方便通常使用typedef定義:

typedef void (^blk) (void);

三,Block存儲域

Block能夠截獲自動變量,自動變量的當前值會被拷貝到棧上作為常量,此時不能在Block內對自動變量進行賦值操作,如果有這種需求,則需要該變量是:

1,靜態變量

2,全局變量

3,或者使用__block修飾符

根據Block中是否引用了自動變量,可以將Block存儲區域分類:

1,_NSConcreteStackBlock-存儲在棧上

2,_NSConcreteGlobalBlock-存儲在全局數據區域(和全局變量一樣)

3,_NSConcreteMallocBlock-存儲在堆上

沒 有引用自動變量或者在全局作用域的Block為_NSConcreteGlobalBlock,其他的基本上都是 _NSConcreteStackBlock。對_NSConcreteStackBlock執行copy操作會生成 _NSConcreteMallocBlock。

一般來說出問題的Block大部分都是_NSConcreteStackBlock,超過了_NSConcreteStackBlock的作用域_NSConcreteStackBlock就會銷毀。

四,對Block執行retain,copy方法的效果

Block是C語言的擴展,C語法也可以使用Block的語法,對應的C語言使用Block_copy,Block_release.

無論是_NSConcreteStackBlock,還是_NSConcreteGlobalBlock,執行retain都不起作用。而_NSConcreteMallocBlock執行retain引用計數+1。

對 于copy操作,_NSConcreteStackBlock會被復制到堆上得到新的_NSConcreteMallocBlock,而 _NSConcreteGlobalBlock執行copy操作不起作用。而對_NSConcreteMallocBlock執行copy操作會引起引用 計數加1。

那么就引出一個問題,對_NSConcreteMallocBlock多次copy會不會引起問題呢?參考Objective-C高級管理115頁。

五,什么時候要對NSConcreteStackBlock執行copy操作?

配 置在棧上的Block也就是NSConcreteStackBlock類型的Block,如果其所屬的變量作用域結束該Block就會廢棄。這個時候如果 繼續使用該Block,就應該使用copy方法,將NSConcreteStackBlock拷貝為_NSConcreteMallocBlock。當 _NSConcreteMallocBlock的引用計數變為0,該_NSConcreteMallocBlock就會被釋放。

如果是非ARC環境,需要顯式的執行copy或者antorelease方法。

而當ARC有效的時候,實際上大部分情況下編譯器已經為我們做好了,自動的將Block從棧上復制到堆上。包括以下幾個情況:

1,Block作為返回值時

類似在非ARC的時候,對返回值Block執行[[returnedBlock copy] autorelease];

2,方法的參數中傳遞Block時

3,Cocoa框架中方法名中還有useringBlock等時

4,GCD相關的一系列API傳遞Block時。

比如:[mutableAarry addObject:stackBlock];這段代碼在非ARC環境下肯定有問題,而在ARC環境下方法參數中傳遞NSConcreteStackBlock會自動執行copy。

六,Block的循環引用

對于非ARC下, 為了防止循環引用, 我們使用__block來修飾在Block中使用的對象:

對于ARC下, 為了防止循環引用, 我們使用__weak來修飾在Block中使用的對象。原理就是:ARC中,Block中如果引用了__strong修飾符的自動變量,則相當于Block對該變量的引用計數+1。

2.4 block中變量的復制與修改

對于block外的變量引用,block默認是將其復制到其數據結構中來實現訪問的,如下圖:

通過block進行閉包的變量是const的。也就是說不能在block中直接修改這些變量。來看看當block試著增加x的值時,會發生什么:

myBlock?=?^(?void?)

{

x++;

returnx;

};

編譯器會報錯,表明在block中變量x是只讀的。

有時候確實需要在block中處理變量,怎么辦?別著急,我們可以用__block關鍵字來聲明變量,這樣就可以在block中修改變量了。

基于之前的代碼,給x變量添加__block關鍵字,如下:

__block?int?x;

對于用__block修飾的外部變量引用,block是復制其引用地址來實現訪問的,如下圖:

3.編譯器中的block

3.1 block的數據結構定義

我們通過大師文章中的一張圖來說明:

上圖這個結構是在棧中的結構,我們來看看對應的結構體定義:

struct?Block_descriptor?{

unsigned?long?int?reserved;

unsigned?long?int?size;

void?(*copy)(void?*dst,?void?*src);

void?(*dispose)(void?*);

};

struct?Block_layout?{

void?*isa;

int?flags;

int?reserved;

void?(*invoke)(void?*,?...);

struct?Block_descriptor?*descriptor;

/*?Imported?variables.?*/

};

從上面代碼看出,Block_layout就是對block結構體的定義:

isa指針:指向表明該block類型的類。

flags:按bit位表示一些block的附加信息,比如判斷block類型、判斷block引用計數、判斷block是否需要執行輔助函數等。

reserved:保留變量,我的理解是表示block內部的變量數。

invoke:函數指針,指向具體的block實現的函數調用地址。

descriptor:block的附加描述信息,比如保留變量數、block的大小、進行copy或dispose的輔助函數指針。

variables:因為block有閉包性,所以可以訪問block外部的局部變量。這些variables就是復制到結構體中的外部局部變量或變量的地址。

3.2 block的類型

block有幾種不同的類型,每種類型都有對應的類,上述中isa指針就是指向這個類。這里列出常見的三種類型:

_NSConcreteGlobalBlock:全局的靜態block,不會訪問任何外部變量,不會涉及到任何拷貝,比如一個空的block。例如:

#include?int?main()

{

^{?printf("Hello,?World!\n");?}?();

return0;

}

_NSConcreteStackBlock:保存在棧中的block,當函數返回時被銷毀。例如:

#include?int?main()

{

char?a?='A';

^{?printf("%c\n",a);?}?();

return0;

_NSConcreteMallocBlock:保存在堆中的block,當引用計數為0時被銷毀。該類型的block都是由 _NSConcreteStackBlock類型的block從棧中復制到堆中形成的。例如下面代碼中,在 exampleB_addBlockToArray方法中的block還是_NSConcreteStackBlock類型的,在exampleB方法中 就被復制到了堆中,成為_NSConcreteMallocBlock類型的block:

void?exampleB_addBlockToArray(NSMutableArray?*array)?{

char?b?='B';

[array?addObject:^{

printf("%c\n",?b);

}];

}

void?exampleB()?{

NSMutableArray?*array?=?[NSMutableArray?array];

exampleB_addBlockToArray(array);

void?(^block)()?=?[array?objectAtIndex:0];

block();

}

總結一下:

_NSConcreteGlobalBlock類型的block要么是空block,要么是不訪問任何外部變量的block。它既不在棧中,也不在堆中,我理解為它可能在內存的全局區。

_NSConcreteStackBlock類型的block有閉包行為,也就是有訪問外部變量,并且該block只且只有有一次執行,因為棧中的空間是可重復使用的,所以當棧中的block執行一次之后就被清除出棧了,所以無法多次使用。

_NSConcreteMallocBlock類型的block有閉包行為,并且該block需要被多次執行。當需要多次執行時,就會把該block從棧中復制到堆中,供以多次執行。

3.3 編譯器如何編譯

我們通過一個簡單的示例來說明:

#import?typedef?void(^BlockA)(void);

__attribute__((noinline))

void?runBlockA(BlockA?block)?{

block();

}

void?doBlockA()?{

BlockA?block?=?^{

//?Empty?block

};

runBlockA(block);

}

上面的代碼定義了一個名為BlockA的block類型,該block在函數doBlockA中實現,并將其作為函數runBlockA的參數,最后在函數doBlockA中調用函數runBloackA。

注 意:如果block的創建和調用都在一個函數里面,那么優化器(optimiser)可能會對代碼做優化處理,從而導致我們看不到編譯器中的一些操作,所 以用__attribute__((noinline))給函數runBlockA添加noinline,這樣優化器就不會在doBlockA函數中對 runBlockA的調用做內聯優化處理。

我們來看看編譯器做的工作內容:

#import?__attribute__((noinline))

void?runBlockA(struct?Block_layout?*block)?{

block->invoke();

}

void?block_invoke(struct?Block_layout?*block)?{

//?Empty?block?function

}

void?doBlockA()?{

struct?Block_descriptor?descriptor;

descriptor->reserved?=?0;

descriptor->size?=?20;

descriptor->copy?=?NULL;

descriptor->dispose?=?NULL;

struct?Block_layout?block;

block->isa?=?_NSConcreteGlobalBlock;

block->flags?=?1342177280;

block->reserved?=?0;

block->invoke?=?block_invoke;

block->descriptor?=?descriptor;

runBlockA(&block);

}

上面的代碼結合block的數據結構定義,我們能很容易得理解編譯器內部對block的工作內容。

3.4 copy()和dispose()

上 文中提到,如果我們想要在以后繼續使用某個block,就必須要對該block進行拷貝操作,即從棧空間復制到堆空間。所以拷貝操作就需要調用 Block_copy()函數,block的descriptor中有一個copy()輔助函數,該函數在Block_copy()中執行,用于當 block需要拷貝對象的時候,拷貝輔助函數會retain住已經拷貝的對象。

既然有有copy那么就應該有release,與 Block_copy()對應的函數是Block_release(),它的作用不言而喻,就是釋放我們不需要再使用的block,block的 descriptor中有一個dispose()輔助函數,該函數在Block_release()中執行,負責做和copy()輔助函數相反的操作,例 如釋放掉所有在block中拷貝的變量等。

4.總結

以上內容是我學習各大師的文章后對自己學習情況的一個記錄,其中有部分文字和代碼示例是來自大師的文章,還有一些自己的理解,如有錯誤還請大家勘誤。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 前言 Blocks是C語言的擴充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了這...
    小人不才閱讀 3,786評論 0 23
  • 一、Objective-C發展史 Objective-C從1983年誕生,已經走過了30多年的歷程。隨著時間的推移...
    沒事蹦蹦閱讀 5,903評論 12 34
  • 《Objective-C高級編程》這本書就講了三個東西:自動引用計數、block、GCD,偏向于從原理上對這些內容...
    WeiHing閱讀 9,902評論 10 69
  • 目錄 Block底層解析什么是block?block編譯轉換結構block實際結構block的類型NSConcre...
    tripleCC閱讀 33,274評論 32 388
  • Block基礎回顧 1.什么是Block? 帶有局部變量的匿名函數(名字不重要,知道怎么用就行),差不多就與C語言...
    Bugfix閱讀 6,801評論 5 61