詳解Objective C 中Block如何捕獲外部值
引言
Block 本質上也是一個 Objective-C 對象,它內部也有個 isa指針。Block 是封裝了函數調用以及函數調用環(huán)境的 Objective-C 對象。Block 的底層結構如下圖所示:

Block 對于不同類型的值會有不同的捕獲方式,本文將通過代碼展示其對于各種場景下的外部值是如何進行捕獲的。
自動變量
首先展示源代碼:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSInteger value = 0;
void(^block)(void) = ^{
NSLog(@"%zd", value);
};
block();
}
return 0;
}
經過 clang -rewrite-objc 之后,得到的代碼如下,可以看到,對于自動變量的捕獲,是會在 Block 結構體中生成一個對應類型的成員變量來實現捕獲的能力,這也解釋了為什么在 Block 中修改捕獲的值的內容,無法對 Block 外的值產生影響。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSInteger value; // 捕獲的 NSInteger value
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger _value, int flags=0) : value(_value) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSInteger value = __cself->value; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_zz_zyxvpxvq6csfxvn_n0000000000000_T_main_e3ca95_mi_0, value);
}
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)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSInteger value = 0;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, value));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
靜態(tài)變量、靜態(tài)全局變量與全局變量
對于靜態(tài)變量、靜態(tài)全局變量與全局變量的捕獲,會稍有不同,其中:
- 全局變量與靜態(tài)全局變量:直接使用,因為地址一直是可以直接獲取的。
- 靜態(tài)變量:捕獲地址使用,因為
block有可能會傳遞出創(chuàng)建時的作用域。
NSInteger globalValue = 1;
static NSInteger staticGlobalValue = 2;
int main(int argc, const char * argv[]) {
@autoreleasepool {
static NSInteger staticValue = 3;
void(^block)(void) = ^{
globalValue += 1;
staticGlobalValue += 2;
staticValue += 3;
};
block();
}
return 0;
}
NSInteger globalValue = 1;
static NSInteger staticGlobalValue = 2;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSInteger *staticValue;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger *_staticValue, int flags=0) : staticValue(_staticValue) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSInteger *staticValue = __cself->staticValue; // bound by copy
globalValue += 1;
staticGlobalValue += 2;
(*staticValue) += 3;
}
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)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
static NSInteger staticValue = 3;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &staticValue));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
帶 __block 的自動變量
被 __block 修飾的自動變量,可以在 Block 內部對其外部的值進行修改:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block NSInteger value = 0;
void(^block)(void) = ^{
value = 10;
};
block();
NSLog(@"%zd", value);
}
return 0;
}
這次生成的代碼復雜了一些,不過只關注 value 部分的話可以發(fā)現,Block 為了捕獲 __block 類型的自動變量,會生成 __Block_byref_value_0 結構體,并通過該結構體來實現對外部 __block 自動變量的捕獲。
struct __Block_byref_value_0 { // 為捕獲 __block 的自動變量,生成的結構體。為的是方便多個 Block 同時捕獲一個自動變量時使用。
void *__isa; // isa 指針
__Block_byref_value_0 *__forwarding; // 在 Block 單純在棧上是,指向的是自己,拷貝到堆上后,指向的是在堆上的 Block。之所以需要這樣的指針是因為當 Block 拷貝到堆上時,調用方式是統(tǒng)一的。
int __flags;
int __size;
NSInteger value; // 具體的值
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_value_0 *value; // 通過引用的方式捕獲 value,其中變量類型為 __Block_byref_value_0
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_value_0 *_value, int flags=0) : value(_value->__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_value_0 *value = __cself->value; // bound by ref
(value->__forwarding->value) = 10; // 賦值代碼
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->value, (void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);}
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};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_value_0 value = {(void*)0,(__Block_byref_value_0 *)&value, 0, sizeof(__Block_byref_value_0), 0};
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_value_0 *)&value, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_zz_zyxvpxvq6csfxvn_n0000000000000_T_main_6bf1c6_mi_0, (value.__forwarding->value));
}
return 0;
}
__block 可以用于解決 block 內部無法修改 auto 變量值的問題,__block 不能修飾全局變量、靜態(tài)變量(static),編譯器會將 __block 變量包裝成一個對象。
當 block 在棧上時,并不會對 __block 變量產生強引用。
當 block 被 copy 到堆時,會調用 block 內部的 copy 函數,copy 函數內部會調用 _Block_object_assign 函數,_Block_object_assign 函數會對 __block 變量形成強引用(retain)。
當 block 從堆中移除時,會調用 block 內部的 dispose 函數,dispose 函數內部會調用 _Block_object_dispose 函數,_Block_object_dispose 函數會自動釋放引用的 __block 變量(release)。
捕獲對象
在探究完對標量類型的捕獲之后,讓我們看一下對對象類型的捕獲:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSArray *array = [NSArray array];
void(^block)(void) = ^{
NSLog(@"%@", array);
};
block();
}
return 0;
}
通過轉譯的代碼可以看出,因為對象類型本身已經是存儲在堆上的值了,所以直接獲取其地址即可,同時其新增了兩個函數 __main_block_copy_0 和 __main_block_dispose_0,這兩個函數是用來將對象拷貝到堆上和被從堆上移除時調用的,其內部又分別調用了 _Block_object_assign 和 _Block_object_dispose 用來對捕獲的對象進行引用計數的增加和減少。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSArray *array;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSArray *_array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSArray *array = __cself->array; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_zz_zyxvpxvq6csfxvn_n0000000000000_T_main_8ba4f7_mi_0, array);
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
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};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSArray *array = ((NSArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("array"));
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
Block 對象本身分為三種類型:
- NSGlobalBlock:沒有訪問
auto變量,調用copy方法之后不會發(fā)生變化。 - NSStackBlock:訪問了
auto變量,調用copy方法之后存儲位置從棧變?yōu)槎选?/li> - NSMallocBlock:
__NSStackBlock__調用了copy方法之后,引用計數增加。
在 ARC 環(huán)境下,編譯器會根據情況自動將棧上的 block 復制到堆上,比如以下情況:
Block作為函數返回值時- 將
Block賦值給__strong指針時 Block作為Cocoa API中方法名含有usingBlock的方法參數時Block作為GCD API的方法參數時
所以,當 Block 內部訪問了對象類型的 auto 變量時。如果 Block 是在棧上,將不會對 auto 變量產生強引用。
如果 Block 被拷貝到堆上,會調用 Block 內部的 copy 函數,copy 函數內部會調用 _Block_object_assign 函數,_Block_object_assign 函數會根據 auto 變量的修飾符(__strong、__weak、__unsafe_unretained)做出相應的操作,形成強引用或者弱引用。
如果 Block 從堆上移除,會調用 Block 內部的 dispose 函數,dispose 函數內部會調用 _Block_object_dispose 函數。_Block_object_dispose 函數會自動釋放引用的 auto 變量(release)。
__block 對象類型的捕獲
如果想在 Block 中,對捕獲的對象的指針指向進行修改,則需要添加 __block 關鍵字:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block NSArray *array = [NSArray array];
void(^block)(void) = ^{
array = [NSArray array];
NSLog(@"%@", array);
};
block();
}
return 0;
}
通過轉譯我們可以看出,跟 __block 修飾的標量類型相似,同樣會生成 __Block_byref_array_0 結構體來捕獲對象類型。同時其內部生成了 __Block_byref_id_object_copy 和 __Block_byref_id_object_dispose 兩個函數指針,用于對被結構體包裝的對象進行內存管理。
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);
}
struct __Block_byref_array_0 {
void *__isa;
__Block_byref_array_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSArray *array;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_array_0 *array; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_array_0 *_array, int flags=0) : array(_array->__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_array_0 *array = __cself->array; // bound by ref
(array->__forwarding->array) = ((NSArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("array"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_zz_zyxvpxvq6csfxvn_n0000000000000_T_main_3593f0_mi_0, (array->__forwarding->array));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 8/*BLOCK_FIELD_IS_BYREF*/);}
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};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_array_0 array = {(void*)0,(__Block_byref_array_0 *)&array, 33554432, sizeof(__Block_byref_array_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("array"))};
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_array_0 *)&array, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
當 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*/);
當 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*/);
以上就是詳解Objective C 中Block如何捕獲外部值的詳細內容,更多關于Objective C Block捕獲外部值的資料請關注腳本之家其它相關文章!
相關文章
iOS使用UIBezierPath實現ProgressView
這篇文章主要為大家詳細介紹了iOS使用UIBezierPath實現ProgressView,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-04-04

