iOS Block用法和實現原理
《Objective-C高級編程》是一本有趣又難懂的書,全書就講了引用計數、Block、GCD三個概念,有趣是因為講原理、實現的部分是其它iOS專業書籍里少有的。然而每個章節不讀個三五遍還是比較難理解貫通的。本文針對其中的Block部分做些簡單的筆記記錄,講述Block的用法和部分實現原理,詳細解說從原書中尋。
Block概要
Block:帶有 自動變量 的 匿名函數 。
匿名函數:沒有函數名的函數,一對{}包裹的內容是匿名函數的作用域。
自動變量:棧上聲明的一個變量不是靜態變量和全局變量,是不可以在這個棧內聲明的匿名函數中使用的,但在Block中卻可以。
雖然使用Block不用聲明類,但是Block提供了類似Objective-C的類一樣可以通過成員變量來 保存作用域外變量值 的方法,那些在Block的一對{}里使用到但卻是在{}作用域以外聲明的變量,就是Block截獲的自動變量。
Block常規概念
Block語法
Block表達式語法:
^ 返回值類型 (參數列表) {表達式}
例如:
^ int (int count) {
return count + 1;
};
其中,可省略部分有:
- 返回類型,例:
^ (int count) { return count + 1; };
- 參數列表為空,則可省略,例:
^ { NSLog(@"No Parameter"); };
^ {表達式}
Block類型變量
聲明Block類型變量語法:
返回值類型 (^變量名)(參數列表) = Block表達式
例如,如下聲明了一個變量名為blk的Block:
int (^blk)(int) = ^(int count) {
return count + 1;
};
當Block類型變量作為函數的參數時,寫作:
- (void)func:(int (^)(int))blk {
NSLog(@"Param:%@", blk);
}
借助typedef可簡寫:
typedef int (^blk_k)(int);
- (void)func:(blk_k)blk {
NSLog(@"Param:%@", blk);
}
Block類型變量作返回值時,寫作:
- (int (^)(int))funcR {
return ^(int count) {
return count ++;
};
}
借助typedef簡寫:
typedef int (^blk_k)(int);
- (blk_k)funcR {
return ^(int count) {
return count ++;
};
}
截獲自動變量值
Block表達式可截獲所使用的自動變量的值。
截獲:保存自動變量的 瞬間值 。
因為是“瞬間值”,所以聲明Block之后,即便在Block外修改自動變量的值,也不會對Block內截獲的自動變量值產生影響。
例如:
int i = 10;
void (^blk)(void) = ^{
NSLog(@"In block, i = %d", i);
};
i = 20;//Block外修改變量i,也不影響Block內的自動變量
blk();//i修改為20后才執行,打印: In block, i = 10
NSLog(@"i = %d", i);//打印:i = 20
__block說明符號
自動變量截獲的值為Block聲明時刻的瞬間值,保存后就不能改寫該值,如需對自動變量進行重新賦值,需要在變量聲明前附加__block說明符,這時該變量稱為__block變量。
例如:
__block int i = 10;//i為__block變量,可在block中重新賦值
void (^blk)(void) = ^{
NSLog(@"In block, i = %d", i);
};
i = 20;
blk();//打印: In block, i = 20
NSLog(@"i = %d", i);//打印:i = 20
自動變量值為一個對象情況
當自動變量為一個類的 對象 ,且沒有使用__block修飾時,雖然不可以在Block內對該變量進行重新賦值,但可以修改該對象的屬性。
如果該對象是個Mutable的對象,例如NSMutableArray,則還可以在Block內對NSMutableArray進行元素的增刪:
NSMutableArray *array = [[NSMutableArray alloc] initWithObjects:@"1", @"2",nil ];
NSLog(@"Array Count:%ld", array.count);//打印Array Count:2
void (^blk)(void) = ^{
[array removeObjectAtIndex:0];//Ok
//array = [NSNSMutableArray new];//沒有__block修飾,編譯失敗!
};
blk();
NSLog(@"Array Count:%ld", array.count);//打印Array Count:1
Block實現原理
使用Clang
Block實際上是作為極普通的 C語言源碼 來處理的:含有Block語法的源碼首先被轉換 成C語言編譯器能處理的源碼 ,再作為普通的C源代碼 進行編譯 。
使用LLVM編譯器的clang命令可將含有Block的Objective-C代碼轉換成C++的源代碼,以探查其具體實現方式:
clang -rewrite-objc 源碼文件名
注:如果使用該命令報錯: ’UIKit/UIKit.h’ file not found 。
Block結構
使用Block的時候,編譯器對Block語法進行了怎樣的轉換?
int main() {
int count = 10;
void (^ blk)() = ^(){
NSLog(@"In Block:%d", count);
};
blk();
}
如上所示的最簡單的Block使用代碼,經clang轉換后,可得到以下幾個部分(有代碼刪減和注釋添加):
static void __main_block_func_0(
struct __main_block_impl_0 *__cself) {
int count = __cself->count; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_d2f8d2_mi_0,
count);
}
這是一個函數的實現,對應Block中{}內的內容,這些內容被當做了C語言函數來處理,函數參數中的 __cself 相當于Objective-C中的self。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc; //描述Block大小、版本等信息
int count;
//構造函數函數
__main_block_impl_0(void *fp,
struct __main_block_desc_0 *desc,
int _count,
int flags=0) : count(_count) {
impl.isa = &_NSConcreteStackBlock; //在函數棧上聲明,則為_NSConcreteStackBlock
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__main_block_impl_0即為 main()函數棧上的Block結構體 ,其中的__block_impl結構體聲明如下:
struct __block_impl {
void *isa;//指明對象的Class
int Flags;
int Reserved;
void *FuncPtr;
};
__block_impl結構體,即為Block的結構體,可理解為 Block的類結構 。
再看下main()函數翻譯的內容:
int main() {
int count = 10;
void (* blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
去除掉復雜的類型轉化,可簡寫為:
int main() {
int count = 10;
sturct __main_block_impl_0 *blk = &__main_block_impl_0(__main_block_func_0, //函數指針
&__main_block_desc_0_DATA)); //Block大小、版本等信息
(*blk->FuncPtr)(blk); //調用FuncPtr指向的函數,并將blk自己作為參數傳入
}
由此,可以看出, Block也是Objective-C中的對象 。
Block有三種類(即__block_impl的 isa 指針指向的值,根據Block對象創建時 所處數據區不同 而進行區別:
- _NSConcreteStackBlock :在棧上創建的Block對象
- _NSConcreteMallocBlock :在堆上創建的Block對象
- _NSConcreteGlobalBlock :全局數據區的Block對象
如何截獲自動變量
上部分介紹了 Block的結構 ,和 作為匿名函數的調用機制 ,那 自動變量截獲 是發生在什么時候呢?
觀察上節代碼中 __main_block_impl_0 結構體(main棧上Block的結構體)的構造函數可以看到,棧上的變量count以參數的形式傳入到了這個構造函數中,此處即為 變量的自動截獲 。
因此可以這樣理解: __block_impl 結構體已經可以代表Block類了,但在棧上又聲明了 __main_block_impl_0 結構體,對 __block_impl 進行 封裝 后才來表示棧上的 Block類 ,就是為了獲取Block中使用到的棧上聲明的變量( 棧上沒在Block中使用的變量不會被捕獲 ),變量被保存在Block的結構體實例中。
所以在blk()執行之前,棧上簡單數據類型的count無論發生什么變化,都不會影響到Block以參數形式傳入而捕獲的值。但這個變量是指向對象的指針時,是可以修改這個對象的屬性的,只是不能為變量重新賦值。
Block的存儲域
上文已提到,根據Block創建的位置不同,Block有三種類型,創建的Block對象分別會存儲到棧、堆、全局數據區域。
void (^blk)(void) = ^{
NSLog(@"Global Block");
};
int main() {
blk();
NSLog(@"%@",[blk class]);//打印:__NSGlobalBlock__
}
像上面代碼塊中的全局blk自然是存儲在全局數據區,但注意在 函數棧上 創建的blk,如果 沒有截獲自動變量 ,Block的結構實例還是會被設置在程序的 全局數據區,而非棧上 :
int main() {
void (^blk)(void) = ^{//沒有截獲自動變量的Block
NSLog(@"Stack Block");
};
blk();
NSLog(@"%@",[blk class]);//打印:__NSGlobalBlock__
int i = 1;
void (^captureBlk)(void) = ^{//截獲自動變量i的Block
NSLog(@"Capture:%d", i);
};
captureBlk();
NSLog(@"%@",[captureBlk class]);//打印:__NSMallocBlock__
}
可以看到截獲了自動變量的Block打印的類是 NSGlobalBlock ,表示存儲在全局數據區。
但為什么捕獲自動變量的Block打印的類卻是設置在堆上的 NSMallocBlock ,而非棧上的 NSStackBlock ?這個問題稍后解釋。
Block復制
配置在棧上的Block,如果其所屬的棧作用域結束,該Block就會被廢棄,對于超出Block作用域仍需使用Block的情況,Block提供了 將Block從棧上復制到堆上的方法 來解決這種問題,即便Block棧作用域已結束,但被拷貝到堆上的Block還可以繼續存在。
復制到堆上的Block, 將_NSConcreteMallocBlock 類對象寫入Block結構體實例的成員變量isa:
impl.isa = &_NSConcreteMallocBlock;
在ARC有效時,大多數情況下編譯器會進行判斷,自動生成將Block從棧上復制到堆上的代碼,以下幾種情況 棧上的Block會自動復制到堆上 :
- 調用Block的copy方法
- 將Block作為函數返回值時
- 將Block賦值給__strong修改的變量時
- 向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block參數時
其它時候向方法的參數中傳遞Block時,需要手動調用copy方法復制Block。
上一節的棧上截獲了自動變量i的Block之所以在棧上創建,卻是 NSMallocBlock_ 類,就是因為這個Block對象賦值給了 _strong修飾的變量 captureBlk(
strong是ARC下對象的默認修飾符)。
因為上面四條規則,在ARC下其實很少見到_NSConcreteStackBlock類的Block,大多數情況編譯器都保證了Block是在堆上創建的,如下代碼所示,僅最后一行代碼直接使用一個不賦值給變量的Block,它的類才是__NSStackBlock
:
int count = 0;
blk_t blk = ^(){
NSLog(@"In Stack:%d", count);
};
NSLog(@"blk's Class:%@", [blk class]);//打印:blk's Class:__NSMallocBlock__
NSLog(@"Global Block:%@", [^{NSLog(@"Global Block");} class]);//打印:Global Block:__NSGlobalBlock__
NSLog(@"Copy Block:%@", [[^{NSLog(@"Copy Block:%d",count);} copy] class]);//打印:Copy Block:__NSMallocBlock__
NSLog(@"Stack Block:%@", [^{NSLog(@"Stack Block:%d",count);} class]);//打印:Stack Block:__NSStackBlock__
另外,原書存在ARC和MRC混合講解、區分不明的情況,比如書中幾個使用到棧上對象導致Crash的例子是MRC條件下才會發生的,但書中沒做特殊說明。
使用__block發生了什么
Block捕獲的自動變量添加__block說明符,就可在Block內讀和寫該變量,也可以在原來的棧上讀寫該變量。
自動變量的截獲保證了棧上的自動變量被銷毀后,Block內仍可使用該變量。
__block保證了棧上和Block內(通常在堆上)可以訪問和修改 “同一個變量” ,__block是如何實現這一功能的?
__block發揮作用的 原理 :將棧上用__block修飾的自動變量 封裝成一個結構體 ,讓其在堆上創建,以方便從棧上或堆上訪問和修改同一份數據。
驗證過程:
現在對剛才的代碼段,加上__block說明符,并在block內外讀寫變量count。
int main() {
__block int count = 10;
void (^ blk)() = ^(){
count = 20;
NSLog(@"In Block:%d", count);//打印:In Block:20
};
count ++;
NSLog(@"Out Block:%d", count);//打印:Out Block:11
blk();
}
將上面的代碼段clang,發現Block的結構體 __main_block_impl_0 結構如下所示:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_count_0 *count; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_count_0 *_count, int flags=0) : count(_count->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
最大的變化就是count變量不再是int類型了,count變成了一個指向 __Block_byref_count_0 結構體的指針, __Block_byref_count_0 結構如下:
struct __Block_byref_count_0 {
void *__isa;
__Block_byref_count_0 *__forwarding;
int __flags;
int __size;
int count;
};
它保存了int count變量,還有一個指向 __Block_byref_count_0 實例的指針 __forwarding ,通過下面兩段代碼 __forwarding 指針的用法可以知道,該指針其實指向的是對象自身:
//Block的執行函數
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_count_0 *count = __cself->count; // bound by ref
(count->__forwarding->count) = 20;//對應count = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_fafeeb_mi_0,
(count->__forwarding->count));
}
//main函數
int main() {
__attribute__((__blocks__(byref))) __Block_byref_count_0 count = {(void*)0,
(__Block_byref_count_0 *)&count,
0,
sizeof(__Block_byref_count_0),
10};
void (* blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0,
&__main_block_desc_0_DATA,
(__Block_byref_count_0 *)&count,
570425344));
(count.__forwarding->count) ++;//對應count ++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_fafeeb_mi_1,
(count.__forwarding->count));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
為什么要通過__forwarding指針完成對count變量的讀寫修改?
為了保證無論是在棧上還是在堆上,都能通過都__forwarding指針找到在堆上創建的count這個 main_block_func_0結構體,以完成對count->count(第一個count是 main_block_func_0對象,第二個count是int類型變量)的訪問和修改。
示意圖如下:
來自:http://www.jianshu.com/p/d28a5633b963