iOS 讀書筆記(2) - Block
Pro Multithreading And Memory Management for iOS and OS X
這次想寫的是這本書的第二章節 - Block 的介紹,
想先申明的是,在這篇裡你不會看到什麼高深的用法,這本書純粹就是介紹原理罷了。

好看的一部動畫,應該也會收BD XD
什麼是 Block ?
Block 是C語言的擴充功能,可以用一句話來表式Blocks的擴充功能。
帶有自動變數修飾符(區域變數)的匿名函數。
顧名思義,所謂匿名函數就是不帶有名稱的函數。 標準C語言是不允許這樣的函數。
所謂匿名函數在其他語言的名稱
程式語言 | Block的名稱 |
---|---|
C+ Blocks | Block |
Smalltalk | Block |
Ruby | Block |
LISP | Lambda |
Python | Lambda |
C++ 11 | Lambda |
Javascript | Anonumous function |
Block 語法
^(int event) {
printf("int:%d",event);
}
實際上,該Block使用了省略方式,完整如下,
^void (int event) {
printf("int:%d",event);
}
如上所示,完整形式的Block語法與一般C語言函數定義相比,僅有兩點不同。
- 沒有函數名。
- 帶有”^”。
以下為Block 語法的BN范式。
Block_literal_expression :: = ^ block_decl compound_statement_body
block_decl ::=
block_decl ::= parameter_list
block_decl ::= type_expression
即使不了解BN范式,通過說明也能有個概念^
返回值類型
參數列表
表達式
可以寫出如下的Block語法
^int (int count) {return count + 1;}
但Block語法是可以省略好幾個項目的,首先是返回值類型
省略返回值類型時,如果表達式中有return時就使用該返回值的類息,沒有就使用void類型,表達式中有多個return 語句時,所有return的返回值類型必須相同。
Block 類型變數
block類型變數與一般C語言變數完全相同,可作為以下用途使用。
- 自動變數(區域變數)
- 函數參數
- 靜態變數
- 靜態全域變數
- 全域變數
那麼,我們就式著使用Block語法將Block賦值給Block變數。
int (^blk)(int) = ^(int count){return count +1;}
因為變數相同所以blk可賦值給blk1
int (^blk1)(int) = blk;
int (^blk2)(int)
blk2 = blk;
但當函數參數或返回值中使用Block變數時,既難寫又難記,這時,我們可以使用typedef 來解決問題。typedef int (^blk_t)int;
這時我們就可使用blk_t 來代替整個block變數。通過使用typedef,函數定義變的更容易理解了。
另外Block 也可以像C語言用指標的方式去使用,這邊就不細講了。
截獲自動變數值
一開始我有說明Block是帶有”自動變數的匿名函數”,那什麼是帶有自動變數值呢?舉個例子
int main()
{
int dmy = 256;
int val = 10;
const char *fmt = "val = %d \n";
void (^blk)(void) = ^{printf(fmt,val);};
val = 2;
fmt = "These values were changed val = %d \n";
blk();
return 0;
}
執行結果會是 val = 10
執行結果並不是改寫後的值,而是執行Block語法時的自動變數的瞬間值。該Block語法在執行時,字符串指標”cal = %d \n”被賦值到自動變數fmt中,int 值被賦值到val 中,因此這邊值被保存(即被截獲),而在執行時被使用。
這就是自動變數值的截獲。
__block 說明符
實際上,截獲變數只能保存Block語法的瞬間值,保存後就不能改寫該值。
這時,若想在Block語法的表達式中將值賦給Block語法外聲明的自動變數,需要在該自動變數上附加__block。
使用附有__block
說明符的自動變數可在Block中賦值,該變數稱為__block變數
。
Blocks的實現
Block的實質
Block 始帶有自動變數得匿名函數,但Block究竟是什麼呢? 下面將通過Block的實現進一步來了解。書中使用了把Objective -C的程式碼編譯成了C語言程式碼,我盡量以我了解的情況說明
當你在.m檔中輸入如下語法
int main()
{
void (^blk)(void) = ^{printf("Block\n");};
blk();
return 0;
}
在command Line中執行clang -rewrite-objc 文件名
發現會編譯成.cpp檔 打開後最下面main開始就是你的源始碼。
我們來看看棧上的__main_block_impl_0
結構體實例(即Block)是如何根據這些參數進行初始化的。 如果展開__main_block_impl_0
結構體的__block_impl
結構體,可記成以下形式:
struct __main_block_impl_0 {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0* Desc;
}
該結構體根據構造函數會像下面這樣進行初始化。
isa = &_NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
什麼是isa = &_NSConcreteStackBlock
呢?
將Block 指標賦給Block結構體成員isa。為了理解它,首先要理解Objective-C類和對象的實質。其實,Block就是Objective-C的對象。
“id”這一變數類型用於存儲Objective-C對象,在Objective-C源代碼中,雖然可以像使用void*類型那樣隨意使用id,但此id類型也能夠在C語言中聲名,在/usr/unclude/objc/runtime.h中是如下聲明的:
typedef struct objc_object {
Class isa;
} *id;
id為objc_object結構體的指標類型,我們再來看看Class。
typedef struct objc_class *Class;
Class為objc_class結構體的指標類型, objc_class又是如下聲明
struct objc_class {
Class isa;
};
這與objc_object結構體相同。然而,objc_object和objc_class歸根究底是在各個對象和類的實現中使用的最基本的結構體。
@interface MyObject : NSObject
{
int cal0;
int val1;
}
@end
基於objc_object結構體,該類的對象結構體如下:
struct MyObject {
Class isa;
int val0;
int val1;
};
意味著,生成的各個對象即由該類生成的對象的各個結構體實例,通過成員變量isa保持該類的結構體實例指標。 如圖所示

