Block是什么?
Block實際上是Objective-C對閉包的實現(xiàn)窟赏。
關于閉包的概念:
In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.
閉包是包含了非本地變量(也叫作自由變量或upvalues)的函數(shù)或函數(shù)的引用岩灭,這些變量不是在這個代碼塊內(nèi)或者任何全局上下文中定義的拌倍,而是在定義代碼塊的環(huán)境中定義(局部變量)。
正文
文章的結(jié)構(gòu)噪径,將分為三個部分柱恤,具體如下:
一.閱讀C++中的Block源碼
1.最簡單的Block
2.截取自由變量的Block
3.使用__block的Block
二.3種Block對象類型
1._NSConcreteGlobalBlock
2._NSConcreteStackBloc
3._NSConcreteMallocBlock
三.循環(huán)引用ARC
一.閱讀C++中的Block源碼
Block實際上是作為極普通的C語言源代碼來處理的,通過支持Block的編譯器找爱,含有Block語法的源代碼轉(zhuǎn)換為一般C語言編譯器能夠處理的源代碼梗顺,并作為極為普通的C語言源代碼被編譯。我們可以在終端通過clang(LLVM編譯器)將Objective-C的代碼轉(zhuǎn)換為C++源代碼车摄,具體指令為:clang -rewrite-objc 源代碼文件名寺谤,即可在文件目錄下生成相應的同名cpp文件仑鸥。
1.最簡單的Block
這是一個最簡單的block,沒有任何外部變量变屁,只在block塊中執(zhí)行一條printf語句眼俊。我們通過clang將main.m轉(zhuǎn)換為main.cpp。通過sublime text打開cpp文件粟关,會看到將近10萬行的代碼疮胖,直接滑動到文件最下方,找到我們需要的學習的代碼闷板,如下圖所示:
在main函數(shù)中可以看到兩行關于block的代碼澎灸,第一行是聲明、初始化blk變量遮晚,第二行則是調(diào)用block方法性昭。blk變量被指向了一個叫做__main_block_impl_0的結(jié)構(gòu)體,結(jié)構(gòu)體的構(gòu)造方法中要傳入兩個參數(shù)县遣,一個是void *fp巩梢,表示函數(shù)指針,另一個是__main_block_desc_0艺玲,存儲了block的描述信息。函數(shù)指針指向的是__main_block_func_0鞠抑,這是一個static函數(shù)饭聚,函數(shù)中只有一行代碼,就是我們要執(zhí)行的printf語句搁拙∶胧幔可以看到__main_block_func_0函數(shù)有一個傳參struct __main_block_impl_0 *__cself,表示block本身箕速,用途類似于OC消息機制要傳入self酪碘,由于blk沒有引用外部變量,所以在當前的函數(shù)中沒有使用到__cself盐茎。__main_block_desc_0有一個靜態(tài)的結(jié)構(gòu)體實例__main_block_desc_0_DATA兴垦,在初始化blk變量的時候傳入的就是這個實例,在該結(jié)構(gòu)體中有保留值reserved字柠,以及block的大小Block_size探越。
__main_block_impl_0中包含了一個通用struct,__block_impl窑业。所有的block都會包含這個結(jié)構(gòu)體钦幔,變量FuncPtr就是函數(shù)指針,可以看到該結(jié)構(gòu)體包含了isa指針常柄,表明Block本質(zhì)上也是個OC對象鲤氢。圖中isa賦值的_NSConcreteStackBlock就是其中一種Block類搀擂。
關于Block的結(jié)構(gòu),有如上一個導圖卷玉。該結(jié)構(gòu)和clang分析出來的本質(zhì)是一樣的哨颂,只是變量名和結(jié)構(gòu)體嵌套略微不同。invoke就是函數(shù)指針揍庄,由于我們沒有使用外部變量咆蒿,所以不存在variables和descriptor中的copy、dispose蚂子。
1.isa指針沃测,所有對象都有該指針,用于實現(xiàn)對象相關的功能食茎。
2.flags蒂破,用于按bit位表示一些block的附加信息,block copy的實現(xiàn)代碼可以看到對該變量的使用别渔。
3.reserved附迷,保留變量。
4.invoke哎媚,函數(shù)指針喇伯,指向具體的Block實現(xiàn)的函數(shù)調(diào)用地址。
5.descriptor拨与,表示該Block的附加描述信息稻据,主要是size大小,以及copy和dispose函數(shù)的指針买喧。
6.variables捻悯,截取過來的變量,Block能夠訪問它外部的局部變量淤毛,就是因為將這些變量(或變量的地址)復制到了結(jié)構(gòu)體中今缚。
額外的說下,雖然結(jié)構(gòu)體的嵌套有差別低淡,但本質(zhì)是一樣的姓言,結(jié)構(gòu)體本身并不帶有任何額外信息,下圖中TestA和TestB在內(nèi)存上是完全一樣的:
另外再說下clang得到的cpp中關于block的調(diào)用方式:
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
這里有一個有意思的強制轉(zhuǎn)換蔗蹋,就是將指向__main_block_impl_0的blk強轉(zhuǎn)成子結(jié)構(gòu)體__block_impl事期,然后直接調(diào)用__block_impl的FuncPtr(函數(shù)指針)≈窖眨可以這樣強轉(zhuǎn)的原因在于__block_impl位于__main_block_impl_0的最頂部兽泣。舉個例子,如下圖所示:
上圖兩次輸出的數(shù)字分別是2和1胁孙。第二次強轉(zhuǎn)所獲得的valueB所存儲的值唠倦,實際上是TestB中的變量a称鳞,因為它是TestB最頂部的int值。結(jié)構(gòu)體的本質(zhì)是稠鼻,我們和C語言約定了一段內(nèi)存空間的長短冈止,及其內(nèi)容的安排,它和int等類型一樣候齿,都是數(shù)據(jù)類型熙暴,其他類型怎么轉(zhuǎn)換,結(jié)構(gòu)體就怎么轉(zhuǎn)換慌盯。當把TestB強轉(zhuǎn)成ValueB后周霉,會按照ValueB的格局對該內(nèi)存空間進行解釋,而這段內(nèi)存的第一段長度等于int類型的空間存儲的是TestB的a的值亚皂,即為1俱箱。
2.截取自由變量的Block
我們這次在Block外部定義一個局部變量test_value,在Block的代碼塊中輸出該變量灭必。同樣使用clang獲取cpp文件如下:
可以看到在__main_block_impl_0結(jié)構(gòu)體中多了一個叫做test_value的int值狞谱。在__main_block_func_0中,首先通過__cself->test_value獲取int值禁漓,然后再進行printf輸出跟衅。注意到此處的test_value是屬于Block中的一塊內(nèi)存空間,和Block外部的test_value沒有了關聯(lián)播歼。
所謂的截取自動變量值与斤,意味著在執(zhí)行Block初始化語句時,將Block所使用的自動變量值保存到Block的結(jié)構(gòu)體實例中荚恶,在Block內(nèi)部修改該變量值并不能影響原先的變量。
如果在Block塊中對test_value執(zhí)行賦值語句磷支,并不能改變Block外部的test_value變量谒撼,實際上,編譯器會進行報錯雾狈。
3.使用__block的Block
對Block外部變量添加__block修飾符就可以在Block塊中對變量進行修改廓潜,我們通過clang看下它的實現(xiàn)原理。
這回代碼多了很多東西善榛,可以看到__main_block_impl_0中的test_value變成了一個指向__Block_byref_test_value_0的指針辩蛋。在main函數(shù)中,初始化test_value并不是簡單的新建一個int型變量移盆,而是構(gòu)造了一個__Block_byref_test_value_0的結(jié)構(gòu)體悼院。test_value變成了一個對象,在它的結(jié)構(gòu)體中咒循,屬性test_value用來存儲原先的int值据途,__forwarding指針指向了初始化的結(jié)構(gòu)體本身绞愚,即使該結(jié)構(gòu)體被拷貝到Block中,__forwarding指針仍然指向最初的那個結(jié)構(gòu)體颖医,所以使用該變量的方式是test_value->__forwarding->test_value位衩。
二.3種Block對象類型
由于Block也是Objective-C對象,所以它有相應的類熔萧。目前有三種Block類:
NSConcreteGlobalBlock糖驴,全局的靜態(tài)Block,不會訪問任何外部變量佛致。
NSConcreteStackBlock贮缕,保存在棧中的Block,當函數(shù)返回時會被銷毀晌杰。
NSConcreteMallocBlock跷睦,保存在堆中的Block,當引用計數(shù)為0時會被銷毀肋演。
1.NSConcreteGlobalBlock
這種情況的block是一個global類型抑诸,在通過NSLog輸出為global類型,并且在clang的cpp文件中能看到impl的isa賦值成為了_NSConcreteGlobalBlock爹殊。
這種情況下的block仍然是global類型蜕乡,通過NSLog輸出的仍然是NSGlobalBlock。但是在clang轉(zhuǎn)換的cpp中梗夸,由于clang改寫的具體實現(xiàn)方式和LLVM不太一樣层玲,并且這里沒有開啟ARC,我們看到的isa指向的是stack類型反症。在開啟ARC時辛块,block應該是global類型。
因為不需要對自動變量進行capture截獲铅碍,所以Block用結(jié)構(gòu)體實例的內(nèi)容不依賴于執(zhí)行時的狀態(tài)润绵,因此整個程序只需要一個實例。這樣就可以將Block的結(jié)構(gòu)體實例放在和全局變量相同的數(shù)據(jù)區(qū)域胞谈。
判斷是否為global類型的Block可以依據(jù)以下條件:
1.記述全局變量的地方有Block語法時
2.Block語法的表達式中不使用應截獲的自動變量時
2.NSConcreteStackBlock
在MRC中調(diào)用了外部變量的Block就會是一個stack類型尘盼,在NSLog中可以看到。注意在ARC中已經(jīng)不存在stack類型的Block了烦绳。
3.NSConcreteMallocBlock
當Block從棧上復制到堆上時卿捎,isa就會被修改為malloc類型。Block執(zhí)行copy方法就會進行棧到堆的拷貝径密,若已經(jīng)是malloc類的Block午阵,則會對Block的引用計數(shù)+1,若是global類型執(zhí)行copy則不起任何作用享扔。
上圖為MRC環(huán)境下趟庄,將stack類型的block進行拷貝得到的對象就是malloc類型括细。
再次使用clang獲取cpp代碼,可以注意到戚啥,descriptor中有copy和dispose方法奋单,就是在Block進行copy時對__block對象也進行引用計數(shù)操作。
棧上的__block變量會被復制到堆上猫十,這時會將成員變量__forwarding的值替換成復制堆上的__block變量的地址
什么時候會將棧上的Block復制到堆览濒?
在以前版本的ARC中:</br>
1.調(diào)用Block的copy實例方法時</br>
2.Block作為函數(shù)返回值返回時</br>
3.將Block賦值給附有__strong修飾符id類型的類或Block類型成員變量時</br>
4.在方法中含有usingBlock的Cocoa框架方法或GCD的API中傳遞Block時
現(xiàn)在,在ARC開啟的情況下拖云,將會只有NSConcreteGlobal和NSConcreteMallocBlock類型的block贷笛。由于ARC已經(jīng)能很好的處理對象的聲明周期的管理,這樣所有對象都放到堆上管理宙项,對于編譯器實現(xiàn)來說乏苦,會比較方便。
查看以上代碼生成的cpp文件尤筐,__block和descriptor中都有copy和dispose方法汇荐,descriptor中的方法用來對__block實例進行引用計數(shù)操作,__block中的方法用來對array進行引用計數(shù)操作盆繁。
上兩張圖的輸出結(jié)果都是0掀淘,主要說下圖二,Block持有了__block對象油昂,但是__block對象無法持有__weak修飾的NSArray對象革娄,所以執(zhí)行Block方法塊時NSArray對象已經(jīng)被釋放。
三.循環(huán)引用
執(zhí)行上方的代碼冕碟,Person的dealloc不會被調(diào)用拦惋,因為blk與Person實例相互引用了。
使用__block變量同樣不能解決循環(huán)引用安寺,因為Block引用了__block對象厕妖,__block對象引用了self,self引用了Block我衬。
在Block代碼塊內(nèi)對__block對象賦值nil可以避免循環(huán)引用,但是如果沒有執(zhí)行過Block或忘記賦值nil都會引起循環(huán)引用饰恕。使用__block的優(yōu)點是挠羔,可控制變量的持有期。
使用__weak可以讓Block無法持有Person實例埋嵌,從而避免了循環(huán)引用破加。另外,為了方式在Block執(zhí)行半途時Person實例被釋放雹嗦,通常在Block方法塊中先創(chuàng)建一個__strong的Person指針對其進行持有范舀,當Block方法塊結(jié)束后合是,strongSelf就會被釋放。