Block

Blocks 是带有自动变量的匿名函数;本质上是 Objc 对象

Block 的实质

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
// Block 实现结构体
struct __block_impl {
void *isa; // Block 对象指针
int Flags;
int Reserved;
void *FuncPtr; // Block 的实现函数地址
}

// 名字按照一定规律动态生成的结构体
struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size; // Block 所占空间大小
}

strcut __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 DESC;
}

// 名字按照一定规律动态生成的实现函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
}

Block 结构体中的 isa 指针

Objc 中的对象和类结构体中都有一个 isa 指针;对象的 isa 指针指向对应的类;而类的 isa 指针则指向元类

Block 的实现部分就是一个 OC 对象。它的 isa 指针指向对应的类。Block 可能由三个类初始化而来:

  • _NSConcreteStackBlock 该类的 Block 对象存储在栈上
  • _NSConcreteMallocBlock 该类的 Block 对象存储在堆中
  • _NSConcreteGlobalBlock 该类的 Block 对象存储在全局数据区(.data)

_NSConcreteGlobalBlock

当 Block 不捕获自动变量时,Block 结构体实例的内容不依赖于执行时的状态,整个程序只需要一个实例,因此将 Block 实力存放在全局数据区即可

有两种情况下创建的 Block 是全局的:

  1. 将 Block 作为全局变量创建时
  2. Block 不捕获任何自动变量时

_NSConcreteStackBlock

除 _NSConcreteGlobalBlock 之外的情况创建的 Block 都是 _NSConcreteStackBlock 类型的,存放在栈中

_NSConcreteMallocBlock

_NSConcreteMallocBlock 通过 _NSConcreteStackBlock copy 而来。当栈上的 Block 超出作用域之后就会被拷贝到堆中,这样 Block 就可以超出作用域而继续存在

以下情况都会被拷贝:

  1. Block 作为函数返回值
  2. Block 作为函数参数传递
  3. Block 作为对象属性

Block 的 Copy

  • _NSConcreteStackBlock Copy 后从栈复制到堆
  • _NSConcreteMallocBlock Copy 后引用计数增加
  • _NSConcreteGlobalBlock Copy 后什么也不做

Block 变量

C 语言中函数可能使用的变量有:自动(局部)变量、静态局部变量、函数参数、全局变量、静态全局变量。其中静态局部变量、全局变量、静态全局变量作用域超出函数作用域,可以在函数中多次传递;而自动变量和函数参数的作用域和函数相同, 在 Block 中使用时会被 Block 捕获

不带 __block 的变量

Block 中捕获的变量是复制该变量的瞬时值,所以并且不能修改变量的值;同时在 Block 执行之后改变变量的值也不会影响 Block 捕获的变量的值

静态局部变量、全局变量、静态全局变量 同样可以在 Block 中任意读写。其中静态局部变量被 Block 捕获的是变量的指针,所以可以更改。而对于自动局部变量和函数参数而言,超出函数作用域后会被销毁,所以 Block 不能简单的捕获它们的指针地址来达到更改的目的。

  • 捕获的变量会被添加到动态生成的 Block 结构体中,相当于 Block 对象自身的属性
1
2
3
4
5
6
7
strcut __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 DESC;
int val1; // 被捕获变量 val1
int val2; // 被捕获变量 val2
}

带有 __block 的变量

使用 __block 声明的变量可以在 Block 中更改

__block 是一个存储域说明符。存储域说明符用于指定变量值的存储区域或方式,C 语言中有以下存储域说明符:

  • typedef 与内存存储无关,由于语法原因被归入此类
  • auto 默认,表明一个变量具有自动存储时期,只能用在具有代码块作用域的变量声明中,存在栈中
  • register 只能用在具有代码块作用域的变量声明中,存在寄存器中
  • static 静态存储,变量一旦被定义便一直存在直到程序结束
  • extern 声明一个在其他地方定义了的变量
  • const 将数据定为不变的,在只能定义声明,以后不可改变其值

带有 __block 声明的局部变量被捕获后会被包装为一个对象(C 结构体),从而通过结构体中的 __forwarding 指针指向变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 捕获的 __block 变量
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
}

// 同样的会将变量添加到 Block 结构体中
strcut __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 DESC;
__Block_byref_val_0 val; // 被捕获变量 val
}

同时,会在动态生成的描述结构体中增加 copydispose 函数来管理捕获变量的内存

1
2
3
4
5
6
7
8
9
10
11
12
13
static void __main_blick_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) {
}

static void __main_blick_dispose_0(struct __main_block_impl_0 *src) {
}

struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
void (*copy)(struct __main_block_impl_0 *, struct __main_block_impl_0 *);
void (*dispose)(struct __main_block_impl_0 *);
}

__block 变量生成的结构体并不在 Block 结构体中,这样是为了多个 Block 同时捕获一个变量时节省资源

__forwarding

Block 使用捕获的带有 __block 声明的变量时,是通过存储的变量生成的结构体的 __forwarding 指针找到对应的结构体,再访问该结构体中的值

1
2
3
4
5
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // 得到存储的变量结构体
(val->__forwarding->val) = 1; // 访问最终的值
}

当 Block 从栈拷贝到堆中时,它捕获的变量也会随之一起拷贝。使用 __forwarding 指针来访问自身的目的在于当变量被从栈拷贝到堆中时,__forwarding 指针的指向会被重置为指向堆的地址,这样就可以保证栈中和堆中的 __block 变量指向的是同一个地址

初始时 __forwarding 指针指向自身。拷贝发生后堆中的 __block 变量 的 __forwarding 也指向自身

如果两个 Block 捕获同一个 __block 变量。其中一个发生拷贝,另一个没有,则这两个 Block 对象一个引用的是栈中的 __block 变量;而另一个引用的是堆中的 __block 变量

在多个 Block 捕获同一个 __block 变量时,起初 __block 变量存储在栈上,当任意一个 Block 发生拷贝,则 __block 变量也随之拷贝到堆上。之后的 Block 再发生拷贝,只会增加 __block 变量的引用计数

Block 变量的复制和销毁

Block 变量的复制和销毁适用 Objc 引用计数内存管理规则

Block 结构体仅在捕获变量时会生成 copydispose 函数;不捕获变量时是全局变量,不需要销毁

  • copy 函数:在栈上的 Block 复制到堆时调用;copy 会将捕获的变量一同 copy
  • dispose 函数:在堆上的 Block 被销毁时调用(栈上的Block通过栈机制进行内存管理);销毁时会将捕获的变量销毁或减去引用计数

不带 __block 的对象在 copy 和 dispose 时会被标记为 BLOCK_FIELD_IS_OBJECT
__block 声明的对象在 copy 和 dispose 时会被标记为 BLOCK_FIELD_IS_BYREF

循环引用

MRC 环境下使用 __block 来避免循环引用。ARC 也可以使用,但是需要在执行完时将捕获的变量手动置为 nil,否则还是会循环引用。同理如果没有执行 Block 也会循环引用

  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2017-2021 HonQi

请我喝杯咖啡吧~