blcok捕獲外部變量源碼研究

Blocks是C語言的擴充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了這個新功能“Blocks”。從那開始,Block就出現在iOS和Mac系統各個API中,并被大家廣泛使用。一句話來形容Blocks,帶有自動變量(局部變量)的匿名函數。Block在OC中的實現如下:




從結構圖中很容易看到isa,所以OC處理Block是按照對象來處理的。在iOS中,isa常見的就是_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock這3種(另外只在GC環境下還有3種使用的_NSConcreteFinalizingBlock,_NSConcreteAutoBlock,_NSConcreteWeakBlockVariable,本文暫不談論這3種,有興趣的看看官方文檔)以上介紹是Block的簡要實現,接下來我們來仔細研究一下Block的捕獲外部變量的特性以及__block的實現原理。

研究工具:clang

為了研究編譯器的實現原理,我們需要使用 clang 命令。clang 命令可以將 Objetive-C 的源碼改寫成 C / C++ 語言的,借此可以研究 block 中各個特性的源碼實現方式。該命令是clang -rewrite-objc block.c

目錄1.Block捕獲外部變量實質

? ? ? ? ?2.Block的copy和release.

Block捕獲外部變量實質拿起我們的Block一起來捕捉外部變量吧。說到外部變量,我們要先說一下C語言中變量有哪幾種。一般可以分為一下5種:自動變量函數參數靜態變量靜態全局變量全局變量研究Block的捕獲外部變量就要除去函數參數這一項,下面一一根據這4種變量類型的捕獲情況進行分析。我們先根據這4種類型自動變量靜態變量靜態全局變量全局變量寫出Block測試代碼。這里很快就出現了一個錯誤,提示說自動變量沒有加__block,由于__block有點復雜;

我們先實驗靜態變量,靜態全局變量,全局變量這3類。測試代碼如下:


運行結果Block 外: ?global_i = 2,static_global_j = 3,static_k = 4,val = 5

? ? ? ? ? ? ? ? Block 中? global_i = 3,static_global_j = 4,static_k = 5,val = 4

這里就有兩點需要弄清楚了1.為什么在Block里面不加__bolck不允許更改變量?

2.為什么自動變量的值沒有增加,而其他幾個變量的值是增加的?自動變量是什么狀態下被block捕獲進去的?

為了弄清楚這兩點,我們用clang轉換一下源碼出來分析分析。


首先全局變量global_i和靜態全局變量static_global_j的值增加,以及它們被Block捕獲進去,這一點很好理解,因為是全局的,作用域很廣,所以Block捕獲了它們進去之后,在Block里面進行++操作,Block結束之后,它們的值依舊可以得以保存下來。接下來仔細看看自動變量和靜態變量的問題。在__main_block_impl_0中,可以看到靜態變量static_k和自動變量val,被Block從外面捕獲進來,成為__main_block_impl_0這個結構體的成員變量了。

接著看構造函數,__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), val(_val)這個構造函數中,自動變量和靜態變量被捕獲為成員變量追加到了構造函數中。main里面的myBlock閉包中的__main_block_impl_0結構體,初始化如下void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));

impl.isa = &_NSConcreteStackBlock;

impl.Flags = 0;impl.FuncPtr = __main_block_impl_0;?

Desc = &__main_block_desc_0_DATA;

*_static_k = 4;val = 4;到此,__main_block_impl_0結構體就是這樣把自動變量捕獲進來的。也就是說,在執行Block語法的時候,Block語法表達式所使用的自動變量的值是被保存進了Block的結構體實例中,也就是Block自身中。這里值得說明的一點是,如果Block外面還有很多自動變量,靜態變量,等等,這些變量在Block里面并不會被使用到。那么這些變量并不會被Block捕獲進來,也就是說并不會在構造函數里面傳入它們的值。Block捕獲外部變量僅僅只捕獲Block閉包里面會用到的值,其他用不到的值,它并不會去捕獲。再研究一下源碼,我們注意到__main_block_func_0這個函數的實現static void __main_block_func_0(struct __main_block_impl_0 *__cself) {? int *static_k = __cself->static_k; // bound by copy? int val = __cself->val; // bound by copy? ? ? ??

global_i ++;? ? ? ??

static_global_j ++;? ? ? ?

?(*static_k) ++;? ? ? ?

?NSLog((NSString*)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),val);? ? }我們可以發現,系統自動給我們加上的注釋,bound by copy,自動變量val雖然被捕獲進來了,但是是用 __cself->val來訪問的。Block僅僅捕獲了val的值,并沒有捕獲val的內存地址。所以在__main_block_func_0這個函數中即使我們重寫這個自動變量val的值,依舊沒法去改變Block外面自動變量val的值。

OC可能是基于這一點,在編譯的層面就防止開發者可能犯的錯誤,因為自動變量沒法在Block中改變外部變量的值,所以編譯過程中就報編譯錯誤。錯誤就是最開始的那張截圖。Variable is not assignable(missing __block type specifier)小結一下:;到此為止,上面提出的第二個問題就解開答案了。自動變量是以值傳遞方式傳遞到Block的構造函數里面去的。Block只捕獲Block中會用到的變量。由于只捕獲了自動變量的值,并非內存地址,所以Block內部不能改變自動變量的值。Block捕獲的外部變量可以改變值的是靜態變量,靜態全局變量,全局變量。上面例子也都證明過了。

剩下問題一我們還沒有解決。回到上面的例子上面來,4種變量里面只有靜態變量,靜態全局變量,全局變量這3種是可以在Block里面被改變值的。仔細觀看源碼,我們能看出這3個變量可以改變值的原因。靜態全局變量,全局變量由于作用域的原因,于是可以直接在Block里面被改變。他們也都存儲在全局區。靜態變量傳遞給Block是內存地址值,所以能在Block里面直接改變值。根據官方文檔我們可以了解到,蘋果要求我們在自動變量前加入 __block關鍵字(__block storage-class-specifier存儲域類說明符),就可以在Block里面改變外部自動變量的值了。

總結一下在Block中改變變量值有2種方式,一是傳遞內存地址指針到Block中,二是改變存儲區方式(__block)。先來實驗一下第一種方式,傳遞內存地址到Block中,改變變量的值。


控制臺輸出:

Block 外? str = Hello,

Block 中? str = Hello,World!

在__main_block_func_0里面可以看到傳遞的是指針。所以成功改變了變量的值。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容