深入理解 Block
- Block 是 C 語言的擴充功能
- Block 是帶有自動變數(區域性變數)的匿名函式
本質
- Block 是一個 Objc 物件
底層實現
下面我將通過一個簡單的例子,結合原始碼進行介紹
int main(int argc, const char * argv[]) { void (^blk)(void) = ^{ printf("Hello Block\n"); }; blk(); return 0; } 複製程式碼
使用 clang -rewrite-objc main.m
,我們可以將 Objc 的原始碼轉成 Cpp 的相關原始碼:
int main(int argc, const char * argv[]) { // Block 的建立 void (*blk)(void) = (void (*)(void))&__main_block_impl_0( (void *)__main_block_func_0, &__main_block_desc_0_DATA); // Block 的使用 ((void (*)(struct __block_impl *))( (struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk); return 0; } 複製程式碼
由上面的原始碼,我們能猜想到:
__main_block_impl_0 FuncPtr
從這裡為切入點看看上面提到的都是啥
Block 的資料結構
Block 的真身:
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; // 省略了建構函式 }; 複製程式碼
- Block 其實不是一個匿名函式,他是一個結構體
-
__main_block_impl_0
名字的命名規則:__所在函式_block_impl_序號
impl 變數的資料結構
__main_block_impl_0
的主要資料:
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; 複製程式碼
-
isa
指標: 體現了 Block 是 Objc 物件的本質 。 -
FuncPtr
指標: 其實就是一個函式指標,指向所謂的匿名函式。
Desc 變數的資料結構
__main_block_desc_0
中放著 Block 的描述資訊
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) }; 複製程式碼
"匿名函式"
__main_block_impl_0
即 Block 建立時候使用到了 __main_block_func_0
正是下面的函式:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { printf("Hello Block\n"); } 複製程式碼
- 這部分和
^{ printf("Hello Block\n"); }
十分相似,由此可看出: 通過 Blocks 使用的匿名函式實際上被作為簡單的 C 語言函式來處理 - 函式名是根據 Block 語法所屬的函式名(此處
main
)和該 Block 語法在函數出現的順序值(此處為 0)來命名的。 - 函式的引數
__cself
相當於 C++ 例項方法中指向例項自身的變數this
,或是 Objective-C 例項方法中指向物件自身的變數self
,即引數__cself
為指向 Block 的變數。 - 上面的
(*blk->impl.FuncPtr)(blk);
中的blk
就是__cself
介紹了基本的資料結構,下面到回到一開始的 main
函式,看看 Block 具體的使用
Block 的建立
void (*blk)(void) = (void (*)(void))&__main_block_impl_0( (void *)__main_block_func_0, &__main_block_desc_0_DATA); /** 去掉轉換的部分 struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA); struct __main_block_impl_0 *blk = &tmp; */ 複製程式碼
-
void (^blk)(void)
就是是一個struct __main_block_impl_0 *blk
- Block 表示式的其實就是通過 所謂的匿名函式
__main_block_func_0
的函式指標 建立一個__main_block_impl_0
結構體,我們用的時候是拿到了這個結構體的指標。
Block 的使用
((void (*)(struct __block_impl *))( (struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk); /** 去掉轉換的部分 (*blk->impl.FuncPtr)(blk); */ 複製程式碼
- Block 真正的使用方法就是使用
__main_block_impl_0
中的函式指標FuncPtr
-
(blk)
這裡是傳入自己,就是給_cself
傳參
Block 的型別
從 Block 中的簡單實現中,我們從 isa
中發現 Block 的本質是 Objc 物件,是物件就有不同型別的類。因此,Block 當然有不同的型別
在 Apple 的 libclosure-73
中的 data.c
上可見, isa
可指向:
void * _NSConcreteStackBlock[32] = { 0 }; // 棧上建立的block void * _NSConcreteMallocBlock[32] = { 0 }; // 堆上建立的block void * _NSConcreteAutoBlock[32] = { 0 }; void * _NSConcreteFinalizingBlock[32] = { 0 }; void * _NSConcreteGlobalBlock[32] = { 0 }; // 作為全域性變數的block void * _NSConcreteWeakBlockVariable[32] = { 0 }; 複製程式碼
其中我們最常見的是:
Block的型別 | 名稱 | 行為 | 儲存位置 |
---|---|---|---|
_NSConcreteStackBlock | 棧Block | 捕獲了局部變數 | 棧 |
_NSConcreteMallocBlock | 堆Block | 對棧Block呼叫copy所得 | 堆 |
_NSConcreteGlobalBlock | 全域性Block | 定義在全域性變數中 | 常量區(資料段) |
PS:記憶體五大區:棧、堆、靜態區(BSS 段)、常量區(資料段)、程式碼段
關於 copy 操作
物件有 copy
操作,Block 也有 copy
操作。不同型別的 Block 呼叫 copy
操作,也會產生不同的複製效果:
Block的型別 | 副本源的配置儲存域 | 複製效果 |
---|---|---|
_NSConcreteStackBlock | 棧 | 從棧複製到堆 |
_NSConcreteGlobalBlock | 常量區(資料段) | 什麼也不做 |
_NSConcreteMallocBlock | 堆 | 引用計數增加 |
棧上的 Block 複製到堆上的時機
- 呼叫 Block 的
copy
例項方法
編譯器自動呼叫 _Block_copy
函式情況
id
PS:在 ARC 環境下,宣告的 Block 屬性用 copy
或 strong
修飾的效果是一樣的,但在 MRC 環境下用 copy 修飾。
捕獲變數
基礎型別變數
以全域性變數、靜態全域性變數、區域性變數、靜態區域性變數為例:
int global_val = 1; static int static_global_val = 2; int main(int argc, const char * argv[]) { int val = 3; static int static_val = 4; void (^blk)(void) = ^{ printf("global_val is %d\n", global_val); printf("static_global_val is %d\n", static_global_val); printf("val is %d\n", val); printf("static_val is %d\n", static_val); }; blk(); return 0; } 複製程式碼
轉換後“匿名函式”對應的程式碼:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int val = __cself->val; // bound by copy int *static_val = __cself->static_val; // bound by copy printf("global_val is %d\n", global_val); printf("static_global_val is %d\n", static_global_val); printf("val is %d\n", val); printf("static_val is %d\n", (*static_val)); } 複製程式碼
- 全域性變數、靜態全域性變數 : 作用域為全域性,因此在 Block 中是直接訪問的。
- 區域性變數 : 生成的
__main_block_impl_0
中存在val
例項,因此對於區域性變數,Block 只是單純的複製建立時候 區域性變數的瞬時值 ,我們可以使用值,但不能修改值。
struct __main_block_impl_0 { // ... int val; // 值傳遞 // ... }; 複製程式碼
- 靜態區域性變數 : 生成的
__main_block_impl_0
中存在static_val
指標,因此 Block 是在建立的時候獲取 靜態區域性變數的指標值 。
struct __main_block_impl_0 { // ... int *static_val; // 指標傳遞 // ... }; 複製程式碼
物件型別變數
模仿基礎型別變數,例項化四個不一樣的 SCPeople
變數:
int main(int argc, const char * argv[]) { // 省略初始化 [globalPeople introduce]; [staticGlobalPeople introduce]; [people introduce]; [staticPeople introduce]; return 0; } 複製程式碼
轉換後"匿名函式"對應的程式碼:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { SCPeople *people = __cself->people; // bound by copy SCPeople **staticPeople = __cself->staticPeople; // bound by copy // 省略 objc_msgSend 轉換 [globalPeople introduce]; [staticGlobalPeople introduce]; [people introduce]; [*staticPeople introduce]; } 複製程式碼
- 全域性物件、靜態全域性物件 : 作用域依然是全域性,因此在 Block 中是直接訪問的。
- 區域性物件 : 生成的
__main_block_impl_0
中存在people
指標例項,因此 Block 獲取的是 指標瞬間值 ,我們可以在 Block 中通過指標可以操作物件,但是不能改變指標的值。
struct __main_block_impl_0 { // ... SCPeople *people; // ... }; 複製程式碼
- 靜態區域性物件 : 生成的
__main_block_impl_0
中存在staticPeople
指標的指標,因此 Block 是在建立的時候獲取 靜態區域性物件的指標值 (即指標的指標)。
struct __main_block_impl_0 { // ... SCPeople **staticPeople; // ... }; 複製程式碼
小結
通過對基礎型別、物件型別與四種不同的變數進行排列組合的小 Demo,不難得出下面的規則:
變數型別 | 是否捕獲到 Block 內部 | 訪問方式 |
---|---|---|
全域性變數 | 否 | 直接訪問 |
靜態全域性變數 | 否 | 直接訪問 |
區域性變數 | 是 | 值訪問 |
靜態區域性變數 | 是 | 指標訪問 |
PS:
- 基礎型別和物件指標型別其實是一樣的,只不過指標的指標看起來比較繞而已。
- 全域性變數與靜態全域性變數的儲存方式、生命週期是相同的。但是作用域不同,全域性變數在所有檔案中都可以訪問到,而靜態全域性變數只能在其申明的檔案中才能訪問到。
變數修改
上面的篇幅通過底層實現,向大家介紹了 Block 這個所謂"匿名函式"是如何捕獲變數的,但是一些時候我們需要修改 Block 中捕獲的變數:
修改全域性變數或靜態全域性變數
全域性變數與靜態全域性變數的作用域都是全域性的,自然在 Block 內外的變數操作都是一樣的。
修改靜態區域性變數
在上面變數捕獲的章節中,我們得知 Block 捕獲的是靜態區域性變數的指標值,因此我們可以在 Block 內部改變靜態區域性變數的值(底層是通過指標來進行操作的)。
修改區域性變數
使用 __block
修飾符來指定我們想改變的區域性變數,達到在 Block 中修改的需要。
我們用同樣的方式,通過底層實現認識一下 __block
,舉一個:chestnut::
__block int val = 0; void (^blk)(void) = ^{ val = 1; }; blk(); 複製程式碼
經過轉換的程式碼中出現了和單純捕獲區域性變數不同的程式碼:
__Block_byref_val_0
結構體
struct __Block_byref_val_0 { void *__isa; // 一個 Objc 物件的體現 __Block_byref_val_0 *__forwarding; // 指向該例項自身的指標 int __flags; int __size; int val; // 原區域性變數 }; 複製程式碼
- 編譯器會將
__block
修飾的變數包裝成一個 Objc 物件。
val
轉換成 __Block_byref_val_0
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = { (void*)0, (__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 0 }; 複製程式碼
__main_block_impl_0
捕獲的變數
struct __main_block_impl_0 { // ... __Block_byref_val_0 *val; // by ref // ... }; 複製程式碼
- Block的
__main_block_impl_0
結構體例項持有指向__block
變數的__Block_byref_val_0
結構體例項的指標。 - 這個捕獲方式和捕獲靜態區域性變數相似,都是指標傳遞
"匿名函式"的操作
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) = 1; } 複製程式碼
(val->__forwarding->val) 解釋
- 左邊的
val
是__main_block_impl_0
中的val
,這個val
通過__block int val
的地址初始化 - 右邊的
val
是__Block_byref_val_0
中的val
,正是__block int val
的val
-
__forwarding
在這裡只是單純指向了自己而已
__forwarding 的存在意義
上面的"棧Blcok"中 __forwarding
在這裡只是單純指向自己,但是在當"棧Blcok"複製變成"堆Block"後, __forwarding
就有他的存在意義了:

__block
修飾符不能用於修飾全域性變數、靜態變數。
記憶體管理
Block 與物件型別
copy & dispose
眾所周知,物件其實也是使用一個指標指向物件的儲存空間,我們的物件值其實也是指標值。雖然是看似物件型別的捕獲與基礎型別的指標型別捕獲差不多,但是捕獲物件的轉換程式碼比基礎指標型別的轉換程式碼要多。 ( __block
變數也會變成一個物件,因此下面的內容也適用於 __block
修飾區域性變數的情況) 。多出來的部分是與記憶體管理相關的 copy
函式與 dispose
函式:
底層實現
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { _Block_object_assign((void*)&dst->people, (void*)src->people, 3/*BLOCK_FIELD_IS_OBJECT*/); _Block_object_assign((void*)&dst->staticPeople, (void*)src->staticPeople, 3/*BLOCK_FIELD_IS_OBJECT*/); } static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->people, 3/*BLOCK_FIELD_IS_OBJECT*/); _Block_object_dispose((void*)src->staticPeople, 3/*BLOCK_FIELD_IS_OBJECT*/); } 複製程式碼
這兩個函式在 Block 資料結構存在於 Desc
變數中:
static struct __main_block_desc_0 { // ... void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); void (*dispose)(struct __main_block_impl_0*); }; // 省略了初始化好的結構體 複製程式碼
函式呼叫時機
函式 | 呼叫時機 |
---|---|
copy 函式 | 棧上的 Block 複製到堆時 |
dispose 函式 | 堆上的 Block 被廢棄時 |
函式意義
-
copy
函式中的_Block_object_assign
函式相當於記憶體管理中的retain
函式,將物件賦值在物件型別的結構體成員變數中。 -
dispose
函式中的_Block_object_dispose
函式相當於記憶體管理中的release
函式,釋放賦值在物件型別的結構體變數中的物件。 - 通過
copy
和dispose
並配合 Objc 執行時庫對其的呼叫可以實現記憶體管理
※ 例子
當 Block 內部訪問了物件型別的區域性變數時:
- 當 Block 儲存在棧上時 : Block 不會對區域性變數產生強引用。
- 當 Block 被
copy
到堆上時 : Block 會呼叫內部的copy
函式,copy
函式內部會呼叫_Block_object_assign
函式,_Block_object_assign
函式會根據區域性變數的修飾符(__strong
、__weak
、__unsafe_unretained
)作出相應的記憶體管理操作。(注意: 多個 Block 對同一個物件進行強引用的時,堆上只會存在一個該物件) - 當 Block 從堆上被移除時 : Block 會呼叫內部的
dispose
函式,dispose
函式內部會呼叫_Block_object_dispose
函式,_Block_object_dispose
函式會自動release
引用的區域性變數。(注意: 直到被引用的物件的引用計數為 0,這個堆上的該物件才會真正釋放)
PS:對於 __block
變數,Block 永遠都是對 __Block_byref_區域性變數名_0
進行強引用。如果 __block
修飾符背後還有其他修飾符,那麼這些修飾符是用於修飾 __Block_byref_區域性變數名_0
中的 區域性變數
的。
現象:Block 中使用的賦值給附有 __strong
修飾符的區域性變數的物件和複製到堆上的 __block
變數由於被堆的 Block 所持有,因而可超出其變數作用域而存在。
迴圈引用
由於 Block 內部能強引用捕獲的物件,因此當該 Block 被物件強引用的時候就是注意以下的引用迴圈問題了:

ARC 環境下解決方案
-
弱引用持有:使用
__weak
或__unsafe_unretained
捕獲物件解決-
weak
修飾的指標變數,在指向的記憶體地址銷燬後,會在 Runtime 的機制下,自動置為nil
。 -
_unsafe_unretained
不會置為nil
,容易出現懸垂指標,發生崩潰。但是_unsafe_unretained
比__weak
效率高。
-
-
使用
__block
變數 :使用__block
修飾物件,在 block 內部用完該物件後,將__block
變數置為nil
即可。雖然能控制物件的持有期間,並且能將其他物件賦值在__block
變數中,但是必須執行該 block。(意味著這個物件的生命週期完全歸我們控制)
MRC 環境下解決方案
- 弱引用持有:使用
__unsafe_unretained
捕獲物件 - 直接使用
__block
修飾物件,無需手動將物件置為nil
,因為底層_Block_object_assign
函式在 MRC 環境下對 block 內部的物件不會進行retain
操作。
MRC 下的 Block
ARC 無效時,需要手動將 Block 從棧複製到堆,也需要手動釋放 Block
- 對於棧上的 Block 呼叫
retain
例項方法是不起作用的 - 對於棧上的 Block 需要呼叫一次
copy
例項方式(引用計數+1),將其配置在堆上,才可繼續使用retain
例項方法 - 需要減少引用的時候,只需呼叫
release
例項方法即可。 - 對於在 C 語言中使用 Block,需要使用
Block_copy
和Block_release
代替copy
和release
。