轻松理解Block  

网上有大量文章对Block进行讲解,每个人有每个人的理解方式,今天我想用回答问题的方式,比较轻松地把Block弄个明白。弄明白的目的是保证我们在写代码时不会犯错,能更好地使用Block。如果有描述不对的地方,望小伙伴指出~

Block为什么能截获局部变量的值却不能改变它的值?

想一下通常我们要保存一个变量的值,它既不是全局变量也不是静态变量,那能把这个变量放在什么地方呢?把它放在对象中是最直接的,只要对象的引用计数不为0,这个对象就不会被销毁掉。其实编译器也大致是这个思路,把Block语法转换成了C++中的结构体。

按照变量的存储位置,局部变量通常有两种类型:值类型和引用类型(也即对象类型)。编译器对使用到这两种类型的局部变量的Block的转换稍有不同,我把它们放在一起,以便做比较。准备示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main(int argc, char * argv[]) {
@autoreleasepool {
int val = 10; // 值类型的局部变量
void (^blk)(void) = ^{
printf("capture val: %d", val);
};

val = 20;

blk(); // 输出10

NSMutableArray *array = [NSMutableArray arrayWithObject:[[NSObject alloc] init]]; // 引用类型的局部变量
void (^obj_blk)(void) = ^{
printf("array count: %lu", (unsigned long)array.count);
};
[array addObject:[[NSObject alloc] init]];

obj_blk(); // 输出2

return 0;
}
}

在终端执行-rewrite-objc -fobjc-arc main_blk.m,会生成一个main_blk.cpp文件。摘出其中关键的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

struct __main_block_impl_1 {
struct __block_impl impl;
struct __main_block_desc_1* Desc;
NSMutableArray *__strong array;
__main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, NSMutableArray *__strong _array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

// main函数里Block语法块经过编译后的代码
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));

void (*obj_blk)(void) = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA, array, 570425344));

由此可以看到,Block实际上是C++中的结构体的实例,并且该结构体的命名为Block所在的函数名(此处为main),和Block出现的顺序(从0开始)组成。该Block的结构体将我们在Block语法块中用到的局部变量作为了成员变量,在执行它的构造函数生成实例时,会把当前的这些局部变量传入进去。在我们定义blk的Block语法块时,因为val为值类型变量,就把val当前的值传入到了构造函数中,这样val的当前值就被保存下来了。同样地,在定义obj_blk的Block语法块时,因为array为引用类型变量,所以传入到构造函数中的是数组对象的引用,这样数组对象当前的引用array就被保存下来了。

再来看一下,被保存下来的局部变量在Block的结构体中是怎样被使用的:

1
2
3
4
5
6
7
8
9
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int val = __cself->val; // bound by copy
printf("capture val: %d", val);
}

static void __main_block_func_1(struct __main_block_impl_1 *__cself) {
NSMutableArray *__strong array = __cself->array; // bound by copy
printf("array count: %lu", (unsigned long)((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
}

Block语法块的方法体会对应地生成一个C++函数,该函数需要传入它所属的Block的结构体,这样就可以拿到当时被保存下来的局部变量了。对于值类型来说,Block语法块执行之后,它再怎么改变,都不会影响到Block的结构体中保存下来的值了。对于引用类型来说,这里可以看到,是用的__strong修饰符,也就是原来的数组对象多了一个引用,所以在Block语法块执行之后,再往array中添加元素,Block中打印出的count会受到影响。

在Block语法块中改变val值也不会影响到Block外面的局部变量val。对于引用类型array来说,在Block语法块中改变数组中的元素的是会影响到Block外面的局部变量array,但是如果给array重新赋值,也不会改变Block外面的局部变量array。下面看一下,为什么局部变量加上了__block修饰就可以改变被Block截获的局部变量的值。

为什么Block能改变__block修饰的局部变量的值?

局部变量用__block修饰后,在Block中改变该局部变量的值,此时它的值在Block外也被改变了,说明编译器对__block变量做了特别的转换。结合上面分析的Block的结构,截获到的局部变量保存在Block的结构体中,那么思考一下,如果是你,你会怎样来解决这个问题呢?改变Block结构体中的变量,Block外面对应的这个变量的在值也会改变。来看一下编译器是怎么做的,把之前的代码加上__block,并在Block语法块中改变valarray的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int main(int argc, char * argv[]) {
@autoreleasepool {
__block int val = 10;

void (^blk)(void) = ^{
val = 30;
};

NSLog(@"val: %d", val);

blk();

__block NSMutableArray *array = [NSMutableArray arrayWithObject:@"Object1"];

void (^obj_blk)(void) = ^{
array = [NSMutableArray arrayWithObject:@"Object2"];
};

NSLog(@"array: %@", array);

obj_blk();

return 0;
}
}

再次用clang编译,先来看一下跟值类型变量val相关的编译后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// __block变量的结构体
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};

// blk语法的结构体
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) = 30;
}

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};

