Block机制详解
我们知道在Block
使用中,Block
内部能够读取外部局部变 量的值。但我们需要改变这个变量的值时,我们需要给它附加上__block
修饰符。
__block
另外一个比较多的使用场景是,为了避免某些情况下Block循环引用的问题,我们也可以给相应对象加上__block
修饰符。
为什么不使用__block
就不能在Block
内部修改外部的局部变量?
我们把以下代码通过 clang -rewrite-objc 源代码文件名重写:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int val = 10;
void (^block)(void) = ^{
NSLog(@"%d", val);
};
block();
}
return 0;
}
可得到如下代码:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
NSInteger val;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 1;
}
static void __main_block_copy_0 (struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0 (struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->val, 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[]) {
{
__AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 0};
void (*block)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344);
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
NSLog((NSString *)&__NSConstantStringImpl__val_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_d7fc4b_mi_0, (val.__forwarding->val));
}
return 0;
}
我们发现由__block
修饰的变量变成了一个__Block_byref_val_0
结构体类型的实例。该结构体的声明如下:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
NSInteger val;
};
注意到这个结构体中包含了该实例本身的引用__forwarding
。
我们从上述被转化的代码中可以看出 Block 本身也一样被转换成了__main_block_impl_0
结构体实例,该实例持有__Block_byref_val_0
结构体实例的指针。
我们再看一下赋值和执行部分代码被转化后的结果:
static void __main_block_func_0 (struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 1;
}
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
我们从__cself
找到__Block_byref_val_0
结构体实例,然后通过该实例的__forwarding
访问成员变量val
。成员变量val
是该实例自身持有的变量,指向的是原来的局部变量。
上面部分我们展示了__block
变量在Block查看和修改的过程,那么问题来了:
当block作为回调执行时,局部变量val
已经出栈了,这个时候代码为什么还能正常工作呢?
我们为什么是通过成员变量__forwarding
而不是直接去访问结构体中我们需要修改的变量呢? __forwarding
被设计出来的原因又是什么呢?
存储域
通过上面的描述我们知道Block和__block
变量实质就是一个相应结构体的实例。
我们在上述转换过的代码中可以发现__main_block_impl_0
结构体构造函数中, isa
指向的是_NSConcreteStackBlock
。
Block还有另外两个与之相似的类:
_NSConcreteStackBlock
保存在栈中的block,出栈时会被销毁
_NSConcreteGlobalBlock
全局的静态block,不会访问任何外部变量
_NSConcreteMallocBlock
保存在堆中的block,当引用计数为0时会被销毁
上述示例代码中,Block是被设为_NSConcreteStackBlock