本文作者主要参考Kazuki Sakamoto,Tomohiko Furumoto著,黎华译的《Objective-C高级编程:iOS与OS X多线程和内存管理》一书。在平时工作中遇到不少关于Block的相关困惑,比如:Block的循环引用问题。于是在反复阅读了该书籍后,决定总结一篇关于Block的相关文章。如果还想深入了解Block,作者建议直接阅读上述书籍。由于作者水平有限,文章难免存在纰漏,恳求各位读者不吝赐教。
Block变量捕获只针对Block中使用的变量,没有使用的不会追加到__xxx(外部函数名)_block_impl_x(序号,从0开始)
C语言结构体中,也就是说不会被Block所捕获。
一、值变量类型的捕获(如:int)
1、全局变量
- 不会捕获,因为在Block变换的C语言函数中可以直接访问到,并且没有任何变化,可直接使用。
2、全局静态变量
- 不会捕获,因为在Block变换的C语言函数中可以直接访问到,并且没有任何变化,可直接使用。
3、局部静态变量
- 捕获,因为变量在Block变换的C语言函数作用域外部,会生成指针指向该变量,并保存在Block转换的C语言结构体中。
4、普通局部变量
- 不会捕获
局部静态变量的捕获机制似乎也适用于局部变量的捕获,但是官方为什么没有这么做?
这里说一下自己的理解,1)因为普通的值类型的局部变量的初衷只是在局部的作用域使用,超出作用域就不想再被其他地方所访问,因此也没有必要对其进行捕获。2)进行捕获的目的是,在Block内部进行对其操作时,外部的变量也能得到相同修改。对于对象类型的普通局部变量而言,例如一个Array对象,在对其添加一个Object后,显然外部Array也是会得到新增的,因为本质上它们指向同一片内存区域。而对于值类型的普通局部变量,如int类型,我们在Block内想修改它,唯一的手段就是修改它的值,但是这是不被允许的,需要加上
__block
修饰符,但加上__block
修饰符之后,它就变成了__block
结构体了,不再是普通的值类型了。
二、指针变量类型(对象)的捕获(如:NSString)
在Block结构体内部会生成相应的 指针变量(对象),如引用外部的array对象,在Block内部则生成id __strong array
的成员变量,这个成员变量的内存管理通过调用Block内部的__xxx_block_copy_x
函数来持有外部的对象,通过__xxx_block_dispose_x
来释放对象。
所以,Block中使用附有__strong
修饰符(默认生成的对象属于strong类型)的局部变量的对象和复制到堆上的__block
变量由于被Block所持有,因而会超出其变量作用域而存在。
注意:如果外界的对象被
__weak
修饰符修饰,那在Block结构体内部会生成相应的weak类型的对象,当外界的对象被释放时,由于weak指针自动置nil的原因,Block结构体内部的对象被置nil,虽然对其可以进行任何的方法调用,不会导致程序崩溃,但都是没有结果的。这也经常用来解决Block的循环引用问题。
三、__block对值变量的使用(如:__block int val = 0)
生成__block
结构体,__block
结构体内部生成一个值类型成员变量,其值等于外界值变量的值。此时在Block内部进行修改时,会以如下形式进行:
__Block_byref_val_0 *val = __cself->val;
(val->__forwarding->val) = 1;
通过__block
结构体的__forwarding
指针访问自己(或者是堆上自己的拷贝)来改变__block
结构体内部的值。
__block
成员变量的内存管理通过调用Block内部的__xxx_block_copy_x
函数来持有对象,通过__xxx_block_dispose_x
来释放对象。
四、__block对指针变量(对象)的使用(如:__block NSString *str)
同样会生成__block
结构体,__block
结构体内部生成一个对象类型成员变量,其引用外界的对象。
__block
成员变量的内存管理通过调用Block内部的__Block_byref_id_object_copy_x
函数来持有对象,通过__Block_byref_id_object_dispose_x
来释放对象。
注意:如果外界的对象被
__weak
修饰符和__block
修饰,那在__block
结构体内部会生成相应的__weak
类型的对象,当外界的对象被释放时,由于weak指针自动置nil的原因,__block
结构体内部的对象被置nil,虽然对其可以进行任何的方法调用,不会导致程序崩溃,但都是没有结果的。
五、__block变量的__forwarding指针
__Block_byref_val_0 *val = __cself->val;
(val->__forwarding->val) = 1;
在Block内部,来获取持有的__block
变量,并通过上述方式修改变量的值,但是__forwarding
指针的存在是不是看似多余呢?
__block
变量内部的__forwarding
指针可以实现无论__block
变量配置在栈上还是堆上时都能正确地访问__block
变量。原因如下:
__block变量的配置存储域 | Block从栈复制到堆时的影响 |
---|---|
栈 | 从栈复制到堆并被Block持有 |
堆 | 被Block持有 |
这里分析一种情况
__block int val = 0;
void (^blk)(void) = [ ^{
++val; } copy ];
++val;
blk();
NSLog(@"%d",val);
打印结果:2
开始,__block
变量和Block都在栈上面,当Block调用copy
方法后,Block被复制到堆中,而其内部调用的__block
变量也会从栈复制到堆并被Block持有,此时__block
变量有栈上的变量和堆上的变量,栈上的__block
变量会将成员变量__forwarding
指针的值替换为复制到目标堆上的__block
变量用结构体实例的地址。
这里曾经存在误区,又学习了一遍。此时可同时访问栈上/堆上的__block
变量,但最终访问的都是同一处__block
变量,如果没有发生__block
变量从栈到堆的拷贝,那么在Block内部/外部访问__block
变量都是属于栈上的同一个__block
变量,反之则是访问堆上的同一个__block
变量
具体原因如下:
__block变量的配置存储域 | 访问过程(__block变量拷贝后) |
---|---|
栈 | val(栈) -> __forwarding 指针(指向堆上的val) -> num |
堆 | val(堆) -> __forwarding 指针(指向堆上的自身) -> num |
这里附上书中的一段原话:通过该功能,无论是在Block语法中、Block语法外使用
__block
变量,还是__block
变量配置在栈上或堆上,都可以顺利地访问同一个__block
变量。
六、block从栈复制到堆的时机
在ARC有效时,大多数情况下编译器会恰当的判断,自动生成将Block从栈复制到堆上的代码,有如下情况:
1、调用Block的copy
实例方法时
2、Block作为函数返回值时或函数的参数中传递Block时
3、将Block赋值给附有__strong
修饰符id类型的类或Block类型的成员变量时
4、在方法名中含有usingBlock
的Cocoa框架方法或Grand Central Dispatch
的API中传递Block时
不过有时候需要手动将Block从栈复制到堆上,此时我们使用“copy
实例方法”。