一、Block的本质
通过将OC代码编译, 我们可以看到Block编译后其实是一个OC对象(有isa指针), 我们就通过编译后的代码来分析一下Block的本质
- 编译后的block整体是一个 __main_block_impl_0 结构体, 他包含了2个成员, impl 和 Desc.
- 在impl中我们可以看到 isa指针, Flags, Reserved和FuncPtr. isa指针表明了block实际也是一个OC对象, 而FuncPtr指向叫做 __main_block_func_0() 函数的一个指针.
- 而desc中目前只有 reserved 和 Block_size 2个成员
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;
}
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
}
所以我们可以说, Block的本质是: 封装了函数调用和函数调用环境的OC对象
二、Block中修改局部变量的值为何无效? (Block的变量捕获)
block捕获值类型
int height = 10;
void(^blcok)(void) = ^{
NSLog(@"height: %d", height); // height: 10
};
height = 20;
blcok();
我们知道, 上面的代码打印结果height = 10, 那么为什么会这样呢? 我们将修改后的block代码再次编译, 查找原因
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int height;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _height, int flags=0) : height(_height) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int height = __cself->height; // bound by copy
NSLog(height);
}
- 首先我们可以看到block中多了一个
int height
, 并且在构造时从外界传入 - 在FuncPtr的函数中, 我们可以看到, 打印的height是block自己的height
block中多的int height
我们就叫做block的变量捕获. 当block代码块中使用到外部变量时, 会将变量捕获到block内部存储起来(值传递). 所以, 当外部的height修改为20时, block打印的依然是是自己的height, 所以输出10.
block捕获指针类型
我们将int类型换成对象, 再次实验
Person *p = [[Person alloc] init];
p.age = 10;
void(^blcok)(void) = ^{
NSLog(@"age: %d, age: %d", p.age); // age: 20
};
p.age = 20;
blcok();
可以看到, 这次输出的是20, 我们根据变量捕获可以猜测, block这次是将对象p的指针捕获到了内部, 通过指针调用时, 输出的是修改之后的age值. 编译代码看我们是否猜测的正确.
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *p;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *_p, int flags=0) : p(_p) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
Person *p = __cself->p; // bound by copy
NSLog(objc_msgSend((id)p, sel_registerName("age"));
}
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*);
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
可以看到, 跟我们猜测的一样, block将p对象的指针捕获到了内部, 通过指针进行调用. 特别的: 跟OC对象一样, 使用的是objc_msgSend() 来调用方法. 另外, 我们可以看到desc中,多了 copy和dispose2个函数. 这2个函数其实是对捕获的p指针进行内存管理
block内部使用外部变量时, 会根据使用变量的类型, 捕获到自己内部. 然后使用时, 使用的是自己捕获的变量/指针. 同时, 在捕获指针类型变量时, 同样会添加内存管理相关的代码
三、Block的修饰词为什么要用copy?
在正常情况下, block是存放在栈上的, 如果block内部引用了auto变量, 那么block是不会对auto变量产生强引用的, 那么在auto变量离开作用域后就会自动释放, 那么block在调用是, 就会发生错误.
当对block使用了copy时, 会将block从栈copy到堆中, 此时, block内部会调用 _Block_object_assign()
根据auto的强弱修饰符对auto进行相应的引用. 这样, block就可以保证在使用时, auto变量没有被释放. 当block释放时, 会调用 _Block_object_dispose()
对自己捕获的变量进行内存管理.
在第二步中, 我们测试的捕获p变量, 编译器增加的代码就是 _Block_object_assign/_Block_object_dispose 相关的操作. 这是因为在ARC环境下, 编译器在某些时机会自动将我们的block copy到堆上
在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况
- block作为函数返回值时
- 将block赋值给 __strong 指针时 (第二步的情况)
- block作为Cocoa API中方法名含有usingBlock的参数时 (如数组的枚举block)
- block作为GCD API方法参数时
四、Block中如何修改auto变量的值? (__block修饰符)
我们都知道, 在block中是无法修改auto变量的值, 编译器会直接报错. 通过我们上边代码的分析, 我们很容易明白. 因为在block体中使用的auto变量时block自己捕获的, 与block外声明的变量无关(block体内的函数与外部的变量也不在一个作用域), 肯定是无法修改的. 同时, 我们只要给auto变量加上 __block修饰符
, 就可以修改auto变量, 这是为什么呢? 我们编译代码, 分析一下
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_b_height_0 *b_height; // by ref
__Block_byref_b_p_1 *b_p; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_b_height_0 *_b_height, int flags=0) : b_height(_b_height->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __Block_byref_b_height_0 {
void *__isa;
__Block_byref_b_height_0 *__forwarding;
int __flags;
int __size;
int b_height;
};
struct __Block_byref_b_p_1 {
void *__isa;
__Block_byref_b_p_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
Person *b_p;
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_b_height_0 *b_height = __cself->b_height; // bound by ref
__Block_byref_b_p_1 *b_p = __cself->b_p; // bound by ref
(b_height->__forwarding->b_height) = 20;
NSLog(b_height->__forwarding->b_height);
NSLog((void *)objc_msgSend)((id)(b_p->__forwarding->b_p), sel_registerName("age")));
}
我们可以看到, 如果使用__block来修饰时, block捕获的变量由 int 变成了 __Block_byref_b_height_0
类型. __Block_byref_b_height_0
中包含了isa指针, 指向自己的__forwarding, __flags, __size以及真正存储变量的b_height.
当我们在修改b_height时, 是通过指针拿到__Block_byref_b_height_0
结构体, 然后修改内部的b_height. 同样的, 对于对象p指针, 也是捕获一个__Block_byref_b_p_1
结构体, 通过结构体内部的b_p指针来访问.
使用__block修饰auto变量以后, block会将变量进行封装, 封装成一个对象以后, 在使用指针进行对象的访问, 来达到在block内部修改auto变量的效果
五、Block中的循环引用? (__weak)
在使用block时, 如果不小心就会发生循环引用, 导致对象不能释放. 循环引用是怎么产生的呢? 一个对象只有block对象, block对象中又因为变量捕获机制持有了当前对象, 这就产生了循环引用, 导致block和当前对象都无法释放.
我们可以在block中使用当前对象时, 改为使用__weak修饰过的对象, 来打破这个循环. 为什么使用__weak就可以打破循环呢?
通过第三章节的讲解, 我们知道block在捕获变量时, 会同时生成 __main_block_copy_0()/__main_block_dispose_0()
函数. 调用_Block_object_assign()
函数时, 如果当前捕获的变量是使用__weak修饰的, 那么在函数内部就不会对当前对象进行强引用, 这样block内部就不会持有当前对象, 这个循环就从中间截断了.