【iOS】OC底层系列五 - block的变量捕获

面试题:

  1. block的原理是怎样的?本质是什么?
  2. __block的作用是什么?有什么使用注意点?
  3. block的属性修饰词为什么是copy?使用block有哪些注意点?
  4. block在修改NSMutableArray,需不需要添加__block

一、block的本质

block其实就是一个封装代码的代码块。

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
int main(int argc, const char * argv[]) {
@autoreleasepool {
/* 定义block(加上一对小括号的意思就可以直接调用block)
^{
NSLog(@"this is a block");
}();
// 输出:this is a block
*/

/*
完整的定义block:
返回值 (^变量名称)(入参类型) = ^(形参){
代码
};
*/

// 示例:
void (^block)(void) = ^{
NSLog(@"this is a block");
};
// 调用block
block();
// 输出:this is a block
}
return 0;
}

1.1. block的内部实现逻辑

我们把上面的代码转换为C++看一下block被转成了什么:

1
2
3
4
5
6
7
8
9
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
// 定义block变量(关键代码:__main_block_impl_0)
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
// 执行block内部的代码(关键代码:(__block_impl *)block)->FuncPtr)
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
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
// block对象的具体信息
struct __block_impl {
void *isa; // block类型信息
int Flags; // 默认0
int Reserved; // 保留字段
void *FuncPtr; // block实现逻辑函数的地址
};

// main函数中block的实现结构体
struct __main_block_impl_0 {
// block的封装对象信息(此处相当于把__block_impl的内部变量信息直接替换到这里)
struct __block_impl impl;
// block的描述信息(最终指向构造函数中__main_block_desc_0的地址)
struct __main_block_desc_0* Desc;
// __main_block_impl_0的构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) : outString(_outString) {
impl.isa = &_NSConcreteStackBlock; // 函数isa指针(block的类型信息)
impl.Flags = flags; // 默认0
impl.FuncPtr = fp; // __main_block_func_0的地址
Desc = desc; // __main_block_desc_0的地址
}
};

// 定义的block描述信息(定义__main_block_desc_0类型的结构体变量__main_block_desc_0_DATA)
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

// 封装了block执行逻辑的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_n4_mw0gb4rs5y99y17hpf8nrxb40000gn_T_main_86ad58_mi_0);
}

block的定义:定义的block被转成了__main_block_impl_0,它的内部有一个结构体变量__block_impl,在__block_impl内部有一个isa指针,用来描述block的类型信息,*FuncPtr是一个函数指针,指向block执行逻辑的函数地址。在__main_block_impl_0内部,指针*Desc__main_block_desc_0类型指针,里面主要描述了block占用内存大小等信息。__main_block_impl_0()是一个构造函数。

block的执行:执行block的代码被封装成结构体__main_block_func_0。这个结构体被当做参数传给了构造函数__main_block_impl_0,在构造函数__main_block_impl_0的内部__main_block_func_0__block_impl的指针FuncPtr引用,最终使用这个指针进行block的调用。

因此block本质上也是一个OC对象(它内部有isa指针,函数调用地址、占用内存大小、外部变量等信息)。总结一句话就是block是封装了函数调用以及函数调用环境的OC对象

block的底层结构如下图所示:

二、block的变量捕获

为了保证block内部能够正常访问外部的变量,block有个变量捕获机制。

知识补充:

在C语言中定义一个局部变量时,有三个关键字:autostaticregister

auto:局部变量在定义的时候默认是auto,所以可以忽略不写,例:auto int age = 10;可以直接把auto忽略写成int age = 10;auto是自动变量的意思,也就是离开当前作用域(距离最近的大括号)就会自动销毁。

static:存放在静态区,程序结束才会释放内存。但如果使用static修饰局部变量,仅仅是访问权限受到限制而已,只能在当前所在函数操作static修饰的局部变量。

register:把局部变量的值使用寄存器进行存储,和auto一样都是随着函数调用而结束内存,平时开发时用的很少,所以不展开讨论。

捕获到block内部:在block内部会专门新增一个成员用来存储外面传入的值,这个操作叫做捕获(capture)。

2.1. auto变量(局部变量)

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
void (^block)(void) = ^{
NSLog(@"age is %d", age);
};
age = 20;
block();
// 输出:age is 10
}
return 0;
}

为什么上面代码中age的输出是20?我们把代码转为C++一探究竟。

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
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 10;
// 把变量age的值传入__main_block_impl_0函数
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
age = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// block内部新增成员变量age
int age;
// 构造函数传入的参数_age自动赋值给成员变量age
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

// 在block执行函数的内部直接访问__main_block_impl_0的成员变量age
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_n4_mw0gb4rs5y99y17hpf8nrxb40000gn_T_main_a74c49_mi_0, age);
}

从上面的代码中可以看到,在最开始定义block的时候,已经把age的值当做参数传入到block内部进行存储了(值传递)。即使在block外部修改age的值,也不影响block内部age的值。

通俗一点讲,由于使用的是auto修饰,所以block会先把局部变量的值保存起来,防止局部变量被随时销毁。

2.2. static变量(局部变量)

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
static int height = 10;
void (^block)(void) = ^{
NSLog(@"age is %d, height is %d", age, height);
};
age = 20;
height = 20;
block();
// 输出:age is 10, height is 20
}
return 0;
}

转C++后的代码:

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
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 10;
static int height = 10;
// 传入变量age的值、变量height的地址
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
age = 20;
height = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 新增成员变量age和height
int age;
// 存储的是外部出入的变量地址
int *height;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), 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 age = __cself->age; // bound by copy
int *height = __cself->height; // bound by copy

