__block说明符
前面讲到Block会捕获外部变量,但是当你试图在Block里面修改捕获的外部变量时。就是出现编译错误,解决的一种办法是将外部变量使用__block
修饰符修饰。下面是添加__block
修饰的外部变量代码:
#include <stdio.h>
int main(int argc, const char * argv[]) {
__block int val = 10;
void (^blk)(void) = ^{ val = 1; };
return 0;
}
该代码可进行编译。变换后如下:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
}; // [1]
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val;
__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;
} // [2]
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, 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[])
{
__Block_byref_val_0 val = {
(void*)0,
(__Block_byref_val_0 *)&val,
0,
sizeof(__Block_byref_val_0),
10
};
blk = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344);
return 0;
}
我们可以发现加上__block
说明符,源码量就急剧的增加了(已做简化)。
- 我们可以发现含有
__block
修饰的变量变成了__Block_byref_val_0
结构体。[1] __Block_byref_val_0
结构体实例的成员变量__forwarding持有该实例自身的指针。- Block转化而来的的
__main_block_func_0
结构体实例持有指向__block
变量的__Block_byref_val_0
结构体实例的指针。__Block_byref_val_0
结构体实例的成员变量__forwarding
持有指向该实例自身的指针。通过成员变量__forwarding
访问成员变量val的地址,从而可以修改自动变量。 - 我们需要负责
Block_byref_i_0
结构体相关的内存管理,所以main_block_desc_0
中增加了copy
和dispose
函数指针,对于在调用前后修改相应变量的引用计数。
Block存储域
__block
变量转换成了__block
变量的结构体类型的自动变量。所谓结构体类型的自动变量,即栈上生成的该结构体的实例。如下表所示:
名称 | 实质 | |
---|---|---|
Block | 栈上Block的结构体实例 | |
__block变量 | 栈上__block变量的结构体实例 |
前面我们看到Block的类型说明_NSConcreteStackBlock.虽然该类没有出现已变换源代码中,但还有与之相识的类,如:
- _NSConcreteStackBlock
- _NSConcreteGlobalBlock
- _NSConcreteMallocBlock
他们分别对应的存储区域如下所示:
类 | 设置对象的存储区域 |
---|---|
_NSConcreteStackBlock | 栈 |
_NSConcreteGlobalBlock | 程序的数据区域(.data区域) |
_NSConcreteMallocBlock | 堆 |
定义Block时期内存区域分配在栈中,其Block类型为__NSConcreteStackBlock
类对象.
那么_NSConcreteMallocBlock
类型的对象由何而来?这里肯定有人还在疑惑为什么__block
变量转化而来的结构体为生成指向自身的__forwarding
指针变量。其目的是为了能然超出范围的Block也有效。有人会说设置个全局的Block不就可以搞定了么。
行不行先来段代码看看:
void (^blk)();
if (/* some condition */) {
blk = ^{ NSLog(@"Block A"); };
}else {
blk = ^{ NSLog(@"Block B"); };
}
blk();
看这段代码我声明了全局的Block变量blk,然后在if语句中定义。如果你不理解block那么就很容易写出这样的代码,其实这段代码是很危险的。因为全局的blk变量是分配在栈上的。在if和else语句中定义的blk内容,编译器会给每个块分配好栈内存,然后等离开了相应的范围之后,编译器有可能把分配给块的内存覆写了。如果编译器未覆写这块栈内存则程序照常运行,如果这块内容被覆写那么程序就会崩溃。
解决上面问题的方法就是使用copy
方法,将block拷贝到堆中。拷贝完之后就是接下来要将的_NSConcreteMallocBlock
类型。该类型是带有引用计数的对象,如果在ARC下,只要引用计数不为0,可以随意的访问,后继的内存管理就交给编译器来完成了。
还有一种类型是_NSConcreteGlobalBlock
类型,这类Block不会捕捉任何状态的外部变量。块所使用的整个内存区域,在编译器已经完全的确定了,不需要每次调用时在栈中创建,如下就是一个全局快:
void (^blk)() = {
NSLog("This is a global block");
}
void main() {
}
__block变量存储域
如果Block配置在栈中,则在Block中使用的__block
变量也分配在栈中。当Block被复制到堆中时,__block
变量也一并被复制在堆中,并被Block所持有。如果非配在堆中的Block被废弃,那么它所使用的__block
变量也就被释放了。下面来看堆和栈上__block
混用的例子
__block int val = 0;
void (^blk) (void) = [^{val++;} copy];
++val;
blk();
NSLog(@"val:%d",val);
执行结果为:
val: 2
在Block中和在Block外修改__block
变量完全等效,它是怎么实现的呢?
是因为执行copy
方法之后,Block被复制到堆中,其内部捕获的__block
变量也一并被复制。而此时分配在栈上的val任然存在的,栈上的__block
变量val会将原本指向自身的__forwarding
指针指向复制到堆中的__block
变量val的地址。这样堆中的__block
变量被修改之后就等同于栈上的block被修改。
通过该功能,无论是在Block的语法中、Block语法外使用__block
变量,还是__block
变量配置在栈上还是堆上,都可以顺利地访问一个__block
变量。
截获对象
先来看一段Blcok截获可变数组对象的例子:
blk blk;
{
NSMutableArray *array = [[NSMutableArray alloc] init];
blk = ^(id obj){
[array addObject:obj];
NSLog(@"arrayCount = %lu",(unsigned long)array.count);
};
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
执行该段代码的结果为
arrayCount = 1;
arrayCount = 2;
arrayCount = 3;
从表面上看是没什么问题,运行的结果也是正确的。而实际上如果我们大量的调用block向可变数组中添加对象元素程序会强制结束。原因是block截获的NSMutableArray对象是分配在栈上的,随着当可变数组元素增加到一定程度会造成栈溢出。
解决方法是调用copy
方法,形式如下:
blk = [^(id obj){
[array addObject:obj];
NSLog(@"arrayCount = %lu",(unsigned long)array.count);
} copy];
Block循环引用
在实际项目中对于Block最常见的问题应该是循环引用。如果Block中使用了__strong
修饰符的对象,那么当block从栈复制到堆时,该对象为Block所持有。这样容易造成循环引用,比较明显的我想大家肯定遇到过,我们来看一个比较隐蔽的,源代码如下:
typedef void (^blk_t)(void);
@interface MyObject : NSObject
@property (nonatomic, strong) id obj;
@property (nonatomic, strong) blk_t blk;
@end
@implementation MyObject
- (id)init
{
self = [super init];
_blk = ^{NSLog(@"obj = %@",_obj);};
return self;
}
@end
通过编译器我们可以看到造成了循环引用,即Block语法内部使用了_obj
变量,是因为_obj
变量实际上截获了self。对编译器来说,_obj
变量只不过是对象的成员变量罢了。
解决的方法便是便是通过__weak
修饰符来修饰会被Block捕获的变量:
id __weak obj = _obj;
_blk = ^{NSLog(@"obj = %@",obj);};
还有一种定义设置weak变量的方式,可以用于宏定义。代码如下:
#define WS(weakSelf) __weak __typeof(&*self)weakSelf = self;
调用WS(ws)
之后ws
就变成为了__weak
修饰符修饰的self
了。
如果你觉得我写的东西对你有点价值的话,希望你能为我增加一个关注量。我的公众号「iOSTalk」。