本文會通過 clang 的 -rewrite-objc
選項來分析 block 的 C 轉換原始碼。其分析方式在該系列上一篇有詳細介紹。請先閱讀 淺談 block(1) – clang 改寫後的 block 結構 。
截獲自動變數
首先需要做程式碼準備工作,我們編寫一段 block 引用外部變數的 c 程式碼。
編譯執行成功後,使用 -rewrite-objc
進行改寫。
1 |
clang -rewrite-objc block.c |
簡化程式碼後,得到以下主要程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; char *str; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, char *_str, int flags=0) : str(_str) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { char *str = __cself->str; // bound by copy printf("%s\n", str); } static struct __main_block_desc_0 { size_t reserved; size_t Block_size; } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0) }; int main() { char *str = "Desgard_Duan"; void (*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, str)); ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); return 0; } |
與上一篇轉換的原始碼不同的是,block 語法表達中的變數作為成員新增到了 __main_block_func_0
結構體中。
1 2 3 4 5 |
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; char *str; // 外部引用變數 } |
並且,在該結構體中的應用變數型別與外部的型別完全相同。在初始化該結構體例項的建構函式也自然會有所差異:
1 |
void (*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, str)); |
去掉強轉語法簡化程式碼:
1 |
void (*block)() = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, str); |
在構造時,除了要傳遞自身(self) __main_block_func_0
結構體,而且還要傳遞 block 的基本資訊,即 reserved 和 size 。這裡傳遞了一個全域性結構體物件 __main_block_desc_0_DATA
,因為他是為 block 量身設計的。最後在將引用值引數傳入建構函式中,以便於構造帶外部引用引數的 block。
進入建構函式後,發現了含有冒號表達的構造語法:
1 2 3 4 5 6 |
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, char *_str, int flags=0) : str(_str) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } |
其實,冒號表示式是 C++ 中的一個固有語法。這是顯示構造的方法之一。另外還有一種構造顯示構造方式,其語法較為繁瑣,即使用 this 指標構造。(關於 C++ 建構函式,可以學習 msdn 文件 建構函式 (C++) )
之後的程式碼與前一篇分析相同,不再討論。
通過整個構造 block 流程分析,我們發現當 block 引用外部物件時,會在結構體內部新建立一個成員進行儲存。此處我們使用的是 char 型別,而在結構體中所使用的 char 是結構體的成員,所以可以得知:block 引用外部物件時候,不是簡單的指標引用(淺複製),而是一種重建(深複製)方式(括號內外分別對於基本資料型別和物件分別描述))。所以如果在 block 中對外部物件進行修改,無論是值修改還是指標修改,自然是沒有任何效果。
引入 __block 關鍵字對擷取變數一探究竟
上文中的 block 所引用的外部成員是一個字元型指標,當我們在 block 內部對其修改後,很容易的想到,會改變該指標的指向。而當 block 中引用外部變數為常用資料型別會有些許的不同:
我們來看這個例子 (這是來自 Pro multithreading and memory management for iOS and OS X 2.3.3 一節的例子):
1 2 |
int val = 0; void (^blk)(void) = ^{val = 1}; |
執行程式碼後會報 error :
1 2 |
error: variable is not assignable (missing __block type specifier) void (^blk)(void) = ^{val = 1}; |
上述書中對此情況是這樣解釋的:
block 中所使用的被截獲自動變數如同“帶有自動變數值的匿名函式”,僅截獲自動變數的值。 block 中使用自動變數後,在 block 的結構體實力中重寫該自動變數也不會改變原先截獲的自動變數。
這應該是 clang 對 block 的引用外界區域性值做的保護措施,也是為了維護 C 語言中的作用域特性。既然談到了作用域,那麼是否可以使用顯示宣告儲存域型別從而在 block 中修改該變數呢?答案是可以的。當 block 中擷取的變數為靜態變數(static),使用下例進行試驗:
1 2 3 4 5 6 |
int main() { static int static_val = 2; void (^blk)(void) = ^{ static_val = 3; }; } |
裝換後的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int *static_val; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; int main() { static int static_val = 2; void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_ return 0; } |
會發現在建構函式中使用的靜態指標 int *_static_val
對其進行訪問。將靜態變數 static_val
的指標傳遞給 __main_block_impl_0
結構體的建構函式並加以儲存。通過指標進行作用域擴充,是 C 中很常見的思想及做法,也是超出作用域使用變數的最簡單方法。
那麼我們為什麼在引用自動變數的時候,不使用該自動變數的指標呢?是應為在 block 截獲變數後,原來的自動變數已經廢棄,因此block 中超過變數作用域從而無法通過指標訪問原來的自動變數。
為了解決這個問題,其實在 block 擴充套件中已經提供了方法(官方文件)。即使用 __block
關鍵字。
__block
關鍵字更準確的表達應為 block說明符(block storage-class-specifier) ,用來描述儲存域。在 C 語言中已經存有如下儲存域宣告關鍵字:
- typedef:常用在為資料型別起別名,而不是一般認識的儲存域宣告關鍵字作用。但在歸類上屬於儲存域宣告關鍵字。
- extern:限制標示,限制定義變數在所有模組中作為全域性變數,並只能被定義一次。
- static:靜態變數儲存在 .data 區。
- auto:自動變數儲存在棧中。
- register:約束變數為單值,儲存在CPU暫存器內。
__block
關鍵字類似於 static
、auto
、register
,用於將變數存於指定儲存域。來分析一下在變數宣告前增加 __block
關鍵字後 clang 對於 block 的轉換動作。
1 2 3 4 |
__block int val = 1; void (^blk)(void) = ^ { val = 2; }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
// 要點 1:__block 變數轉換結構 struct __Block_byref_val_0 { void *__isa; __Block_byref_val_0 *__forwarding; int __flags; int __size; int val; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_val_0 *val; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_val_0 *val = __cself->val; // bound by ref // 要點 2:__forwarding 自環指標存在意義 (val->__forwarding->val) = 2; } static struct __main_block_desc_0 { size_t reserved; size_t Block_size; // 要點 3:copy/dispose 方法內部實現 void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); void (*dispose)(struct __main_block_impl_0*); } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0 }; int main() { __attribute__((__blocks__(byref))) __Block_byref_val_0 val = { (void*)0, (__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 1 }; void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344)); return 0; } |
發現核心程式碼部分有所增加,我們先從入口函式看起。
1 2 3 4 5 6 7 |
__Block_byref_val_0 val = { (void*)0, (__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 1 }; |
原先的 val 變成了 __Block_byre_val_0
結構體型別變數。並且這個結構體的定義是之前未曾見過的。並且我們將 val 初始化的數值 1,也出現在這個構造中,說明該結構體持有原成員變數。
1 2 3 4 5 6 7 |
struct __Block_byref_val_0 { void *__isa; __Block_byref_val_0 *__forwarding; int __flags; int __size; int val; }; |
在 __block
變數的結構體中,除了有指向類物件的 isa
指標,物件負載資訊 flags
,大小 size
,以及持有的原變數 val
,還有一個自身型別的 __forwarding
指標。從建構函式中,會發現一個有趣的現象,__forwarding
指標會指向自身,形成自環。後面會詳細介紹它。
而在 block 體執行段,是這樣定義的。
1 2 3 4 |
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_val_0 *val = __cself->val; // bound by ref (val->__forwarding->val) = 2; } |
第一步中獲得 val 的方法和 block 中引用外部變數的方式是一致的,通過 self 來獲取變數。而對於外部 block 變數賦值的時候,這種寫法引起了我們的注意:(val->
forwarding->val) = 2; ,這樣做的目的何在,在後文會做出分析。
__block 變數結構
當 block 內部引用外部的 block 變數,會使用以上結構對 block 做出轉換。另外,該結構體並不宣告在 __main_block_impl_0
block 結構體中,是因為這樣可以對多個 block 引用 __block 情況下,達到複用效果,從而節省不必要的空間開銷。
1 2 3 |
__block int val = 0; void (^blk1)(void) = ^{val = 1;}; void (^blk2)(void) = ^{val = 2;}; |
只觀察入口方法:
1 2 3 4 5 6 7 8 9 10 |
__Block_byref_val_0 = {0, &val, 0, sizeof(__Block_byref_val_0), 10}; blk1 = &__main_block_impl_0(__main_block_func_0 , &__main_block_desc_0_DATA , &val , 0x22000000); blk2 = &__main_block_impl_0(__main_block_func_1 , &__main_block_desc_1_DATA , &val , 0x22000000); |
發現 val 指標被複用,使得兩個 block 同時使用一個 __block 只需要對其結構宣告一次即可。
接觸 Objective-C 語言環境下的 block
通過兩篇文的 block 的結構轉換,我們發現其實 block 的實質是一個物件 (Object),從封裝成結構體物件,再到 isa 指標結構,都是明顯的體現。對於 block 也是如此,在轉換後將其封裝成了 block 結構體型別,以物件方式處理。
帶著 C 程式碼中的 block 擴充套件轉換規則開始進入 Objective-C block 的學習。首先需要知道 block 的三個型別。
型別 | 物件儲存域 | 地址單元 |
---|---|---|
_NSConcreteStackBlock | 棧 | 高地址 |
_NSConcreteMallocBlock | 堆 | |
_NSConcreteGloalBlock | 靜態區(.data) | 低地址 |
在上一篇文中的末尾部分,簡單的說了一下全域性靜態的儲存問題。這裡再一次強調, _NSConcreteGloalBlock
的 block 會在一下兩種情況下出現(與 clang 轉換結果不大相同):
- 全域性變數位置
- block 中不引用外部變數
而在其他情況下,基本上 block 的型別都為 _NSConcreteStackBlock 。但是在棧上的 block 會受到作用域的限制,一旦所屬的變數作用域結束,該 block 就會被釋放。由此,引出了 _NSConcreteMallocBlock 堆 block 型別。
block 提供了將 block 和 __block 變數從棧上覆制到堆上的方法來解決這個問題。將配置在站上的 block 複製到堆上,這樣可以保證在 block 變數作用域結束後,堆上仍舊可訪問。
block 變數通過 forwarding 可以無論在堆上還是棧上都能正常訪問。當 block 儲存在堆上的時候,對應的棧上 block 的 forwarding 成員會斷開自環,而指向堆上的 block 物件。這也就是 forwarding 指標存在的真實用意。
在複製到堆的過程中,forwarding 指標是如何更改指向的?這個問題在下一篇中進行介紹。這篇文主要講述了 block 變數在 block 中的結構,以及如何獲取外部變數,並可以對其修改的詳細過程,希望有所收穫。
@酷酷的哀殿 和哀殿君私下討論了很久,感覺文中說的 __main_block_impl_0重用 有些模糊,我在這裡詳細的解釋一下:
在研究截獲外界變數的時候,如果外部變數沒有加 __block 關鍵字,則會在 block 的結構體中增加這個變數作為成員,例如上述程式碼中的 str:
1 2 3 4 5 6 7 8 9 10 11 |
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; char *str; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, char *_str, int flags=0) : str(_str) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; |
而外部變數使用 __block 關鍵字以後,會將該變數轉換為一個 __block 結構體,如果根據擷取外部變數的做法,慣性思維告訴我們 clang 應該會做出如下改變:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; struct __Block_byref_str_0 { void *__isa; __Block_byref_val_0 *__forwarding; int __flags; int __size; char *str; }; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, char *_str, int flags=0) : str(_str) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; |
而為了避免多個 block 每次引用 __block 都要在 block 的 struct 內部宣告 __Block_byref_str_0 結構體,clang 的做法是將 __Block_byref_str_0 結構體放到 __main_block_impl_0 結構體外部進行宣告,這樣做可以達到宣告覆用,從而減輕了記憶體中程式碼段的內容。
其中截獲變數的原理,可以閱讀 clang 的官方程式碼。筆者也在閱讀學習中。
若想檢視更多的iOS Source Probe文章,收錄在這個Github倉庫中。