前言
在Objective-C中,block是一個很常見的東西,說白了就是個匿名函式,網上有很多關於block如何使用的文章,講的都非常精彩,這裡主要探討下block的實現原理。關於如何使用block,請參考網上的教程。
例項
先來新建一個控制檯工程,main.m裡的程式碼如下,並思考下最後的輸出結果是什麼:
void blockFunc1() {
int num = 100;
void (^block)(void) = ^() {
NSLog(@"num = %d
", num);
};
num = 200;
block();
}
void blockFunc2() {
__block int num = 100;
void (^block)(void) = ^() {
NSLog(@"num = %d
", num);
};
num = 200;
block();
}
int num = 100;
void blockFunc3() {
void (^block)(void) = ^() {
NSLog(@"num = %d
", num);
};
num = 200;
block();
}
void blockFunc4()
{
static int num = 100;
void (^block)(void) = ^{
NSLog(@"num = %d", num);
};
num = 200;
block();
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
^{ printf("Hello, World!
"); } ();
blockFunc1();
blockFunc2();
blockFunc3();
blockFunc4();
}
return 0;
}
列印結果是:
Hello, World!
2018-03-21 15:50:01.996591+0800 BlockDemo[34825:4848536] num = 100
2018-03-21 15:50:01.997009+0800 BlockDemo[34825:4848536] num = 200
2018-03-21 15:50:01.997025+0800 BlockDemo[34825:4848536] num = 200
2018-03-21 15:50:01.997037+0800 BlockDemo[34825:4848536] num = 200
聰明的讀者應該早就知道這個結果,?,先簡單解釋一下:
1、^{ printf("Hello, World!
"); } ();
這個沒啥好說的,肯定就列印“Hello, World!”了。
2、blockFunc1
裡面,num
是以值傳遞的方式被block獲取,所以儘管後面更改了num
的值,但是在block裡面還是保持保持原來的值。
2、blockFunc2
裡面,num由__block
修飾,num
在block變成了外部的一個引用(後面會通過原始碼解釋),所以在block外部改變num
的值時,block裡面的num
也隨著改變。
3、blockFunc3
裡面,block引用的是一個全域性的num,所以,num改變的時候也會改變block內部num的值。
4、blockFunc3
裡面,block引用的是一個static的num,所以,num改變也會改變block內部的num的值。
原始碼分析
也許大家看到上面的解釋還是不知道為啥會這樣,所以接下,我通過原始碼來分析下其中的緣由,我們先把這段先轉換成c++檔案,cd到main.m所在的目錄,並執行這條命令clang -rewrite-objc main.m
,通過這條命令可以把main.m檔案轉換成cpp檔案,裡面可以看到block的結構。我們開啟這份檔案,這個檔案比較長,直接拉到最後。可以看到在檔案的最後是main函式的入口,程式碼如下:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)) ();
blockFunc1();
blockFunc2();
blockFunc3();
blockFunc4();
}
return 0;
}
先看第一行程式碼,構造了一個__main_block_impl_0物件,__main_block_impl_0是一個結構體。相關程式碼如下:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
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;
}
};
從程式碼可知,最後block會轉化成一個__block_impl物件,而block執行的程式碼會轉化成一個靜態函式,__block_impl裡面的FuncPtr會指向這個靜態函式。在這裡printf("Hello, World!
這個block轉換後的靜態函式如下:
");
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Hello, World!
");
}
所以整個過程是這樣的:
1、先構造一個__main_block_impl_0
物件,構造的時候把__main_block_func_0
傳進去,當然還有別的引數,這裡先不考慮。
2、在__main_block_impl_0
的構造方法中,再把__main_block_func_0
賦給__block_impl
的FuncPtr。
3、呼叫FuncPtr。
所以,從上面可以看出,block實際上是轉化為了一個__block_impl
物件,這個物件有isa指標,用來表示block的型別,上面的block的isa指向&_NSConcreteStackBlock。同時block物件還有一個FuncPtr指標,用來指向block執行的方法(轉換後的靜態函式)。
再來看看blockFunc1相關的內容
struct __blockFunc1_block_impl_0 {
struct __block_impl impl;
struct __blockFunc1_block_desc_0* Desc;
int num;
__blockFunc1_block_impl_0(void *fp, struct __blockFunc1_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __blockFunc1_block_func_0(struct __blockFunc1_block_impl_0 *__cself) {
int num = __cself->num; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_rv_338km0ws0gb2gk132_zrs0wc0000gp_T_main_b5d67f_mi_0, num);
}
void blockFunc1() {
int num = 100;
void (*block)(void) = ((void (*)())&__blockFunc1_block_impl_0((void *)__blockFunc1_block_func_0, &__blockFunc1_block_desc_0_DATA, num));
num = 200;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
在這個函式裡面,構造block的時候把num傳了進去,而且是普通值傳遞,這樣的話其實是拷貝了一份num。然後在執行block方法的時候,使用的是拷貝的那份num,從int num = __cself->num; // bound by copy
可以看出。這個block也是_NSConcreteStackBlock型別的。
再來看看__block修飾過的num在block裡面是怎麼傳遞的,我們看看blockFunc2相關的程式碼:
// 封裝num的結構
struct __Block_byref_num_0 {
void *__isa;
__Block_byref_num_0 *__forwarding;
int __flags;
int __size;
int num;
};
struct __blockFunc2_block_impl_0 {
struct __block_impl impl;
struct __blockFunc2_block_desc_0* Desc;
__Block_byref_num_0 *num; // by ref
__blockFunc2_block_impl_0(void *fp, struct __blockFunc2_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __blockFunc2_block_func_0(struct __blockFunc2_block_impl_0 *__cself) {
__Block_byref_num_0 *num = __cself->num; // bound by ref
NSLog((NSString *)&__NSConstantStringImpl__var_folders_rv_338km0ws0gb2gk132_zrs0wc0000gp_T_main_b5d67f_mi_1, (num->__forwarding->num));
}
void blockFunc2() {
__attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 100};
void (*block)(void) = ((void (*)())&__blockFunc2_block_impl_0((void *)__blockFunc2_block_func_0, &__blockFunc2_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344));
(num.__forwarding->num) = 200;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
從程式碼中可以看到,__block修飾的num在內部被包裝成一個__Block_byref_num_0的物件,假設叫a,原來num的值100儲存在物件a的num欄位中,同時這個物件a有一個__forwarding欄位,指向a本身。當改變num的值的時候(原始碼是num = 200;
),這段程式碼變為(num.__forwarding->num) = 200;
,也就是說把物件a裡面的num欄位的值變為了200。同時,在block的執行函式__blockFunc2_block_func_0中,列印出來的取值是從__Block_byref_num_0 *num = __cself->num;
取出,也就是取得是改變後的值,所以列印結果是200。這就是為什麼用__block修飾的變數可以在block內部被修改。
那當num為全域性變數的時候,block又是怎樣的呢?請看程式碼:
static void __blockFunc3_block_func_0(struct __blockFunc3_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_rv_338km0ws0gb2gk132_zrs0wc0000gp_T_main_b5d67f_mi_2, num);
}
void blockFunc3() {
void (*block)(void) = ((void (*)())&__blockFunc3_block_impl_0((void *)__blockFunc3_block_func_0, &__blockFunc3_block_desc_0_DATA));
num = 200;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
從程式碼裡可以看出,這種情況很簡單,block物件根本沒有num欄位,也就是列印的時候直接取得全域性的num。
最後一種情況也很簡單,當num時static的時候,構造block物件的時候直接用引用傳值的方式把num放到block物件中。所以,當外部改變num的值的時候,也能反映到block內部。程式碼如下:
void blockFunc4()
{
static int num = 100;
void (*block)(void) = ((void (*)())&__blockFunc4_block_impl_0((void *)__blockFunc4_block_func_0, &__blockFunc4_block_desc_0_DATA, &num));
num = 200;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
總結
這篇文章主要從原始碼的角度講述了block的實現機制,並針對四種情況分析了block是如何引用外部變數的,分別是:
1、當引用區域性變數的時候,如果沒有__block修飾,那麼在block內部獲取的是外部變數的一份拷貝,改變外部變數不影響block內部的那份拷貝。
2、當引用區域性變數的時候,同時區域性變數用__block修飾,那麼在block內部使用的實際上是外部變數的一個引用,所以改變外部變數會影響block內部變數的值。
3、當引用全域性變數的時候,block並不持有這個變數。
4、當引用static變數的時候,block會以引用的方式持有這個變數。當在外部修改這個變數的時候,會影響block內部持有的這個變數的值。