Block概要
Block:帶有自動變量的匿名函數。
匿名函數:沒有函數名的函數,一對{}包裹的內容是匿名函數的作用域。
自動變量:棧上聲明的一個變量不是靜態變量和全局變量,是不可以在這個棧內聲明的匿名函數中使用的,但在Block中卻可以。
雖然使用Block不用聲明類,但是Block提供了類似Objective-C的類一樣可以通過成員變量來保存作用域外變量值的方法,那些在Block的一對{}里使用到但卻是在{}作用域以外聲明的變量,就是Block截獲的自動變量。
Block常規概念
Block語法
Block表達式語法:
^ 返回值類型 (參數列表) {表達式}
例如:
^int(intcount) {returncount +1;? ? };
其中,可省略部分有:
返回類型,例:
^ (intcount) {returncount +1;? ? };
參數列表為空,則可省略,例:
^ {NSLog(@"No Parameter");? ? };
即最簡模式語法為:
^ {表達式}
Block類型變量
聲明Block類型變量語法:
返回值類型 (^變量名)(參數列表) = Block表達式
例如,如下聲明了一個變量名為blk的Block:
int(^blk)(int) = ^(intcount) {returncount +1;? ? };
當Block類型變量作為函數的參數時,寫作:
- (void)func:(int(^)(int))blk {NSLog(@"Param:%@", blk);}
借助typedef可簡寫:
typedefint(^blk_k)(int);- (void)func:(blk_k)blk {NSLog(@"Param:%@", blk);}
Block類型變量作返回值時,寫作:
- (int(^)(int))funcR {return^(intcount) {returncount ++;? ? };}
借助typedef簡寫:
typedefint(^blk_k)(int);- (blk_k)funcR {return^(intcount) {returncount ++;? ? };}
截獲自動變量值
Block表達式可截獲所使用的自動變量的值。
截獲:保存自動變量的瞬間值。
因為是“瞬間值”,所以聲明Block之后,即便在Block外修改自動變量的值,也不會對Block內截獲的自動變量值產生影響。
例如:
inti =10;void(^blk)(void) = ^{NSLog(@"In block, i = %d", i);? ? };? ? i =20;//Block外修改變量i,也不影響Block內的自動變量blk();//i修改為20后才執行,打印: In block, i = 10NSLog(@"i = %d", i);//打印:i = 20
__block說明符號
自動變量截獲的值為Block聲明時刻的瞬間值,保存后就不能改寫該值,如需對自動變量進行重新賦值,需要在變量聲明前附加__block說明符,這時該變量稱為__block變量。
例如:
__blockinti =10;//i為__block變量,可在block中重新賦值void(^blk)(void) = ^{NSLog(@"In block, i = %d", i);? ? };? ? i =20;? ? blk();//打印: In block, i = 20NSLog(@"i = %d", i);//打印:i = 20
自動變量值為一個對象情況
當自動變量為一個類的對象,且沒有使用__block修飾時,雖然不可以在Block內對該變量進行重新賦值,但可以修改該對象的屬性。
如果該對象是個Mutable的對象,例如NSMutableArray,則還可以在Block內對NSMutableArray進行元素的增刪:
NSMutableArray*array = [[NSMutableArrayalloc] initWithObjects:@"1",@"2",nil];NSLog(@"Array Count:%ld", array.count);//打印Array Count:2void(^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,可參考《Objective-C編譯成C++代碼報錯》解決。
Block結構
使用Block的時候,編譯器對Block語法進行了怎樣的轉換?
intmain(){intcount =10;void(^ blk)() = ^(){? ? ? ? NSLog(@"In Block:%d", count);? ? };? ? blk();}
如上所示的最簡單的Block使用代碼,經clang轉換后,可得到以下幾個部分(有代碼刪減和注釋添加):
staticvoid__main_block_func_0(struct__main_block_impl_0 *__cself) {intcount = __cself->count;// bound by copyNSLog((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_implimpl;struct__main_block_desc_0*Desc;//描述Block大小、版本等信息intcount;//構造函數函數__main_block_impl_0(void*fp,? ? ? ? ? struct __main_block_desc_0 *desc,int_count,intflags=0) : count(_count) {? ? impl.isa = &_NSConcreteStackBlock;//在函數棧上聲明,則為_NSConcreteStackBlockimpl.Flags = flags;? ? impl.FuncPtr = fp;? ? Desc = desc;? }};
__main_block_impl_0即為main()函數棧上的Block結構體,其中的__block_impl結構體聲明如下:
struct__block_impl{void*isa;//指明對象的ClassintFlags;intReserved;void*FuncPtr;};
__block_impl結構體,即為Block的結構體,可理解為Block的類結構。
再看下main()函數翻譯的內容:
intmain(){intcount =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);}
去除掉復雜的類型轉化,可簡寫為:
intmain(){intcount =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指針指向的值,isa說明參考《Objective-C isa 指針 與 runtime 機制》),根據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");};intmain() {? ? blk();NSLog(@"%@",[blkclass]);//打印:__NSGlobalBlock__}
像上面代碼塊中的全局blk自然是存儲在全局數據區,但注意在函數棧上創建的blk,如果沒有截獲自動變量,Block的結構實例還是會被設置在程序的全局數據區,而非棧上:
intmain() {void(^blk)(void) = ^{//沒有截獲自動變量的BlockNSLog(@"Stack Block");? ? };? ? blk();NSLog(@"%@",[blkclass]);//打印:__NSGlobalBlock__inti =1;void(^captureBlk)(void) = ^{//截獲自動變量i的BlockNSLog(@"Capture:%d", i);? ? };? ? captureBlk();NSLog(@"%@",[captureBlkclass]);//打印:__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:
intcount =0;? ? blk_t blk = ^(){NSLog(@"In Stack:%d", count);? ? };NSLog(@"blk's Class:%@", [blkclass]);//打印: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下Block自動copy的區別,查看《Block 小測驗》里幾道題目就能區分了。
另外,原書存在ARC和MRC混合講解、區分不明的情況,比如書中幾個使用到棧上對象導致Crash的例子是MRC條件下才會發生的,但書中沒做特殊說明。
使用__block發生了什么
Block捕獲的自動變量添加__block說明符,就可在Block內讀和寫該變量,也可以在原來的棧上讀寫該變量。
自動變量的截獲保證了棧上的自動變量被銷毀后,Block內仍可使用該變量。
__block保證了棧上和Block內(通常在堆上)可以訪問和修改“同一個變量”,__block是如何實現這一功能的?
__block發揮作用的原理:將棧上用__block修飾的自動變量封裝成一個結構體,讓其在堆上創建,以方便從棧上或堆上訪問和修改同一份數據。
驗證過程:
現在對剛才的代碼段,加上__block說明符,并在block內外讀寫變量count。
intmain() {? ? __blockintcount =10;void(^ blk)() = ^(){? ? ? ? count =20;NSLog(@"In Block:%d", count);//打印:In Block:20};? ? count ++;NSLog(@"Out Block:%d", count);//打印:Out Block:11blk();}
將上面的代碼段clang,發現Block的結構體__main_block_impl_0結構如下所示:
struct__main_block_impl_0{struct__block_implimpl;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,intflags=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;intcount;};
它保存了int count變量,還有一個指向__Block_byref_count_0實例的指針__forwarding,通過下面兩段代碼__forwarding指針的用法可以知道,該指針其實指向的是對象自身:
//Block的執行函數staticvoid __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函數intmain() {? ? __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類型變量)的訪問和修改。
示意圖如下:
Block的循環引用
Block的循環引用原理和解決方法大家都比較熟悉,此處將結合上文的介紹,介紹一種不常用的解決Block循環引用的方法和一種借助Block參數解決該問題的方法。
Block循環引用原因:一個對象A有Block類型的屬性,從而持有這個Block,如果Block的代碼塊中使用到這個對象A,或者僅僅是用用到A對象的屬性,會使Block也持有A對象,導致兩者互相持有,不能在作用域結束后正常釋放。
解決原理:對象A照常持有Block,但Block不能強引用持有對象A以打破循環。
解決方法:
方法一:對Block內要使用的對象A使用_*_weak*進行修飾,Block對對象A弱引用打破循環。
有三種常用形式:
使用__weak ClassName
__block XXViewController* weakSelf =self;self.blk = ^{NSLog(@"In Block : %@",weakSelf);? ? };
使用__weak typeof(self)
__weaktypeof(self) weakSelf =self;self.blk = ^{NSLog(@"In Block : %@",weakSelf);? ? };
Reactive Cocoa中的@weakify和@strongify
@weakify(self);self.blk = ^{? ? ? ? @strongify(self);? ? ? ? NSLog(@"In Block : %@",self);? ? };
其原理參考《@weakify, @strongify》,自己簡便實現參考《@weak - @strong 宏的實現》
方法二:對Block內要使用的對象A使用__block進行修飾,并在代碼塊內,使用完__block變量后將其設為nil,并且該block必須至少執行一次。
__block XXController *blkSelf =self;self.blk = ^{NSLog(@"In Block : %@",blkSelf);? ? };
注意上述代碼仍存在內存泄露,因為:
XXController對象持有Block對象blk
blk對象持有__block變量blkSelf
__block變量blkSelf持有XXController對象
__block XXController *blkSelf =self;self.blk = ^{NSLog(@"In Block : %@",blkSelf);? ? ? ? blkSelf =nil;//不能省略};self.blk();//該block必須執行一次,否則還是內存泄露
在block代碼塊內,使用完使用完__block變量后將其設為nil,并且該block必須至少執行一次后,不存在內存泄露,因為此時:
XXController對象持有Block對象blk
blk對象持有__block變量blkSelf(類型為編譯器創建的結構體)
__block變量blkSelf在執行blk()之后被設置為nil(__block變量結構體的__forwarding指針指向了nil),不再持有XXController對象,打破循環
第二種使用__block打破循環的方法,優點是:
可通過__block變量動態控制持有XXController對象的時間,運行時決定是否將nil或其他變量賦值給__block變量
不能使用__weak的系統中,使用__unsafe_unretained來替代__weak打破循環可能有野指針問題,使用__block則可避免該問題
其缺點也明顯:
必須手動保證__block變量最后設置為nil
block必須執行一次,否則__block不為nil循環應用仍存在
因此,還是避免使用第二種不常用方式,直接使用__weak打破Block循環引用。
方法三:將在Block內要使用到的對象(一般為self對象),以Block參數的形式傳入,Block就不會捕獲該對象,而將其作為參數使用,其生命周期系統的棧自動管理,不造成內存泄露。
即原來使用__weak的寫法:
__weaktypeof(self) weakSelf =self;self.blk = ^{? ? ? ? __strongtypeof(self) strongSelf = weakSelf;NSLog(@"Use Property:%@", strongSelf.name);//……};self.blk();
改為Block傳參寫法后:
self.blk = ^(UIViewController*vc) {NSLog(@"Use Property:%@", vc.name);? ? };self.blk(self);
優點:
簡化了兩行代碼,更優雅
更明確的API設計:告訴API使用者,該方法的Block直接使用傳進來的參數對象,不會造成循環引用,不用調用者再使用weak避免循環
該種用法的詳細思路,和clang后的數據結構,可參考《Heap-Stack Dance》。
鏈接:http://www.lxweimin.com/p/d28a5633b963