談Objective-C Block的實現
前言
這裡 有關於 block 的 5 道測試題,建議你閱讀本文之前先做一下測試。
先介紹一下什麼是閉包。在 wikipedia 上,閉包的定義) 是:
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.
翻譯過來,閉包是一個函式(或指向函式的指標),再加上該函式執行的外部的上下文變數(有時候也稱作自由變數)。
block 實際上就是 Objective-C 語言對於閉包的實現。 block 配合上 dispatch_queue,可以方便地實現簡單的多執行緒程式設計和非同步程式設計,關於這個,我之前寫過一篇文章介紹:《使用 GCD》。
本文主要介紹 Objective-C 語言的 block 在編譯器中的實現方式。主要包括:
- block 的內部實現資料結構介紹
- block 的三種型別及其相關的記憶體管理方式
- block 如何通過 capture 變數來達到訪問函式外的變數
實現方式
資料結構定義
block 的資料結構定義如下(圖片來自 這裡):
對應的結構體定義如下:
通過該圖,我們可以知道,一個 block 例項實際上由 6 部分構成:
- isa 指標,所有物件都有該指標,用於實現物件相關的功能。
- flags,用於按 bit 位表示一些 block 的附加資訊,本文後面介紹 block copy 的實現程式碼可以看到對該變數的使用。
- reserved,保留變數。
- invoke,函式指標,指向具體的 block 實現的函式呼叫地址。
- descriptor, 表示該 block 的附加描述資訊,主要是 size 大小,以及 copy 和 dispose 函式的指標。
- variables,capture 過來的變數,block 能夠訪問它外部的區域性變數,就是因為將這些變數(或變數的地址)複製到了結構體中。
該資料結構和後面的 clang 分析出來的結構實際是一樣的,不過僅是結構體的巢狀方式不一樣。但這一點我一開始沒有想明白,所以也給大家解釋一下,如下 2 個結構體 SampleA 和 SampleB 在記憶體上是完全一樣的,原因是結構體本身並不帶有任何額外的附加資訊。
在 Objective-C 語言中,一共有 3 種型別的 block:
- _NSConcreteGlobalBlock 全域性的靜態 block,不會訪問任何外部變數。
- _NSConcreteStackBlock 儲存在棧中的 block,當函式返回時會被銷燬。
- _NSConcreteMallocBlock 儲存在堆中的 block,當引用計數為 0 時會被銷燬。
我們在下面會分別來檢視它們各自的實現方式上的差別。
研究工具:clang
為了研究編譯器是如何實現 block 的,我們需要使用 clang。clang 提供一個命令,可以將 Objetive-C 的原始碼改寫成 c 語言的,藉此可以研究 block 具體的原始碼實現方式。該命令是
NSConcreteGlobalBlock 型別的 block 的實現
我們先新建一個名為 block1.c 的原始檔:
然後在命令列中輸入clang
-rewrite-objc block1.c
即可在目錄中看到 clang 輸出了一個名為 block1.cpp 的檔案。該檔案就是 block 在 c 語言實現,我將 block1.cpp 中一些無關的程式碼去掉,將關鍵程式碼引用如下:
下面我們就具體看一下是如何實現的。__main_block_impl_0 就是該 block 的實現,從中我們可以看出:
- 一個 block 實際是一個物件,它主要由一個 isa 和 一個 impl 和 一個 descriptor 組成。
-
在本例中,isa 指向 _NSConcreteGlobalBlock, 主要是為了實現物件的所有特性,在此我們就不展開討論了。 -
由於 clang 改寫的具體實現方式和 LLVM 不太一樣,並且這裡沒有開啟 ARC。所以這裡我們看到 isa 指向的還是
_NSConcreteStackBlock
。但在 LLVM 的實現中,開啟 ARC 時,block 應該是 _NSConcreteGlobalBlock 型別,具體可以看 《objective-c-blocks-quiz》 第二題的解釋。 - impl 是實際的函式指標,本例中,它指向 __main_block_func_0。這裡的 impl 相當於之前提到的 invoke 變數,只是 clang 編譯器對變數的命名不一樣而已。
- descriptor 是用於描述當前這個 block 的附加資訊的,包括結構體的大小,需要 capture 和 dispose 的變數列表等。結構體大小需要儲存是因為,每個 block 因為會 capture 一些變數,這些變數會加到 __main_block_impl_0 這個結構體中,使其體積變大。在該例子中我們還看不到相關 capture 的程式碼,後面將會看到。
NSConcreteStackBlock 型別的 block 的實現
我們另外新建一個名為 block2.c 的檔案,輸入以下內容:
用之前提到的 clang 工具,轉換後的關鍵程式碼如下:
在本例中,我們可以看到:
- 本例中,isa 指向 _NSConcreteStackBlock,說明這是一個分配在棧上的例項。
- main_block_impl_0 中增加了一個變數 a,在 block 中引用的變數 a 實際是在申明 block 時,被複制到 main_block_impl_0 結構體中的那個變數 a。因為這樣,我們就能理解,在 block 內部修改變數 a 的內容,不會影響外部的實際變數 a。
- main_block_impl_0 中由於增加了一個變數 a,所以結構體的大小變大了,該結構體大小被寫在了 main_block_desc_0 中。
我們修改上面的原始碼,在變數前面增加 __block 關鍵字:
生成的關鍵程式碼如下,可以看到,差異相當大:
從程式碼中我們可以看到:
- 原始碼中增加一個名為 __Block_byref_i_0 的結構體,用來儲存我們要 capture 並且修改的變數 i。
- main_block_impl_0 中引用的是 Block_byref_i_0 的結構體指標,這樣就可以達到修改外部變數的作用。
- __Block_byref_i_0 結構體中帶有 isa,說明它也是一個物件。
- 我們需要負責 Block_byref_i_0 結構體相關的記憶體管理,所以 main_block_desc_0 中增加了 copy 和 dispose 函式指標,對於在呼叫前後修改相應變數的引用計數。
NSConcreteMallocBlock 型別的 block 的實現
NSConcreteMallocBlock 型別的 block 通常不會在原始碼中直接出現,因為預設它是當一個 block 被 copy 的時候,才會將這個 block 複製到堆中。以下是一個 block 被 copy 時的示例程式碼 (來自 這裡),可以看到,在第 8 步,目標的 block 型別被修改為 _NSConcreteMallocBlock。
變數的複製
對於 block 外的變數引用,block 預設是將其複製到其資料結構中來實現訪問的,如下圖所示(圖片來自 這裡):
對於用 __block 修飾的外部變數引用,block 是複製其引用地址來實現訪問的,如下圖所示(圖片來自 這裡):
LLVM 原始碼
在 LLVM 開源的關於 block 的實現原始碼,其內容也和我們用 clang 改寫得到的內容相似,印證了我們對於 block 內部資料結構的推測。
ARC 對 block 型別的影響
在 ARC 開啟的情況下,將只會有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 型別的 block。
原本的 NSConcreteStackBlock 的 block 會被 NSConcreteMallocBlock 型別的 block 替代。證明方式是以下程式碼在 XCode 中,會輸出 <__NSMallocBlock__:
0x100109960>
。在蘋果的 官方文件 中也提到,當把棧中的
block 返回時,不需要呼叫 copy 方法了。
我個人認為這麼做的原因是,由於 ARC 已經能很好地處理物件的生命週期的管理,這樣所有物件都放到堆上管理,對於編譯器實現來說,會比較方便。
參考連結
希望本文能加深你對於 block 的理解。我在學習中,查閱了以下文章,一併分享給大家。祝大家玩得開心~
相關文章
- Objective-C block 實現機制ObjectBloC
- Objective-C中的BlockObjectBloC
- Objective-C中的Block(閉包)ObjectBloC
- Objective-C中的Block回撥模式ObjectBloC模式
- block沒那麼難(一):block的實現BloC
- block實現原理BloC
- Objective-C記憶體管理:BlockObject記憶體BloC
- Objective-C Block閉包概念解析ObjectBloC
- iOS - 對 block 實現的探究iOSBloC
- 談談Objective-C中的協議Object協議
- 談談Objective-C的物件拷貝Object物件
- 使用 Block 實現 KVOBloC
- 理解 Block 實現原理BloC
- 探索iOS中Block的實現原理iOSBloC
- Objective-C Associated Objects 的實現原理Object
- Objective-C 拾遺:從Heap and Stack到BlockObjectBloC
- Objective-C 之ARC實現Object
- iOS中Block實現原理的全面分析iOSBloC
- Swift3.0 Delegate和Block的實現SwiftBloC
- iOS 揭露Block的內部實現原理iOSBloC
- block底層淺談BloC
- Objective-C基礎之四(深入理解Block)ObjectBloC
- (轉)Objective-C 拾遺:從Heap and Stack到BlockObjectBloC
- Object-C語言Block的實現方式ObjectC語言BloC
- iOS探索:Block解析淺談iOSBloC
- Block實現鏈式程式設計BloC程式設計
- 淺談 Objective-C Associated ObjectsObject
- 深入研究Block捕獲外部變數和__block實現原理BloC變數
- 用Objective-C實現雜湊表Object
- 如何在 Objective-C 的環境下實現 deferObject
- Objective-C 的底層實現(Runtime)(轉載)Object
- 談談Object類的終止器的實現!Object
- 談談 Promise 以及實現 Fetch 的思路Promise
- 大家談談struts的分頁實現吧
- Objective-C環境下的靜態陣列實現Object陣列
- Objective-C中初始化方法的實現與作用Object
- 談談fork/join實現原理
- 淺談Innodb的鎖實現