Block底层实现
运用clang -rewrite-objc
命令可以把 Objective-C 代码转换为 C/C++ 代码,从而展示 Block 的底层实现。以下是一个最普通的 Block 代码示例:
1 | int main(int argc, const char *argv[]) { |
经过clang -rewrite-objc
的转换,可以得到 Block 结构体的声明:
1 | struct __block_impl { |
具体到名为 blk 的 Block, 它的结构体实现如下:
1 | struct __main_block_impl_0 { |
其中在构造函数中初始化__main_block_impl_0
的成员变量。注意到 impl 结构体的 isa 变量指向_NSConcreteStackBlock
,此__main_block_impl_0
结构体相当于基于objc_object
结构体的类对象结构体,说明该 Block 是一个位于栈上的对象。Block 的实质就是对象。
当 Block 是全局变量时,isa 会指向_NSConcreteGlobalBlock
,当 Block 被 copy 到堆(heap)时,isa 会指向 _NSConcreteMallocBlock
。
另外,Block 里面包含的匿名函数被转换为一个静态函数,实现如下:
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { |
结构体__main_block_desc_0
包含了该 Block 的大小:
1 | static struct __main_block_desc_0 { |
综合以上的实现,main 函数中的 Objc 代码被转换为:
1 | int main(int argc, const char * argv[]) { |
截获自动变量
待转换的 Objective-C 代码如下所示:
1 | int main(int argc, const char * argv[]) { |
经过转换之后,__main_block_impl_0
结构体对比上一节的实现有所变化:
1 | struct __main_block_impl_0 { |
Block 中使用的局部变量加入到了结构体的成员变量中,只捕捉了实际使用到的变量。
Block 匿名函数转换为如下所示的函数:
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { |
用__main_block_impl_0
中的成员变量给函数中的局部变量赋值,所以函数中使用的变量值始终等于声明 Block 时捕获到的该变量的值。因为 C 语言不支持a[] = b[]
,在 Block 中无法使用 C 语言数组。如果在 Block 中对局部变量进行赋值,因为 Block 不写回对局部变量的修改,会导致一个编译期错误。
最后 main 函数中的代码被转换为:1
2
3
4
5
6
7
8int main(int argc, const char * argv[]) {
int dmy = 250;
int val = 10;
const char *hhh = "val = %d\n";
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, hhh, val));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
说明 Block 构造时传入的参数是 Block 声明时还未改变值的局部变量。
截获静态变量
如果在 Block 中使用了静态变量,__main_block_impl_0
中会增加一个成员变量,用来保存指向该静态变量的指针。在 Block 匿名函数的实现中,使用这个指针变量操作值的改变和赋值。在声明带有静态变量的 Block 时,__main_block_impl_0
的构造函数通过传递指针实现对静态变量的捕获。如下:
1 | int main(int argc, const char * argv[]) { |
__block 变量
在变量捕获之后,对于自动变量,不能再改变其值。如果想改变变量的值,可以考虑使用静态变量或全局变量,实际通过操作指针来改变其值。如果不想使用静态变量,__block 变量可以成为另一个选择。
如果捕获了 __block 变量,代码转换后会生成一个新的 __block 变量结构体,如下所示:
1 | struct __Block_byref_block_val_0 { |
其中成员变量block_val
存储了 __block 变量的值,__forwarding
指针指向自身。在 Block 被 copy 到堆上后,它指向的是堆上的 __block 变量结构体实例。这样可以在 Block 之内和之外访问 __block 变量并修改。
在主体 Block 的实现结构体中,增加block_val
成员变量保存指向 __block 变量的指针:
1 | struct __main_block_impl_0 { |
最终在 main 函数中,__block 变量被转换为__Block_byref_block_val_0
结构体在栈上的一个实例。
1 | // __block int block_val = 250; |
与 __block 变量类似,Block 在捕获对象时,也会在主 Block 结构体中生成一个 id 类型的成员变量,保存捕获的对象。同时会生成__main_block_copy_0()
和__main_block_dispose_0()
两个静态函数。__main_block_copy_0()
在 Block 拷贝到堆上时调用,作用是把目标对象 dst 中的 block_val 赋给 Block 结构体中的成员变量 block_var,并使其持有该对象。数字3
表示捕获的变量是一个普通对象,数字8
表示捕获的变量是一个 __block 变量。__main_block_dispose_0
在 Block 在堆上被销毁时调用,作用是释放 Block 结构体中的成员变量 block_var 持有的对象。
1 | static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);} |
__main_block_desc_0
结构体增加了两个函数指针类型的成员变量,分别指向 copy 和 dispose 函数。
1 | static struct __main_block_desc_0 { |
消除循环引用
捕获
self
引起循环引用。
当 Block 从栈上复制到堆上时,捕获的strong
对象将被 Block 持有。如果该对象再持有 Block, 就会引起循环引用的问题,两者内存将无法正常释放。这种内存泄漏常见的出现场景就是某个类持有 Block, 而 Block 中又捕获了强引用的self
对象。
解决方式即把self
赋值给一个用__weak
修饰符声明的weakSelf
变量,并在 Block 中使用该weakSelf
变量。当 Block 存在时,self
对象始终存在,因此不需判断被赋值后的weakSelf
是否为空。1
2
3
4
5
6- (id)init {
self = [super init];
id __weak weakSelf = self;
Blk blk0 = ^{NSLog(@"self = %@", weakSelf);};
return self;
}如果需要在 Block 中多次使用
weakSelf
,最好在 Block 里先将weakSelf
赋给一个__strong
修饰的strongSelf
,防止weakSelf
提前 nil out。这个技巧被称为”strong weak dance”,在 AFNetworking 源码中有所体现:1
2
3
4
5
6
7
8
9
10__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
strongSelf.networkReachabilityStatus = status;
if (strongSelf.networkReachabilityStatusBlock) {
strongSelf.networkReachabilityStatusBlock(status);
}
};__block 变量引起循环引用
当某类Obj
的对象持有 Block, Block 中又持有 __block 变量,__block 变量持有Obj
对象,这样会造成循环引用。如图:
为了消除循环引用,在 Block 函数中通过将nil
赋值给 __block 变量,可以让 __block 变量释放持有的Obj
对象,即达成目标。1
2
3
4
5
6
7
8
9-(id)init {
self = [super init];
__block id tmp = self;
Blk blk0 = ^{
NSLog(@"self = %@", tmp);
tmp = nil;
};
return self;
}
使用 __block 变量的好处
- 可以通过控制 __block 变量来控制对象的生命周期。
- 在不支持 __weak 修饰符的更早的 OS 版本,用 __block 替代 __unsafe_unretained 修饰符以避免迷途指针(dangling pointer)的问题。