淺談 block(2) – 截獲變數方式

發表於2016-09-05

本文會通過 clang 的 -rewrite-objc 選項來分析 block 的 C 轉換原始碼。其分析方式在該系列上一篇有詳細介紹。請先閱讀 淺談 block(1) – clang 改寫後的 block 結構

截獲自動變數

首先需要做程式碼準備工作,我們編寫一段 block 引用外部變數的 c 程式碼。

11208988-6c48dd229868e475

編譯執行成功後,使用 -rewrite-objc 進行改寫。

簡化程式碼後,得到以下主要程式碼:

與上一篇轉換的原始碼不同的是,block 語法表達中的變數作為成員新增到了 __main_block_func_0 結構體中。

並且,在該結構體中的應用變數型別與外部的型別完全相同。在初始化該結構體例項的建構函式也自然會有所差異:

去掉強轉語法簡化程式碼:

在構造時,除了要傳遞自身(self) __main_block_func_0 結構體,而且還要傳遞 block 的基本資訊,即 reserved 和 size 。這裡傳遞了一個全域性結構體物件 __main_block_desc_0_DATA ,因為他是為 block 量身設計的。最後在將引用值引數傳入建構函式中,以便於構造帶外部引用引數的 block。

進入建構函式後,發現了含有冒號表達的構造語法:

其實,冒號表示式是 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 一節的例子):

執行程式碼後會報 error :

上述書中對此情況是這樣解釋的:

block 中所使用的被截獲自動變數如同“帶有自動變數值的匿名函式”,僅截獲自動變數的值。 block 中使用自動變數後,在 block 的結構體實力中重寫該自動變數也不會改變原先截獲的自動變數。

這應該是 clang 對 block 的引用外界區域性值做的保護措施,也是為了維護 C 語言中的作用域特性。既然談到了作用域,那麼是否可以使用顯示宣告儲存域型別從而在 block 中修改該變數呢?答案是可以的。當 block 中擷取的變數為靜態變數(static),使用下例進行試驗:

裝換後的程式碼:

會發現在建構函式中使用的靜態指標 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 關鍵字類似於 staticautoregister,用於將變數存於指定儲存域。來分析一下在變數宣告前增加 __block 關鍵字後 clang 對於 block 的轉換動作。

發現核心程式碼部分有所增加,我們先從入口函式看起。

原先的 val 變成了 __Block_byre_val_0 結構體型別變數。並且這個結構體的定義是之前未曾見過的。並且我們將 val 初始化的數值 1,也出現在這個構造中,說明該結構體持有原成員變數。

__block 變數的結構體中,除了有指向類物件的 isa 指標,物件負載資訊 flags,大小 size,以及持有的原變數 val,還有一個自身型別的 __forwarding 指標。從建構函式中,會發現一個有趣的現象,__forwarding 指標會指向自身,形成自環。後面會詳細介紹它。

而在 block 體執行段,是這樣定義的。

第一步中獲得 val 的方法和 block 中引用外部變數的方式是一致的,通過 self 來獲取變數。而對於外部 block 變數賦值的時候,這種寫法引起了我們的注意:(val->forwarding->val) = 2; ,這樣做的目的何在,在後文會做出分析。

__block 變數結構

12208988-143f333fe095dd18
__block結構

當 block 內部引用外部的 block 變數,會使用以上結構對 block 做出轉換。另外,該結構體並不宣告在 __main_block_impl_0 block 結構體中,是因為這樣可以對多個 block 引用 __block 情況下,達到複用效果,從而節省不必要的空間開銷。

只觀察入口方法:

發現 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 指標存在的真實用意。

13208988-58c0d5e0bb501fe2

在複製到堆的過程中,forwarding 指標是如何更改指向的?這個問題在下一篇中進行介紹。這篇文主要講述了 block 變數在 block 中的結構,以及如何獲取外部變數,並可以對其修改的詳細過程,希望有所收穫。


@酷酷的哀殿 和哀殿君私下討論了很久,感覺文中說的 __main_block_impl_0重用 有些模糊,我在這裡詳細的解釋一下:

在研究截獲外界變數的時候,如果外部變數沒有加 __block 關鍵字,則會在 block 的結構體中增加這個變數作為成員,例如上述程式碼中的 str:

而外部變數使用 __block 關鍵字以後,會將該變數轉換為一個 __block 結構體,如果根據擷取外部變數的做法,慣性思維告訴我們 clang 應該會做出如下改變:

而為了避免多個 block 每次引用 __block 都要在 block 的 struct 內部宣告 __Block_byref_str_0 結構體,clang 的做法是將 __Block_byref_str_0 結構體放到 __main_block_impl_0 結構體外部進行宣告,這樣做可以達到宣告覆用,從而減輕了記憶體中程式碼段的內容。

其中截獲變數的原理,可以閱讀 clang 的官方程式碼。筆者也在閱讀學習中。


若想檢視更多的iOS Source Probe文章,收錄在這個Github倉庫中

相關文章