// 访问height的时候,访问的是变量地址所指向的内存(*height)
NSLog((NSString *)&__NSConstantStringImpl__var_folders_n4_mw0gb4rs5y99y17hpf8nrxb40000gn_T_main_4a5f22_mi_0, age, (*height));
}

发现使用static修饰的局部变量,在block中使用的是地址传递(指针传递),所以在外部修改static修饰的局部变量存储的值时会影响block内部访问时的值,因为在外部修改和在block内部访问的都是同一个变量内存地址。

由于局部变量是用static修饰的,存储在静态区,程序结束后才会释放,且只能在局部变量所在函数中访问,防止跨函数访问局部变量时所在函数执行结束而找不到static修饰的局部变量,所以block内部使用的是地址传递。

2.3. 全局变量

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int age_ = 10;
static int height_ = 20;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block)(void) = ^{
NSLog(@"age is %d, height is %d", age_, height_);
};
age_ = 20;
height_ = 20;
block();
// 输出:age is 20, height is 20
}
return 0;
}

转C++后的代码:

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
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
age_ = 20;
height_ = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}

int age_ = 10;
static int height_ = 20;

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// 直接访问的全局变量
NSLog((NSString *)&__NSConstantStringImpl__var_folders_n4_mw0gb4rs5y99y17hpf8nrxb40000gn_T_main_15dfb8_mi_0, age_, height_);
}

全局变量不管是否使用static修饰,在block中并没有捕获全局变量的值或地址,都是直接访问变量。

全局变量存储在全局区,程序结束后才会释放内存,并且和block同处一个文件访问权限,可以随意访问,所以不需要捕获到block内部。

2.4. self

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
@interface DBPerson : NSObject

@property (nonatomic, copy) NSString *name;

- (instancetype)initWithName:(NSString *)name;

@end


@implementation DBPerson

- (void)test {
void (^block)(void) = ^{
NSLog(@"%@", self);
};
block();
}

- (instancetype)initWithName:(NSString *)name {
self = [super init];
if (self) {
self.name = name;
}
return self;
}

@end

疑问:test函数中的self会不会被block捕获?

转C++代码:

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
// test函数中block的实现
struct __DBPerson__test_block_impl_0 {
struct __block_impl impl;
struct __DBPerson__test_block_desc_0* Desc;
DBPerson *self;
__DBPerson__test_block_impl_0(void *fp, struct __DBPerson__test_block_desc_0 *desc, DBPerson *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

// block的执行逻辑代码
static void __DBPerson__test_block_func_0(struct __DBPerson__test_block_impl_0 *__cself) {
DBPerson *self = __cself->self; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_n4_mw0gb4rs5y99y17hpf8nrxb40000gn_T_DBPerson_8d7ab4_mi_0, self);
}

// test函数
static void _I_DBPerson_test(DBPerson * self, SEL _cmd) {
void (*block)(void) = ((void (*)())&__DBPerson__test_block_impl_0((void *)__DBPerson__test_block_func_0, &__DBPerson__test_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

// 属性name自动生成的setter方法
static NSString * _Nonnull _I_DBPerson_name(DBPerson * self, SEL _cmd) {
return (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_DBPerson$_name));
}

// 属性name自动生成的getter方法
static void _I_DBPerson_setName_(DBPerson * self, SEL _cmd, NSString * _Nonnull name) {
objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct DBPerson, _name), (id)name, 0, 1);
}

通过上面的代码发现,self会被捕获,而且self是当做局部变量被捕获到block内部的。每一个对象方法都有两个默认参数:self_cmd,所以可在方法中直接使用self进行类的实例对象操作和_cmd打印当前方法信息。从这也可以看出,在对象方法中能够使用self,是因为每一个方法都会传入self_cmd两个默认参数,这样才能知道不同对象的self指向的是哪个实例对象。

如果在block中访问类的成员变量,会被捕获到block内部么?

1
2
3
4
5
6
- (void)test {
void (^block)(void) = ^{
NSLog(@"%@", _name);
};
block();
}

转C++:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// test函数中block的实现
struct __DBPerson__test_block_impl_0 {
struct __block_impl impl;
struct __DBPerson__test_block_desc_0* Desc;
DBPerson *self;
__DBPerson__test_block_impl_0(void *fp, struct __DBPerson__test_block_desc_0 *desc, DBPerson *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

// block的执行逻辑代码
static void __DBPerson__test_block_func_0(struct __DBPerson__test_block_impl_0 *__cself) {
DBPerson *self = __cself->self; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_n4_mw0gb4rs5y99y17hpf8nrxb40000gn_T_DBPerson_d96a82_mi_0, (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_DBPerson$_name)));
}

// test函数
static void _I_DBPerson_test(DBPerson * self, SEL _cmd) {
void (*block)(void) = ((void (*)())&__DBPerson__test_block_impl_0((void *)__DBPerson__test_block_func_0, &__DBPerson__test_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

发现并没有捕获_name,而是使用self访问的name。因为self已经被捕获,而且可以通过self直接访问成员变量,所以就没有必要再次捕获成员变量。

其实在编写代码的时候,编译器已经提示我们:block已经捕获了self变量,只需要表明现在访问的成员变量是self的成员变量就行了。

1
2
3
4
5
6
- (void)test {
void (^block)(void) = ^{
NSLog(@"%@", self->_name);
};
block();
}