main中语句的转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {
(void*)0,
(__Block_byref_val_0 *)&val,
0,
sizeof(__Block_byref_val_0),
10
};


void (*blk)(void) = ((void (*)())&__main_block_impl_0(
(void *)__main_block_func_0,
&__main_block_desc_0_DATA,
(__Block_byref_val_0 *)&val,
570425344
));

NSLog((NSString *)&__NSConstantStringImpl__var_folders_nx_dxppf5lj151119zfqd6lrc_m0000gn_T_main_blk_4f4f81_mi_0, (val.__forwarding->val));

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

__block变量val转换成了__Block_byref_val_0结构体实例,可以理解成把该变量val包装到了一个对应的引用类型的变量中,同时在blk语法的结构体__main_block_impl_0中保存了对该引用类型变量的引用。所以当在Block里改变这个引用类型变量的成员变量val时,Block外的引用类型变量的成员变量val也改变了。所以在OC中变量val的值被Block内部改变了。用图可表示如下:

到这里你可能会有疑问了,既然把__block变量转换成结构体后就能够达到在Block内改变Block外的局部变量的值,那为什么上面编译后的代码都是通过__forwarding赋值的呢?先来看一下__forwarding的赋值过程:

  • 在main函数中通过构造函数,赋给__Block_byref_val_0的成员变量__forwarding的值是val变量自己。也就是说,此时__forwarding指向的是自己所在的结构体。
  • __main_block_impl_0的构造函数中,赋给成员变量val的值是传入构造函数的_val__forwarding。也就是说,__main_block_impl_0中的val__Block_byref_val_0中的__forwarding所指向的会一直一样。

这里有点绕,我们用图来说一下:

先来分析一下在__main_block_func_0中修改的是哪个值呢?

1
2
__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 30;

__cself->val就是上图的val2,val2与obj1中的__forwarding指向一致。__forwarding在这里又指回obj1,val2也就指向obj1。所以val->__forwarding指向obj1,(val->__forwarding->val) = 30也就是给obj1中的val赋值为30。

再看一下在Block外面又用的是哪个值呢?

1
NSLog((NSString *)&__NSConstantStringImpl__var_folders_nx_dxppf5lj151119zfqd6lrc_m0000gn_T_main_blk_4f4f81_mi_0, (val.__forwarding->val));

(val.__forwarding->val)这里第一个val是上图中的val1,val.__forwarding指向obj1,所以(val.__forwarding->val)这里拿的就是obj1中的val的值。

然而__forwarding如果一直指向__block结构体自己也就没有什么存在的价值了。实际上,刚创建完的__block变量和Block语法是分配在栈上的,我们可以对Block执行copy操作,将Block从栈复制到堆上,__block变量也会一起随着从栈上复制到堆上。可以做个实验,此时在Block中改变截获的__block修饰的局部变量的值,依然可以改变。此时的__forwarding指向的就是堆上的__Block_byref_val_0的实例了。

再分析一下此时在__main_block_func_0中修改的是哪个值。

__cself->val就是上图的val2,val2与obj1中的__forwarding指向一致。__forwarding在这里指向了obj2,val2也就指向obj2,而obj2的__forwarding又指向了自己,所以val->__forwarding指向obj2,(val->__forwarding->val) = 30也就是给obj2中的val赋值为30。

再看一下此时在Block外面又用的是哪个值。

