0.前言
日常開發中經常會用到 Block,但如果對它的底層實現沒有深入地挖掘過,就不能算是真正掌握,本篇就來探究一下 Block 的底層實現原理。
1.舉個 ??
先來看一個例子,下邊是一種簡單的 block 使用場景: 無參數、無返回值的 block。
typedef void(^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 30;
// 創建
MyBlock blk = ^{
NSLog(@"My age is %d .", age);
};
// 執行
blk();
}
return 0;
}
2.Block的實質
為了探究 Block 的本質,我們需要借助 clang 將含有 Block 語法的源代碼轉換成 C++ 代碼。
2.1 Block 的底層結構
終端執行 $ clang -rewrite-objc main.m
命令,就可以將 main.m
文件編譯生成 main.cpp
文件,這里截取了 main.cpp
文件中與 block 相關的代碼,并添加了部分注釋:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
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)};
// Block 的結構體
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
// 構造函數
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// block 的 { } 里邊的代碼構成的函數
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_d9ff54_mi_0, age);
}
// main() 函數
int main(int argc, const char * argv[]) {
/* @autoreleasepool */
{
__AtAutoreleasePool __autoreleasepool;
int age = 30;
MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
return 0;
}
下面開始一步步討論源碼。從 main() 函數開始,關于自動釋放池的代碼不在此處討論,先看一下 block 的創建過程:
MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
// 簡化后的代碼:
MyBlock blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age);
我們注意到這里實際上有兩個函數:__main_block_impl_0()
和 __main_block_func_0()
。
先來看后者,具體代碼如下,實際是 block 的 { }
里邊的代碼構成的函數。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_d9ff54_mi_0, age);
}
然后搜索前一個函數的函數名 __main_block_impl_0
,發現它位于下邊這個結構體里邊:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
// 構造函數
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock; // 指明該 block 的類型(此處是棧上的 block)。
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
而這個結構體就是 block 經編譯后得到的結構,很明顯 __main_block_impl_0() 是它的構造函數,我們發現,這個構造函數里邊都是在給 block 的前 3 個元素 (2 個結構體和一個 int age) 賦值。
第一個元素 impl ,它的組成是這樣的:
struct __block_impl {
void *isa; // 用于說明 block 的類型
int Flags; // 標識位
int Reserved; // 保留字段
void *FuncPtr; // 指針
};
FuncPtr
是一個指針,根據名字推斷應該是一個函數指針,結合main()
函數中執行構造函數創建 block 的過程可以看出,FuncPtr
指向的是 block 的{ }
里邊的代碼構成的函數。isa
指明了block 的類型,構造函數中給它賦的值是&_NSConcreteStackBlock
,說明他是棧上的 block。關于 block 的類型,下一小節就會講到。
第二個元素 Desc
是 __main_block_desc_0
類型的結構體,如下所示:
static struct __main_block_desc_0 {
size_t reserved; // 保留字段
size_t Block_size; // block 的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
其中 Block_size
從名字推斷應該是結構體的大小,緊隨其后定義了一個 __main_block_desc_0
類型的變量 __main_block_desc_0_DATA,它的第二個元素值就是當前 block 的大小 sizeof(struct __main_block_impl_0)
,從 main()
函數中執行 block 構造函數的語句可以看出,__main_block_desc_0_DATA 最終賦值給了 block 中的 Desc
,進一步驗證了 Block_size 中存放的是 block 的大小。
第三個元素 int age
是 block 捕獲的一個 auto 變量,關于捕獲變量的機制,后面會詳細討論。
最后回到 main()
函數的最后一行代碼:
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
// 簡化后:
blk->FuncPtr(blk);
很明顯這是執行 blk 里邊的指針 FuncPtr 指向的函數,而且將 blk 自己傳了進去,這樣,就可以在函數內部訪問到 block 捕獲的變量,如前文提到的 int age。
至此,本文開頭的 block 的底層結構基本介紹完了,看起來比較零散,這里繪制了一張總圖做個簡單小結:
2.2 Block 的類型
在此,簡單說明一下 block 的類型,block 的 3 種類型及其內存分布如下:
那么這 3 種類型的 Block 有什么區別呢,為了搞清楚這個問題,我們需要先回顧一下 4 種常見的變量類型及其代碼示例:
- 自動變量(auto 變量)
- 靜態局部變量
- 全局變量
- 靜態全局變量
// *** 4 中變量的代碼示例:
// 全局變量
int global_var = 10;
// 靜態全局變量
static int static_global_var = 20;
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 自動變量(局部變量)
int local_var = 30; // <==> auto int local_var = 30;
// 靜態局部變量
static int local_static_var = 40;
MyBlock blk = ^{
NSLog(@"\n global_var: %d\n static_global_var: %d\n local_var: %d\n local_static_var: %d\n", global_var, static_global_var, local_var, local_static_var);
};
blk();
}
return 0;
}
關于各種 Block 的區別,可以簡單匯總成下邊的圖表:
也就是說:
- 如果 Block 里邊訪問了 auto 變量,那么他就是棧上的 Block;
- 如果沒有訪問 auto 變量,就是全局的 Block;
- 如果對棧上的 Block 執行了 copy 操作,就變成了堆上的 Block。
2.3 Block 的 copy
上文提到了對棧上 Block 的 copy 操作,那么為什么需要 copy 呢?原因是:設置在棧上的 Block 如果其所屬的作用域結束,該 Block 就會被廢棄,為了延長它的生命周期,就需要將其復制到堆上。
既然棧上的 block 經 copy 后會從棧上復制到堆上,那么另外兩種 Block 執行 copy 操作又會發生什么呢? 每一種 Block 被 copy 后的結果如下:
ARC 環境下,編譯器會根據情況自動將棧上的 block 復制到堆上,比如滿足一下條件之一時:
- block 作為函數返回值時;
- 將 block 賦值給 __strong 指針時;
- block 作為 Cocoa API 中方法名含有 usingBlock 的方法參數時;
- block 作為 GCD API 的方法參數時。
MRC 環境下,需要手動調用 block 的 copy 操作,才能將棧上的 block 復制到堆上。
3.變量捕獲
為了保證 Block 內部能夠正常訪問外部的變量,block有個變量捕獲機制,我們以前邊介紹常見變量類型的代碼為例,看看 Block 是怎么捕獲變量的。
執行 clang -rewrite-objc main.m
之后,轉換的 block 的源碼如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 捕獲的變量
int local_var;
int *local_static_var;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _local_var, int *_local_static_var, int flags=0) : local_var(_local_var), local_static_var(_local_static_var) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
從上邊的源碼可以看出來,對于這 4 種不同的變量,實際只捕獲了自動變量 local_var
和靜態局部標量 local_static_var
,而且自動變量是捕獲了值,靜態局部變量捕獲的是變量地址。至于為什么這么設計,推測可能的原因如下:
3.1 捕獲自動變量
實際開發中,block 捕獲到的變量基本都是自動變量(局部變量),理由是:對于全局變量,任何地方都可以訪問它,不安全;對于靜態局部變量,它會一直存在于內存中,對內存是一種浪費。
對于基本數據類型的自動變量,前邊已經講過了,就是簡單的值捕獲,接下來我們重點討論一下對象類型 auto 變量的捕獲。
下邊是 block 訪問外部對象類型 auto 變量的簡單實例。
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
MyBlock blk = ^{
NSLog(@"%@", obj);
};
blk();
}
return 0;
}
執行 clang -rewrite-objc main.m
后,生成的源碼中有這 2 點不同:
- 捕獲了 NSObject *obj;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSObject *obj;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *_obj, int flags=0) : obj(_obj) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
- __main_block_desc_0 中增加了兩個指針
copy
和dispose
,整個文件里新增了 2 個函數__main_block_copy_0()
和__main_block_dispose_0()
,結合上下問可以知道,這兩個函數地址最終傳給了 block 里 Desc 中的copy
和dispose
這 2 個指針。
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
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
};
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
這兩個函數調用時機和作用如下:
如果 block 從棧上拷貝到堆上
會調用 block 內部的copy()
函數,此函數內部會調用_Block_object_assign()
函數,它會根據 auto 變量的修飾符(__strong、__weak、__unsafe_unretained)做出相應的操作,形成 強引用 或 弱引用。如果 block 從堆上移除
會調用 block 內部的dispose()
函數,此函數內部會調用_Block_object_dispose()
函數,它會自動釋放引用的 auto 變量(即 release)。
另外,如果 block 一直是在棧上,將不會對 auto 變量產生強引用。
3.2 捕獲 __block 變量
前邊我們只是在 block 內部使用
變量,事實上,如果直接修改
變量的話,比如下邊這個例子,就會報錯:此變量不可賦值 (錯誤信息見注釋)。
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
MyBlock blk = ^{
age = 20; // Error: Variable is not assignable (missing __block type specifier)
NSLog(@"%d", age);
};
blk();
}
return 0;
}
按照錯誤信息的提示,如果給 int age = 10;
前邊加上 __block
,就可以解決 block 內部無法修改 auto 變量的問題,實際操作后,發現果然可以正常輸出 age 的新值 20。
3.2.1 __block 變量能夠被 block 修改的原因
現在來看看 __block 修飾符到底做了什么,先將上邊的代碼轉成 C++ 源碼,下邊截取了其中部分關鍵代碼:
// 新出現的結構體
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__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_age_0 *age = __cself->age; // bound by ref
(age->__forwarding->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_1dfa13_mi_0, (age->__forwarding->age));
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
return 0;
}
先看 main()
函數,__block int age
變成了 __Block_byref_age_0
類型的 age
,也就是說編譯器將 __block 修飾的變量包裝成了一個新的結構:
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
__Block_byref_age_0
這個結構體里邊有一個 int age ,用于存儲 age 的值 (10)。還有一個重要成員 __Block_byref_age_0 *__forwarding;
,結合 block 的構造函數,我們知道 __forwarding 指針實際指向了它所在的結構體。
之所以這么做是為了當 block 被拷貝到堆上以后,無論訪問棧上的 block 還是 堆上的 block,最終都是訪問的堆上的同一個 block(拷貝后,堆上的 __forwarding 指向自己所在的 __block 變量,棧上的 __forwarding 指向堆上的 __block 變量),如下圖所示。
接下來,看看 block 的結構 __main_block_impl_0
,里邊多了一個變量 __Block_byref_age_0 *age;
,即 block 捕獲了這個新的結構體 __Block_byref_age_0
的地址,所以 block 里邊就可以通過地址訪問這個結構體,進而修改里邊 int age 的值。
__block 修飾的對象類型的 auto 變量與此類似,差別僅在于新生成的結構體:
從上圖可知,__block 修飾的對象類型轉換后的結構體里邊多了兩個函數指針,他們分別指向下面 2 個函數,負責內存管理的相關操作,下邊就會講到。
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
3.2.2 被 __block 修飾的對象類型的內存管理
對于 被 __block 修飾的對象類型,內存管理分以下 3 種情況:
1.當 __block 變量 在棧上時,不會對指向的對象產生強引用。
-
2.當 __block 變量 被 copy 到堆時,分兩種情況:
- ARC 環境下,會調用
__block 變量內部
的 copy 函數,它會調用_Block_object_assign()
函數,此函數會根據所指向對象的修飾符(__strong、__weak、__unsafe_unretained)做出相應的操作,形成強引用(retain)或者弱引用。
- ARC 環境下,會調用
- MRC 環境下,不會形成強引用(retain)。
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block NSObject *obj = [[NSObject alloc] init];
MyBlock block = [^{
NSLog(@"%p", obj);
} copy];
block();
[obj release];
}
return 0;
}
如上所示,在 MRC 環境下,對象前加了 __block,不會對 block 形成強引用, 即當執行完 [obj release];
之后,person 就被釋放了。
- 3.如果 __block 變量從堆上移除,會調用
__block 變量內部
的dispose()
函數,它會調用_Block_object_dispose()
函數,此函數會自動釋放指向的對象(release)
3.2.3 對象類型的auto變量 和 __block 變量
當block在棧上時,對它們都不會產生強引用
-
當 block 拷貝到堆上時,都會通過 copy 函數來處理它們
對于 __block變量(假設變量名叫做a),最終會執行
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
-
對于對象類型的 auto 變量(假設變量名叫做p),最終會執行
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
以上兩者最終調用的方法是相同的,只不過最后一個參數有差別,前者是 8(表示引用類型),后者是 3 (表示對象),下面對
_Block_object_dispose()
函數的調用與之類似。
-
當 block 從堆上移除時,都會通過 dispose 函數來釋放它們
對于 __block變量(假設變量名叫做a),最終會執行
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
對于對象類型的auto變量(假設變量名叫做p),最終會在執行
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
4.循環引用
關于使用 block 可能遇到的循環引用問題,我們分 ARC 和 MRC 兩種情況進行討論。
ARC 環境
在 ARC 環境下,目前大概有 3 種常見的解決循環引用的方式:
- __weak
此方式是最常見也是推薦使用的一種方式,基本原理是,self 對 block 的引用維持強引用,不過將 block 對 self 的引用改成了弱引用。
__weak typeof(self) weakSelf = self;
self.block = ^{
printf("%p", weakSelf);
};
- __unsafe_unreturned
這種方式與上邊的方式類似,不過當 weakSelf 指向的對象銷毀后,指針已然指向那塊已經被回收的內存,可能發生野指針錯誤,所以是不安全的。
__unsafe_unretained typeof(self) weakSelf = self;
self.block = ^{
printf("%p", weakSelf);
};
- __block
__block typeof(self) weakSelf = self;
self.block = ^{
printf("%p", weakSelf);
weakSelf = nil;
};
self.block();
我們知道,當在變量前邊加了 __block 之后就多了一個 __blcok 變量,于是里邊的引用關系就變成了:
為了打破這個循環引用的關系,需要在 block 里邊將對象置為 nil,而且必須執行 block 才能斷開 __block 變量對對象的強引用。
MRC 環境
MRC 環境下解決循環引用的方式與 ARC 環境類似,只是由于 MRC 環境下不可以使用 weak,所以只有 __unsafe_unreturned
和 __block
2 種解決方式。對于 __block
的方式,在MRC中,__block 變量
不會對 weakSelf 產生強引用,也就不需要將其置為 nil 并執行 block 了。
__block typeof(self) weakSelf = self;
self.block = ^{
printf("%p", weakSelf);
};
5.小結
以上就是對 Block 底層實現的一個簡單討論,受自己的知識積累所限,難免有理解不到位的地方,后期會及時修正。