在Objcetive-C中,比如NSObject 的class_t結構體實例或是NSmutableArray的class_t結構體實例等,均生成並保持各個類的屬性以及父類的指標,並被Objcetive-C運行時庫所使用。
到這裡,就可以理解Objective-C的類與對象的實質了。
回到剛才的Block結構體。
isa = &_NSConcreteStackBlock;
此_NSConcreteStackBlock相當於class_t的結構體實例。在將Block作為Objective-C類對象處理時,關於該類的信息放置於_NSConcreteStackBlock中。
現在終於能理解Block的實質,知道Block即為Objective-C的對象了。
__block 說明符
前述我們知截取的自動變數在Block中改變其值時,會產生編譯錯誤。
解決這個問題的方法有兩種。
第一種:C語言中有一個變數,允許Block改寫值,具體如下:
- 靜態變數
- 靜態全域變數
全域變數
雖然Block語法的匿名函數部份簡單地變換了C語言函數,但從這個變換的函數中訪問靜態全域變數/全域變數 並沒有任何改變,可直接使用。
但是靜態變數的情況下,轉換後的函數原本就設置在含有Block語法的函數外,所以無法從變數作用域訪問。,
解決Block中不能保存值這種問題的第二種方法是使用 “__block
說明符”
__block說明符類似於static,auto和register,它們用於將變數值設置到哪個存儲域中。
Block存儲域
Block 與 __block 變數的實質
名稱 | 實質 |
---|---|
Block | 棧上Block結構體實例 |
__block變數 | 棧上__block變數的結構體實例 |
通過之前的說明可知Block也是Objective-C對象。將Block當作Objective-C對象來看時,該Block為_NSConcreteStackBlock。
雖然該類並沒有出現在已變換的源代碼中,但有很多與之類似的類,如
- _NSConcreteStackBlock
- _NSConcreteGlobalBlock
- _NSConcreteMallocBlock
其名稱與內容可整理如下
類 | 設置對象的存儲域 |
---|---|
_NSConcreteStackBlock | 棧(stack) |
_NSConcreteGlobalBlock | 程序的數據區域(Data區) |
_NSConcreteMallocBlock | 推(heap) |
雖然我們在很多地方都看到的是_NSConcreteStackBlock
但有兩種情況下Block是_NSConcreteGlobalBlock
- 記述全局變量的地方有Block語法時
- Block語法的表達式中不使用應截獲的自動變數時
那_NSConcreteMallocBlock呢?
設置在stack中的Block在作用域完後就會被廢棄,因為__block也配置在stack上所以同時也會被廢棄。
Block提供了將Block和__block變數從棧上複製到推上的方法來解決這個問題。既使stack上的作用域已結束,heap上的Block依舊存在。
這時isa = &_NSConcreteMallocBlock;
而__block
變數結構體成員中有個forwarding 可以實現無論`block在stack或是heap上都能正卻地訪問
__block變數`。
在大多數情形下,編譯器會恰當地進行判斷是否該複製Block,自動生成將Block殂棧上複製到推上的代碼。我們來看個例子
typedef int (^blk_t)(int);
blk_t func(int rate)
{
return ^(int count){return rate * count;};
}
可轉換如下
blk_t func(int rate)
{
blk_t tmp = &__func_block_impl_0(
__func_block_0, &__func_block_desc_0_DATA, rate);
tmp = objc_retainBlock(tmp);
return objc_autoreleaseReturnValue(tmp);
}
因為在ARC下,所以blk_t是附有__strong修飾符的。
objc_retainBlock實際上就是_Block_copy函數。
所以將Block作為函數返回值返回時,編譯器會自動生成複製到heap的代碼。
前面說過大多數情況下編譯器會自動幫我們生成代碼,另外則是需我們手動使用copy方法。那什麼情況下需要手動Copy呢
- 向方法或函數的參數中傳遞Block時
但是如果在方法或函數中適當地複製了傳遞過來的參數,那麼就不必在調用該方法或函數前手動複製了。 以下方法不用手動複製;
- Cocoa框架的方法且方法名中含有usingBlock等時。
- Grant Central Dispatch 的API。
|
|
|
|
此代碼在執行blk()時就會當掉,因為getBlockArray結束時,棧上的Block被廢棄的緣故,可惜此時編譯器不能判斷是否需要複製,當然你也可以用讓編譯器進行判斷自行手動COPY,但是從stack到heap是會耗CPU資源的,過多的copy只是在消耗cpu資源,因此只在此情形下手動進行複製。
經由自己測試,執行時,並不會當在blk(),網路查了許多資料,
以我個人觀點,因為當成return值返回時,array被帶有
autoreleasePool中, array裡的block前面有說過,
當block被當作參數傳遞時不能被判斷,但是當作返回值時,
會進行_Block_copy,所以這邊element 0 會被複製到heap中
執行是沒問題 有值可以出現,但是當出了了id的作用域時,
被釋放裡面的element 1 因為編譯器誤判,沒有複製到heap上,
所以雙重release ,造成程式Crash。
另外,對於已配置在heap的Block以及配置在Data區的Block調用copy又會如何呢?Block的副本
Block的類 | 副本源的配致存儲域 | 複製效果 |
---|---|---|
_NSConcreteStackBlock | 棧(stack) | 從棧複製到推 |
_NSConcreteGlobalBlock | 程序讀數據區域 | 什麼也不做 |
_NSConcreteMallocBlock | 推(heap) | 引用計數增加 |
那麼如果多次調用copy對同一個Block時有沒有問題呢?答案是沒有問題的。
請各位自行想想__strong 賦值後的情況即可得知一二。
截穫對象
以下程式碼生成並持有NSMutableArray對象,由于附有__strong的賦值目標變數的作用域立即結束,因此對象被立即釋放並廢棄。
{
id array = [[NSMUtableArray alloc]init];
}
我們來看一下在Block語法中使用該變數array的程式碼。
blk_t blk;
{
id array = [[NSMUtableArray alloc]init];
blk = [^(id obj) {
[array addObject:obj];
NSLog(@"array cpunt = %ld",[array count]);
}copy];
}]
blk([[NSObject alloc]init]);
blk([[NSObject alloc]init]);
blk([[NSObject alloc]init]);
變數作用域結束的同時,變數array被廢棄,其強引用失效,因此賦值給array的NSMurableArray被定被釋放。 但是該程式碼運行正常,其結果如下
array count = 1;
array count = 2;
array count = 3;
這一結果意味著賦值給變數array類的對象在該程式碼最後Block的執行部份超出其變數作用域而存在。
為什麼呢,還記得我們說過copy方法會把Block從stack複製到heap。
在Block從stack -> heap時會調用Copy函數,從heap廢棄時會調用dispose函數。
那麼什麼時候棧上的Block會複製到推上呢?
- 調用Block的Copy實例方法時
- Block作為函數返回值返回時
- 將Block賦值給附有__strong修飾符id類型或Block類型成員變數時
- 在方法明中含有usingBlock的cocoa方法或GCD的API傳遞Block時
也就是說,雖然從源代碼來看,在上面這些情況下棧上的Block被複製到推上,但其實可歸結為_Block_copy函數被調用時Block從棧複製到推。
鄉對的,在釋放複製到推上的Block後,誰都不持有Block而使其被廢棄時調用dispose函數,這相當於對象的dealloc方法。
有了惡些構造,通過使用附有__strong修飾符的自動變數,Block中截取的對象就能夠超出其變數作用域而存在。
那麼在剛才的程式碼中,如果不掉用copy時又會如何呢?
執行該程式碼後,程式會強制結束。
因為只有調用_Block_copy函數才能持有截獲附有__strong的對象類型的自動變數值,所以不進行copy情況下,即使截獲饹為象,它也會隨著變數作用域的結束而被廢棄。
因此,Block中使用對象類型自動變數時,除以下情況,推毽使用copy實例方法。
- Block作為函數返回值返回時
- 將Block賦值給附有__strong修飾符id類型或Block類型成員變數時
- 在方法明中含有usingBlock的cocoa方法或GCD的API傳遞Block時
__block變量和對象
前面說過當stack -> heap時除了Block外還有__block
變數也會被複製到heap上,即使對象賦值複製到推上的附有__strong
的對象類型__block
變數中,只要__block在推上繼續存在,那麼該對象就會繼續處於被持有狀態。
這與Block中使用賦值給附有__strong的對象類型自動變數的對象相同。
前例,若用__weak修飾符時會發生什麼事呢?
程式碼一樣可正常運行但是結果確都是 0 。
這是由於附有__strong
的變數array在該變數作用域結束時同時被釋放,廢棄 nil被賦值在附有__weak的變數中。
若同時指定block , weak時會怎樣呢?
理由同__weak
,即使加__block
說明符,附有__strong
的變量array也會再該作用域結束被釋放,nil同時會指給__weak指向的變數。
Block循環引用
如果在Block中使用附有__strong的對象類型自動變量,那麼當Block從棧複製到推時,該對象為Block所持有。這樣容易引起循環引用。 我們來看個例子
typedef void (^blk_t)(void);
@interface MyObject : NSObject
{
blk_t blk;
}
@end
@implementation MyObject
- (id)init
{
self = [super init];
blk = ^{NSLog(@"self = %@", self);};
}
- (void)dealloc
{
NSLog(@"dealloc");
}
@end
int main()
{
id a =[[MyObject alloc]init];
NSlog(@"%@",a);
return 0;
}
該程式碼中dealloc方法一定不會被調用。
為避免此循環引用,可聲明附有__weak修飾符的變數,並將self賦值使用。
- (id)init
{
self = [super init];
id __weak tmp = self;
blk = ^{NSLog(@"self = %@", tmp);};
}
這樣即可打破循環引用,因為我們用了弱引用來打斷引用的方式。
另外,以下程式碼沒有使用self一樣引起了循環引用。
|
|
光這樣打時,編譯器就會出現警告,
及Block語法內使用的obj實際上截獲了self。對編譯器來說,obj只不過是對象用結構體的成員變量blk = ^{NSLog(@"obj = %@", self->obj);};
另外,還可以使用__block來避免循環引用
這段代碼沒有引起循環引用,但是如果不調用execBlock實例方法時,即不執行賦值給成員變數blk的Block,便會循環引用並引起內存洩漏。
下面我們對使用__block
變數避免循環引用的方法和使用__weak
及__unsafe_unretained修飾符避免循環引用做個比較。
使用__block變數的優點如下
- 通過__block變數可控制對象的持有期間。
- 在不能使用
__weak
的環境中事unsafe_unretained修飾符即可。在直行Block時可動態地決定是否將nil或其它對象賦值在block變數中。
使用__block缺點如下
- 為了避免循環引用必須執行Block
存在值行Block語法,卻不執行Block的路徑時,無法避免循環引用。若由於Block引發了循環引用時,跟據Block的用途選擇使用 __block
, __weak
, _unsafe_unretained
修飾符來避免循環引用。