(val.__forwarding->val)这里第一个val是上图中的val1,val.__forwarding指向obj2,所以(val.__forwarding->val)这里拿的就是obj2中的val的值。

所以__forwarding使得__block变量不管是配置在栈上还是配置在堆上都能够被正确访问到。

到此,为什么Block能改变block修饰的局部变量的值就分析完了。`block变量是引用类型时编译出来的代码与值类型差不多,下面是引用类型array`编译后相关的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
struct __Block_byref_array_1 {
void *__isa;
__Block_byref_array_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSMutableArray *__strong array;
};

struct __main_block_impl_1 {
struct __block_impl impl;
struct __main_block_desc_1* Desc;
__Block_byref_array_1 *array; // by ref
__main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, __Block_byref_array_1 *_array, int flags=0) : array(_array->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __main_block_func_1(struct __main_block_impl_1 *__cself) {
__Block_byref_array_1 *array = __cself->array; // bound by ref

(array->__forwarding->array) = ((NSMutableArray * _Nonnull (*)(id, SEL, ObjectType _Nonnull __strong))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("arrayWithObject:"), (id _Nonnull)(NSString *)&__NSConstantStringImpl__var_folders_nx_dxppf5lj151119zfqd6lrc_m0000gn_T_main_blk_4f4f81_mi_2);
}

static void __main_block_copy_1(struct __main_block_impl_1*dst, struct __main_block_impl_1*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_1(struct __main_block_impl_1*src) {_Block_object_dispose((void*)src->array, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_1 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_1*, struct __main_block_impl_1*);
void (*dispose)(struct __main_block_impl_1*);
} __main_block_desc_1_DATA = { 0, sizeof(struct __main_block_impl_1), __main_block_copy_1, __main_block_dispose_1};

// main 中的代码
__attribute__((__blocks__(byref))) __Block_byref_array_1 array = {(void*)0,(__Block_byref_array_1 *)&array, 33554432, sizeof(__Block_byref_array_1), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSMutableArray * _Nonnull (*)(id, SEL, ObjectType _Nonnull __strong))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("arrayWithObject:"), (id _Nonnull)(NSString *)&__NSConstantStringImpl__var_folders_nx_dxppf5lj151119zfqd6lrc_m0000gn_T_main_blk_4f4f81_mi_1)};

void (*obj_blk)(void) = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA, (__Block_byref_array_1 *)&array, 570425344));

NSLog((NSString *)&__NSConstantStringImpl__var_folders_nx_dxppf5lj151119zfqd6lrc_m0000gn_T_main_blk_4f4f81_mi_3, (array.__forwarding->array));

((void (*)(__block_impl *))((__block_impl *)obj_blk)->FuncPtr)((__block_impl *)obj_blk);

为什么要对Block调用copy,以及何时需要?

从上面转换出的代码可以看到Block刚被创建时是分配在栈的,__block变量同样也是在创建时是被分配在栈的。Block和__block变量的实质如下表所示:

名称 实质
Block 栈上的Block的结构体实例
__block变量 栈上__block变量的结构体实例

另一方面Block也可以当作Objective-C对象看待,上面我们看到了此时Block的类为_NSConcreteStackBlock,但是它还可以是与之类似的其他的类:

设置对象的存储域
_NSConcreteStackBlock
_NSConcreteGlobalBlock 程序的数据区域(.data区)
_NSConcreteMallocBlock

即使转换后的代码通常是_NSConcreteStackBlock,但是以下情况,Block为_NSConcreteGlobalBlock类的对象,也就是会放到程序的数据区域:

  • Block是写在全局变量的地方
  • Block语法的表达式中没有用到外面的局部变量。

那什么时候Block是_NSConcreteMallocBlock类的对象呢?也就是Block和__block变量什么时候会被分配到堆上呢?

分配在栈上的变量会在作用域结束后被废弃,但是Block也可以作为参数被传递,作为返回值被返回,这些时候都是超出了其作用域的。为了Block在超出其作用域后仍然能够正确使用,需要把Block放在堆上,因为堆上的对象只要它的引用计数不为0就不会被废弃。

