详解Objective C 中Block如何捕获外部值
作者:小橘爷
引言
Block
本质上也是一个 Objective-C
对象,它内部也有个 isa
指针。Block
是封装了函数调用以及函数调用环境的 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; }
静态变量、静态全局变量与全局变量
对于静态变量、静态全局变量与全局变量的捕获,会稍有不同,其中:
- 全局变量与静态全局变量:直接使用,因为地址一直是可以直接获取的。
- 静态变量:捕获地址使用,因为
block
有可能会传递出创建时的作用域。
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
部分的话可以发现,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 拷贝到堆上时,调用方式是统一的。 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
不能修饰全局变量、静态变量(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
方法之后不会发生变化。 - NSStackBlock:访问了
auto
变量,调用copy
方法之后存储位置从栈变为堆。 - NSMallocBlock:
__NSStackBlock__
调用了copy
方法之后,引用计数增加。
在 ARC
环境下,编译器会根据情况自动将栈上的 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捕获外部值的资料请关注脚本之家其它相关文章!