Block實際上是Objective-C對閉包的實現。
關于閉包的概念:
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)的函數或函數的引用,這些變量不是在這個代碼塊內或者任何全局上下文中定義的,而是在定義代碼塊的環境中定義(局部變量)。
正文
文章的結構,將分為三個部分,具體如下:
一.閱讀C++中的Block源碼
1.最簡單的Block
2.截取自由變量的Block
3.使用__block的Block
二.3種Block對象類型
1._NSConcreteGlobalBlock
2._NSConcreteStackBloc
3._NSConcreteMallocBlock
三.循環引用ARC
一.閱讀C++中的Block源碼
Block實際上是作為極普通的C語言源代碼來處理的,通過支持Block的編譯器,含有Block語法的源代碼轉換為一般C語言編譯器能夠處理的源代碼,并作為極為普通的C語言源代碼被編譯。我們可以在終端通過clang(LLVM編譯器)將Objective-C的代碼轉換為C++源代碼,具體指令為:clang -rewrite-objc 源代碼文件名,即可在文件目錄下生成相應的同名cpp文件。
1.最簡單的Block
這是一個最簡單的block,沒有任何外部變量,只在block塊中執行一條printf語句。我們通過clang將main.m轉換為main.cpp。通過sublime text打開cpp文件,會看到將近10萬行的代碼,直接滑動到文件最下方,找到我們需要的學習的代碼,如下圖所示:
在main函數中可以看到兩行關于block的代碼,第一行是聲明、初始化blk變量,第二行則是調用block方法。blk變量被指向了一個叫做__main_block_impl_0的結構體,結構體的構造方法中要傳入兩個參數,一個是void *fp,表示函數指針,另一個是__main_block_desc_0,存儲了block的描述信息。函數指針指向的是__main_block_func_0,這是一個static函數,函數中只有一行代碼,就是我們要執行的printf語句。可以看到__main_block_func_0函數有一個傳參struct __main_block_impl_0 *__cself,表示block本身,用途類似于OC消息機制要傳入self,由于blk沒有引用外部變量,所以在當前的函數中沒有使用到__cself。__main_block_desc_0有一個靜態的結構體實例__main_block_desc_0_DATA,在初始化blk變量的時候傳入的就是這個實例,在該結構體中有保留值reserved,以及block的大小Block_size。
__main_block_impl_0中包含了一個通用struct,__block_impl。所有的block都會包含這個結構體,變量FuncPtr就是函數指針,可以看到該結構體包含了isa指針,表明Block本質上也是個OC對象。圖中isa賦值的_NSConcreteStackBlock就是其中一種Block類。
關于Block的結構,有如上一個導圖。該結構和clang分析出來的本質是一樣的,只是變量名和結構體嵌套略微不同。invoke就是函數指針,由于我們沒有使用外部變量,所以不存在variables和descriptor中的copy、dispose。
1.isa指針,所有對象都有該指針,用于實現對象相關的功能。
2.flags,用于按bit位表示一些block的附加信息,block copy的實現代碼可以看到對該變量的使用。
3.reserved,保留變量。
4.invoke,函數指針,指向具體的Block實現的函數調用地址。
5.descriptor,表示該Block的附加描述信息,主要是size大小,以及copy和dispose函數的指針。
6.variables,截取過來的變量,Block能夠訪問它外部的局部變量,就是因為將這些變量(或變量的地址)復制到了結構體中。
額外的說下,雖然結構體的嵌套有差別,但本質是一樣的,結構體本身并不帶有任何額外信息,下圖中TestA和TestB在內存上是完全一樣的:
另外再說下clang得到的cpp中關于block的調用方式:
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
這里有一個有意思的強制轉換,就是將指向__main_block_impl_0的blk強轉成子結構體__block_impl,然后直接調用__block_impl的FuncPtr(函數指針)。可以這樣強轉的原因在于__block_impl位于__main_block_impl_0的最頂部。舉個例子,如下圖所示:
上圖兩次輸出的數字分別是2和1。第二次強轉所獲得的valueB所存儲的值,實際上是TestB中的變量a,因為它是TestB最頂部的int值。結構體的本質是,我們和C語言約定了一段內存空間的長短,及其內容的安排,它和int等類型一樣,都是數據類型,其他類型怎么轉換,結構體就怎么轉換。當把TestB強轉成ValueB后,會按照ValueB的格局對該內存空間進行解釋,而這段內存的第一段長度等于int類型的空間存儲的是TestB的a的值,即為1。
2.截取自由變量的Block
我們這次在Block外部定義一個局部變量test_value,在Block的代碼塊中輸出該變量。同樣使用clang獲取cpp文件如下:
可以看到在__main_block_impl_0結構體中多了一個叫做test_value的int值。在__main_block_func_0中,首先通過__cself->test_value獲取int值,然后再進行printf輸出。注意到此處的test_value是屬于Block中的一塊內存空間,和Block外部的test_value沒有了關聯。
所謂的截取自動變量值,意味著在執行Block初始化語句時,將Block所使用的自動變量值保存到Block的結構體實例中,在Block內部修改該變量值并不能影響原先的變量。
如果在Block塊中對test_value執行賦值語句,并不能改變Block外部的test_value變量,實際上,編譯器會進行報錯。
3.使用__block的Block
對Block外部變量添加__block修飾符就可以在Block塊中對變量進行修改,我們通過clang看下它的實現原理。
這回代碼多了很多東西,可以看到__main_block_impl_0中的test_value變成了一個指向__Block_byref_test_value_0的指針。在main函數中,初始化test_value并不是簡單的新建一個int型變量,而是構造了一個__Block_byref_test_value_0的結構體。test_value變成了一個對象,在它的結構體中,屬性test_value用來存儲原先的int值,__forwarding指針指向了初始化的結構體本身,即使該結構體被拷貝到Block中,__forwarding指針仍然指向最初的那個結構體,所以使用該變量的方式是test_value->__forwarding->test_value。
二.3種Block對象類型
由于Block也是Objective-C對象,所以它有相應的類。目前有三種Block類:
NSConcreteGlobalBlock,全局的靜態Block,不會訪問任何外部變量。
NSConcreteStackBlock,保存在棧中的Block,當函數返回時會被銷毀。
NSConcreteMallocBlock,保存在堆中的Block,當引用計數為0時會被銷毀。
1.NSConcreteGlobalBlock
這種情況的block是一個global類型,在通過NSLog輸出為global類型,并且在clang的cpp文件中能看到impl的isa賦值成為了_NSConcreteGlobalBlock。
這種情況下的block仍然是global類型,通過NSLog輸出的仍然是NSGlobalBlock。但是在clang轉換的cpp中,由于clang改寫的具體實現方式和LLVM不太一樣,并且這里沒有開啟ARC,我們看到的isa指向的是stack類型。在開啟ARC時,block應該是global類型。
因為不需要對自動變量進行capture截獲,所以Block用結構體實例的內容不依賴于執行時的狀態,因此整個程序只需要一個實例。這樣就可以將Block的結構體實例放在和全局變量相同的數據區域。
判斷是否為global類型的Block可以依據以下條件:
1.記述全局變量的地方有Block語法時
2.Block語法的表達式中不使用應截獲的自動變量時
2.NSConcreteStackBlock
在MRC中調用了外部變量的Block就會是一個stack類型,在NSLog中可以看到。注意在ARC中已經不存在stack類型的Block了。
3.NSConcreteMallocBlock
當Block從棧上復制到堆上時,isa就會被修改為malloc類型。Block執行copy方法就會進行棧到堆的拷貝,若已經是malloc類的Block,則會對Block的引用計數+1,若是global類型執行copy則不起任何作用。
上圖為MRC環境下,將stack類型的block進行拷貝得到的對象就是malloc類型。
再次使用clang獲取cpp代碼,可以注意到,descriptor中有copy和dispose方法,就是在Block進行copy時對__block對象也進行引用計數操作。
棧上的__block變量會被復制到堆上,這時會將成員變量__forwarding的值替換成復制堆上的__block變量的地址
什么時候會將棧上的Block復制到堆?
在以前版本的ARC中:
1.調用Block的copy實例方法時
2.Block作為函數返回值返回時
3.將Block賦值給附有__strong修飾符id類型的類或Block類型成員變量時
4.在方法中含有usingBlock的Cocoa框架方法或GCD的API中傳遞Block時
現在,在ARC開啟的情況下,將會只有NSConcreteGlobal和NSConcreteMallocBlock類型的block。由于ARC已經能很好的處理對象的聲明周期的管理,這樣所有對象都放到堆上管理,對于編譯器實現來說,會比較方便。
查看以上代碼生成的cpp文件,__block和descriptor中都有copy和dispose方法,descriptor中的方法用來對__block實例進行引用計數操作,__block中的方法用來對array進行引用計數操作。
上兩張圖的輸出結果都是0,主要說下圖二,Block持有了__block對象,但是__block對象無法持有__weak修飾的NSArray對象,所以執行Block方法塊時NSArray對象已經被釋放。
三.循環引用
執行上方的代碼,Person的dealloc不會被調用,因為blk與Person實例相互引用了。
使用__block變量同樣不能解決循環引用,因為Block引用了__block對象,__block對象引用了self,self引用了Block。
在Block代碼塊內對__block對象賦值nil可以避免循環引用,但是如果沒有執行過Block或忘記賦值nil都會引起循環引用。使用__block的優點是,可控制變量的持有期。
使用__weak可以讓Block無法持有Person實例,從而避免了循環引用。另外,為了方式在Block執行半途時Person實例被釋放,通常在Block方法塊中先創建一個__strong的Person指針對其進行持有,當Block方法塊結束后,strongSelf就會被釋放。