作者:雪山飛狐_91ae
連結:https://www.jianshu.com/p/1f271940e5cc
Block在iOS開發中的用途非常廣,今天我們就來一起探索一下Block的底層結構。
1、Block的底層結構
下麵是一個沒有引數和傳回值的簡單的Block:
int main(int argc, char * argv[]) {
@autoreleasepool {
void (^block)(void) = ^{
NSLog(@"Hello World!");
};
block();
return 0;
}
}
為了探索Block的底層結構,我們將main.m檔案轉化為C++的原始碼、我們開啟命令列。cd到包含main.m檔案的檔案夾,然後輸入:clang -rewrite-objc main.m
,這個時候在該檔案夾的目錄下會生成main.cpp檔案。
這個檔案非常長,我們直接拉到檔案的最下麵,找到main函式:
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
//定義block
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
//呼叫block
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
}
這第一行程式碼是定義一個block變數,第二行程式碼是呼叫block。這兩行程式碼看起來非常複雜。但是我們可以去簡化一下,怎麼簡化呢?
變數前面的()一般是做強制型別轉換的,比如在呼叫block這一行,
block
前面有一個()是(__block_impl *),這就是進行了一個強制型別轉換,將其轉換為一個_block_impl
型別的結構體指標,那像這樣的強制型別轉換非常妨礙我們理解程式碼,我們可以暫時將這些強制型別轉換去掉,這樣可以幫助我們理解程式碼。
化簡後的程式碼如下:
//定義block
void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
//呼叫block
block->FuncPtr(block);
這樣化簡後的程式碼就要清爽多了。我們一句一句的看,先看第一句:
void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
這句程式碼的意思好像就是呼叫_main_block_impl_0
這個函式,給這個函式傳進兩個引數_main_block_func_0
和&_main_block_desc_0_DATA
,然後得到這個函式的傳回值,取函式傳回值的地址,賦值給block這個指標。
我們在稍微上一點的位置可以找到_main_block_impl_0
這個結構:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
//建構式,類似於OC的init方法
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__block_impl這個結構體的結構我們可以command+f在main.cpp檔案中搜索得到:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
_main_block_desc_0結構體的結構在main.cpp檔案的最下麵可以找到:
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)};
這是一個C++的結構體。而且在這個結構體內還包含一個函式,這個函式的函式名和結構體名稱一致,這在C語言中是沒有的,這是C++特有的。
在C++的結構體包含的函式稱為結構體的建構式,它就相當於是OC中的init方法,用來初始化結構體。OC中的init方法傳回的是物件本身,C++的結構體中的構造方法傳回的也是結構體物件。
那麼我們就知道了,__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
傳回的就是_main_block_impl_0
這個結構體物件,然後取結構體物件的地址賦值給block指標。換句話說,block指向的就是初始化後的
_main_block_impl_0
結構體物件。
我們再看一下初始化_main_block_impl_0
結構體傳進去的引數:
-
第一個引數是
_main_block_func_0
,這個引數的結構在上面一點的位置也能找到:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_74_wk04zv690mz36wn0g18r5nxm0000gn_T_main_3b803f_mi_0);
}
這個函式其實就是把我們Block中要執行的程式碼封裝到這個函式內部了。我們可以看到這個函式內部就一行程式碼,就是一個NSlog函式,這也就是NSLog(@"Hello World!");
這句程式碼。
把這個函式指標傳給_main_block_impl_0
的建構式的第一個引數,然後用這個函式指標去初始化_main_block_impl_0
這個結構體的第一個成員變數impl
的成員變數FuncPtr
。也就是說FuncPtr
這個指標指向_main_block_func_0
這個函式。
-
第二個引數是
&_main_block_desc_0_DATA
。
我們看一下這個結構:
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)};
在結構體的建構式中,0賦值給了sizeof(struct __main_block_impl_0)
是賦值給了Block_size,可以看出這個結構體存放的是_main_block_impl_0
這個結構體的資訊。在
_main_block_impl_0
的建構式中我們可以看到,_main_block_desc_0
這個結構體的地址被賦值給了_main_block_impl_0
的第二個成員變數Desc
這個結構體指標。也就是說Desc這個結構體指標指向
_main_block_desc_0_DATA
這個結構體。
那麼我們總結一下:
所以第一句定義block
void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
總結起來就是:
-
1.建立一個函式
_main_block_func_0
,這個函式
的作用就是將我們block中要執行的程式碼封裝到函式內部,方便呼叫。 -
2.建立一個結構體
_main_block_desc_0
,這個結構體中主要包含_main_block_impl_0
這個結構體佔用的儲存空間大小等資訊。
-
3.將1中建立的
_main_block_func_0
這個函式的地址,和2中建立的_main_block_desc_0
這個結構體的地址傳給_main_block_impl_0
的建構式。
-
4.利用
_main_block_func_0
初始化_main_block_impl_0
結構體的第一個成員變數impl
的成員變數FuncPtr
。這樣_main_bck_impl_0
這個結構體也就得到了block中那個程式碼塊的地址。
-
5.利用
_mian_block_desc_0_DATA
去初始化_mian_block_impl_0
的第二個成員變數Desc
。
下麵我們再看第二步呼叫block:
block->FuncPtr(block);
我們知道,block實質上就是指向_main_block_impl_0
這個結構體的指標,而FuncPtr是_main_block_impl_0
的第第一個成員變數impl
的成員變數,正常來講,block想要呼叫自己的成員變數的成員變數的成員變數,應該像下麵這樣呼叫:
block->impl->FuncPtr
然而事實卻不是這樣,這是為什麼呢?
原因就在於之前我們把所有的強制型別轉換給刪掉了,之前block前面的()是(__block_impl *),為什麼可以這樣強制轉換呢?因為block指向的是
_main_block_impl_0
這個結構體的首地址,而_main_block_impl_0
的第一個成員變數是struct __block_impl impl;
,所以impl和_main_block_impl_0
的首地址是一樣的,因此指向_main_block_impl_0
的首地址的指標也就可以被強制轉換為指向impl的首地址的指標。
之前說過,FuncPtr這個指標在建構式中是被初始化為指向_mian_block_func_0
這個函式的地址。因此透過block->FuncPtr
呼叫也就獲取了_main_block_func_0
這個函式的地址,然後對_main_block_func_0
進行呼叫,也就是執行block中的程式碼了。這中間block又被當做引數傳進了_main_block_func_0
這個函式。
2、變數捕獲-auto變數
auto變數是宣告在函式內部的變數,比如int a = 0;;
這句程式碼宣告在函式內部,那a就是auto變數,等價於auto int a = 0;
auto變數時分配在棧區,當超出作用域時,其佔用的記憶體會被系統自動銷毀並生成。下麵看一段程式碼:
int a = 10;
void (^block)(void) = ^{
NSLog(@"%d", a);
};
a = 20;
block();
這是一個很簡單的Block捕獲自動變數的例子,我們看一下列印結果:
2018-09-04 20:39:45.436534+0800 copytest[17163:477148] 10
自動變數a的值明明已經變為了20,為什麼輸出結果還是10呢?我們把這段程式碼轉化為C++的原始碼看看。
int main(int argc, char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; int a = 10; void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a)); a = 20; ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); return 0; }}
我們還是把程式碼化簡一下來看:
int a = 10;
void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a);
a = 20;
block->FuncPtr)(block);
對比一下上面分析的沒有捕獲自動變數的原始碼,我們發現這裡_main_block_impl_0中傳入的引數多了一個a。然後我們往上翻看看_main_block_impl_0的結構:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a; //這是新加入的成員變數
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
在_main_block_impl_0這個結構體中我們發現多了一個int型別的成員變數a,在結構體的建構式中多了一個引數int _a,並且用這個int _a去初始化成員變數a。
所以在
中傳入了自動變數a用來初始化_main_block_impl_0的成員變數a。那這個時候_main_block_impl_0的成員變數a就被賦值為10了。void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a);
由於上面這一步是值傳遞,所以當執行a = 20
時,_main_block_impl_0結構體的成員變數a的值是不會隨之改變的,仍然是10。
然後我們再來看一下_main_block_func_0的結構有何變化:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_74_wk04zv690mz36wn0g18r5nxm0000gn_T_main_fb5f0d_mi_0, a);
}
可以看到,透過傳入的_main_block_impl_0這個結構體獲得其成員變數a的值。
3、變數捕獲-static變數
上面講的捕獲的是自動變數,在函式內部宣告的變數預設為自動變數,即預設用auto修飾。那麼如果在函式內部宣告的變數用static修飾,又會帶來哪些不同呢?static變數和auto變數的不同之處在於變數的記憶體的回收時機。auto變數在其作用域結束時就會被系統自動回收,而static變數在變數的作用域結束時並不會被系統自動回收。
先看一段程式碼:
static int a = 10;
void (^block)(void) = ^{
NSLog(@"%d", a);
};
a = 20;
block();
我們看一下列印結果:
2018-09-04 21:09:40.440020+0800 copytest[17949:499740] 20
結果是20,這個和2中的列印結果不一樣,為什麼區域性變數從auto變成了static結果會不一樣呢?我們還是從原始碼來分析:
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
static int a = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a;));
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
}
我們把程式碼化簡一下:
static int a = 10;
void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &a;);
a = 20;
block->FuncPtr(block);
和2不一樣的是,這裡傳入_main_block_impl_0的是&a;,也即是a這個變數的地址值。那麼這個&a;是賦值給誰了呢?我們上翻找到_main_block_impl_0的結構:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
這裡我們可以看到結構體多了一個指標型別的成員變數int *a,然後在建構式中,將傳遞過來的&a;,賦值給這個指標變數。也就是說,在_main_block_impl_0這個結構體中多了一個成員變數,這個成員變數是指標,指向a這個變數。所以當a變數的值發生變化時,能夠得到最新的值。
4、變數捕獲-全域性變數
2和3分析了兩種型別的區域性變數,auto區域性變數和static區域性變數。這一部分則分析全域性變數。全域性變數會不會像區域性變數一樣被block所捕獲呢?我們還是看一下實體:
int height = 10;static int weight = 20;int main(int argc, char * argv[]) { @autoreleasepool { void (^block)(void) = ^{ NSLog(@"%d %d", height, weight); }; height = 30; weight = 40; block(); return 0; }}
列印結果:
2018-09-04 21:41:19.016278+0800 copytest[18774:524773] 30 40
我們還是檢視一下原始碼:
int height = 10;
static int weight = 20;
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
height = 30;
weight = 40;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
}
這裡我們可以看到,height和weight這兩個全域性變數沒有作為引數傳入_main_block_impl_0中去。然後我們再檢視一下_main_block_impl_0的結構:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到,_main_block_impl_0中並沒有增加成員變數。然後我們再看_main_block_func_0的結構:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_74_wk04zv690mz36wn0g18r5nxm0000gn_T_main_46c51b_mi_0, height, weight);
}
可以看到,這個地方在呼叫的時候是直接呼叫的全域性變數height和weight。
所以我們可以得出結論,block並不會不會全域性變數。
總結:
變數型別 | 是否捕獲到block內部 | 訪問方式 |
---|---|---|
區域性變數auto | 是 | 值傳遞 |
區域性變數static | 是 | 指標傳遞 |
全域性變數 | 否 | 直接訪問 |
思考 為什麼對於不同型別的變數,block的處理方式不同呢?
這是由變數的生命週期決定的。對於自動變數,當作用域結束時,會被系統自動回收,而block很可能是在超出自動變數作用域的時候去執行,如果之前沒有捕獲自動變數,那麼後面執行的時候,自動變數已經被回收了,得不到正確的值。對於static區域性變數,它的生命週期不會因為作用域結束而結束,所以block只需要捕獲這個變數的地址,在執行的時候透過這個地址去獲取變數的值,這樣可以獲得變數的最新的值。gao’mi而對於全域性變數,在任何位置都可以直接讀取變數的值。思考 為什麼對於不同型別的變數,block的處理方式不同呢?
5、變數捕獲-self變數
看下麵一段程式碼:
@implementation Person
- (void)test{
void(^block)(void) = ^{
NSLog(@"%@", self);
};
}
@end
這個Person類中只有一個東西,就是test這個函式,那麼這個block有沒有捕獲self變數呢?
要搞清這個問題,我們只需要知道搞清楚這裡self變數是區域性變數還是全域性變數,如果是區域性變數,那麼是一定會捕獲的,而如果是全域性變數,則一定不會被捕獲。
我們把這個Person.m檔案轉化為c++的原始碼,然後找到test函式在c++中的表示:
static void _I_Person_test(Person * self, SEL _cmd) {
void(*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));
}
我們可以看到,本來Person.m中,這個test函式我是沒有傳任何引數的,但是轉化為c++的程式碼後,這裡傳入了兩個引數,一個是self引數,一個是_cmd。self很常見,_cmd表示test函式本身。所以我們就很清楚了,self是作為引數傳進來,也就是區域性變數,那麼block應該是捕獲了self變數,事實是不是這樣呢?我們只需要檢視一下_Person_test_block_impl_0的結構就可以知道了。
_Person_test_block_impl_0的結構:
struct __Person__test_block_impl_0 {
struct __block_impl impl;
struct __Person__test_block_desc_0* Desc;
Person *self;
__Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到,self確實是作為成員變數被捕獲了。
6、Block的型別
前面已經說過了,Block的本質就是一個OC物件,既然它是OC物件,那麼它就有型別。
在搞清楚Block的型別之前,先把ARC關掉,因為ARC幫我們做了太多的事,不方便我們觀察結果。關掉ARC的方法在Build Settings裡面搜尋Objective-C Automatic Reference Counting,把這一項置為NO。
int height = 10;
static int weight = 20;
int main(int argc, char * argv[]) {
@autoreleasepool {
int age = 10;
void (^block)(void) = ^{
NSLog(@"%d %d", height, age);
};
NSLog(@"%@
%@
%@
%@", [block class], [[block class] superclass], [[[block class] superclass] superclass], [[[[block class] superclass] superclass] superclass]);
return 0;
}
}
上面的程式碼的列印結果是:
__NSStackBlock__
__NSStackBlock
NSBlock
NSObject
這說明上面定義的這個block的型別是NSStackBlock,並且它最終繼承自NSObject也說明Block的本質是OC物件。
Block有三種型別,分別是NSGlobalBlock,MallocBlock,NSStackBlock。
這三種型別的Block物件的儲存區域如下:
類 | 物件的儲存域 |
---|---|
NSStackBlock | 棧 |
NSGlobalBlock | 程式的資料區域(.data區) |
NSMallocBlock | 堆 |
截獲了自動變數的Block是NSStackBlock型別,沒有截獲自動變數的Block則是NSGlobalStack型別,NSStackBlock型別的Block進行copy操作之後其型別變成了NSMallocBlock型別。
Block的型別 | 副本的配置儲存域 | 複製效果 |
---|---|---|
NSStackBlock | 棧 | 從棧複製到堆 |
NSGlobalStack | 程式的資料區域 | 什麼也不做 |
NSMallocBlock | 堆 | 取用計數增加 |
下麵我們一起分析一下NSStackBlock型別的Block進行copy操作後Block物件從棧複製到了堆有什麼道理,我們首先來看一段程式碼:
void (^block)(void);
void test() {
int age = 10;
block = ^{
NSLog(@"age=%d", age);
};
}
int main(int argc, char * argv[]) {
@autoreleasepool {
test();
block();
return 0;
}
}
不出意外的話,列印結果應該是10,那麼結果是不是這樣呢?我們列印看一下:
age=-411258824
很奇怪,列印了一個這麼奇怪的數字。這是為什麼呢?
block使用了自動變數age,所以它是NSStackBlock型別的,因此block是存放在棧區,age是被捕獲作為結構體的成員變數,其值也是被儲存在棧區。所以當test這個函式呼叫完畢後,它棧上的東西就有可能被銷毀了,一旦銷毀了,age值就不確定是多少了。透過列印結果也可以看到,確實是影響到了block的執行。
如果我們對block執行copy操作,結果會不會不一樣呢?
void (^block)(void);
void test() {
int age = 10;
block = [^{
NSLog(@"age=%d", age);
} copy];
}
int main(int argc, char * argv[]) {
@autoreleasepool {
test();
block();
return 0;
}
}
列印結果:
age=10
這個時候得出了正確的輸出。
因為對block進行copy操作後,block從棧區被覆制到了堆區,它的成員變數age也隨之被覆制到了堆區,這樣test函式執行完之後,它的棧區被銷毀並不影響block,因此能得出正確的輸出。
7、ARC環境下自動為Block進行copy操作的情況
6中講的最後一個例子:
void (^block)(void);
void test() {
int age = 10;
block = ^{
NSLog(@"age=%d", age);
};
}
int main(int argc, char * argv[]) {
@autoreleasepool {
test();
block();
return 0;
}
}
這種使用方式其實非常常見,我們在使用的時候也沒有發現有什麼問題,那為什麼在MRC環境下就有問題呢?因為在ARC環境下編譯器為我們做了很多copy操作。其中有一個規則就是如果Block被強指標指著,那麼編譯器就會對其進行copy操作。我們看到這裡:
^{
NSLog(@"age=%d", age);
};
這個Block塊是被強指標指著,所以它會進行copy操作,由於其使用了自動變數,所以是棧區的Block。經過複製以後就到了堆區,這樣由於Block在堆區,所以就不受Block執行完成的影響,隨時可以獲取age的正確值。
總結一下ARC環境下自動進行copy操作的情況一共有以下幾種:
-
block作為函式傳回值時。
-
將block賦值給__strong指標時。
-
block作為Cocoa API中方法名含有usingBlock的方法引數時。
-
GCD中的API。
block作為函式傳回值時
typedef void(^PDBlock)(void);
PDBlock test() {
int age = 10;
return ^{
NSLog(@"age=%d", age);
};
}
int main(int argc, char * argv[]) {
@autoreleasepool {
PDBlock block = test();
block();
return 0;
}
}
test函式的傳回值是一個block,那這種情況的時候,在棧區的
^{
NSLog(@"age=%d", age);
};
這個block會被覆制到堆區
將block賦值給強指標時
7中第一個例子就是將block賦值給強指標時,進行了copy操作的情況。
block作為Cocoa API中方法名含有usingBlock的方法引數時
比如說遍歷陣列的函式:
NSArray *array = [[NSArray alloc] init];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"%d", idx);
}];
enumerateObjectsUsingBlock:
這個函式中的block會進行copy操作
GCD中的API
GCD中的很多API的引數都有block,這個時候都會對block進行一次copy操作,比如下麵這個dispatch_after函式:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"wait");
});