还有一种是我们上面的例子中array的情况,Block截获到的是对象。可以看到,在__main_block_impl_1中出现了NSMutableArray *__strong array,由__strong修饰的引用类型成员变量,但是C语言结构体中不能含有__strong修饰符的变量,因为编译器不知道应何时进行C语言结构体的初始化和废弃操作,不能很好地管理内存。

那如何来将栈上的Block放到堆上呢?既然Block也可以看做Objective-C对象,也可以对其执行copy操作,这样就可以把栈上的Block复制到堆上,同时被Block截获的__block变量也会一起被复制到堆上。

对Block执行copy的影响如下:

Block的类 原Block的配置存储域 复制效果
_NSConcreteStackBlock 从栈复制到堆
_NSConcreteGlobalBlock 程序的数据区域 什么也不做
_NSConcreteMallocBlock 引用计数增加

Block从栈复制到堆时对__block变量产生的影响

__block变量的配置存储域 Block从栈复制到堆时的影响
从栈复制到堆时并被Block持有
被Block持有

那是不是需要将Block从栈复制到堆上时就需要我们手动对Block调用copy方法呢?当然不是的,编译器能够在下面这些情况时自动对Block进行复制:

  • Block截获对象时
  • Block作为函数返回值返回时
  • 将Block赋值给附有__strong修饰符id类型或Block类型的成员变量时

还有一种情况也不需要我们手动对Block复制:

  • 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时

因为这种情况是在这些方法或函数内部对传入的Block参数进行复制了。这里我们也可以借鉴一下,对于暴露给别人用的方法或函数需要传入Block时,在方法或函数的内部对该Block调用copy,确保安全使用。

其他情况时就要我们自己来判断是否要对Block执行copy了,从上面列出的copy对Block的影响可以看到,多次copy是没有太多影响了,在ARC的情况内存依然能正确被释放。在不确定时,都可以对Bloc进行copy。但是将Block从栈上复制到堆上其实是一次比较耗内存的操作,如果我们能弄明白其中的来龙去脉,也能帮助我们写出更高效的程序。

为什么会发生循环引用,以及如何避免?

通过上面编译后的代码可以看到,Block会持有它所截获到的外部变量,这就有可能导致循环引用。如果Block是类的成员变量,这个Block会被类的实例对象持有,在Block中又用到了self,或者其他成员变量,如此Block又会持有self,所以循环引用就发生了。

如何避免循环引用呢?

  • 声明附有__weak修饰符的变量,并将self赋值使用。我们用clang来转换一下下面的代码看看。
1
2
3
4
5
6
7
NSMutableArray *array = [NSMutableArray arrayWithObject:[[NSObject alloc] init]];

NSMutableArray *__weak temp = array;

void (^obj_blk)(void) = ^{
[temp addObject:@"Hello World!"];
};

在用clang需要指定iOS的版本:

1
xcrun -sdk iphonesimulator clang -rewrite-objc -fobjc-arc -stdlib=libc++ -mios-version-min=12.1 -fobjc-runtime=ios-12.1 -Wno-deprecated-declarations main.m

转换后的Block结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct __main_block_impl_1 {
struct __block_impl impl;
struct __main_block_desc_1* Desc;

NSMutableArray *__weak temp;

__main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, NSMutableArray *__weak _temp, int flags=0) : temp(_temp) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

可以看到,Block的结构体中存的就是__weak的成员变量temp,而不再是__strong了。Block对截获的变量持有弱引用的问题是,Block无法控制截获变量的生命周期,有可能你还在用该变量呢,但是它在Block外面已经被释放了,所以使用时要额外注意。

  • __block变量避免循环引用。
1
2
3
4
5
6
__block id temp = self;

blk_ = ^{
NSLog(@"self = %@", temp);
temp = nil;
}

这里关键是要注意,Block一定要执行,在Block中用完截获的变量时一定要将其置为nil。这样就打破了__block变量对对象的强引用。在确保Block一定会执行时,最好使用__block变量避免循环引用,因为可以动态决定是否将nil或其他对象赋值在__block变量中,

所有总结,完。

参考资料

[1] 《Objective-C高级编程》

本文作者:意林
本文链接:http://www.shinancao.cn/2019/06/08/iOS-block-1
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!