狮子书笔记-理解 Block

背景

每次看完 狮子书🦁️ 的 Block 部分,过一段时间记忆就会开始模糊…

想达到熟悉源码实现的程度,恐怕还需要反复翻看和一定场景的锻炼 ,而大部分开发日常也碰不到需要源码的场景。

不过 Block 作为 iOS 日常开发高频使用的东西,无论如何还是需要想办法去加深记忆的。于是就有了这篇笔记,加入了一些自己的 图解,图像理解起来不会那么抽象,也许能帮助我们(可能只是菜鸡的我)更好的记忆。

文章主要按下面顺序来做:

  • Block 语法
  • Block 使用
  • Block 实现 - Block 如何做到捕获变量
  • Block 实现 - Block 为什么不能直接对捕获的自动变量赋值?
  • Block 实现 - __Block 说明符的相关实现
  • Block 存储域 - NSConcreteStackBlcok / NSConcreteMallocBlock 的关系
  • Block 存储域 - 为什么要有 MallocBlock?
  • Block 存储域 - __Block 变量 如何从栈复制到堆
  • Block 存储域 - 为什么 __Block 变量要有成员变量__forwarding
  • Block 实现 - 截获对象的引用和释放

本文内容基本根据 《Objective-C 高级编程 - iOS 与 OS X 多线程和内存管理》而来,推荐所有 iOS 开发者阅读 📖

Block 语法

对于如何定义一个 Block,首先一定要去理解 Block 的 BN 范式 (Backus-Naur Form):

1
2
3
4
5
6
7
Block_literal_expression ::= ^ block_decl compound_statement_body

block_decl ::=

block_decl ::= parameter_list

block_decl ::= type_expression

关键在于记忆 block_decl 的定义 ,就能比较理解容易 Block 的语法了。

如果不太明白 BN 范式 的意思,尝试看看 BNF范式(巴科斯范式) 会有助于我们理解,其实就是这副图的关系:

再结合 Block 具体的 4 种写法:

  • ^ 返回值类型 参数列表 表达式
  • ^ 返回值类型 参数列表 表达式
  • ^ 返回值类型 参数列表 表达式
  • ^ 返回值类型 参数列表 表达式

上面可以看到, Block 语法中是有省略写法的,对于省略的情况主要遵循 2 点规则:

  • 如果省略返回值,那么 return 什么类型就返回什么类型,多个 return 则需要相同类型
  • 如果省略参数列表,即参数类型为 void 可以省略,代表不使用参数。

日常比较多的一种写法是 同时省略,返回值是void,参数也是 void ,可以这么写:

1
^{ printf("hello"); }

值得注意的是 Block 同时省略返回值和参数时,返回类型也只取决于 return 的类型 ,例如这么写:

1
2
3
4
NSInteger num = ^{ return 8;}();

NSLog(@"num %@",@(num))
//输出: num 8

掌握了上面的东西,在手写 Block 的能力上估计能有所进步了,虽然大家肯定还是补全用的多 : )

Block 使用

在使用上,是可以直接把 Block 当作一个变量来使用的:

  • 自动变量 (局部变量)
  • 函数参数
  • 静态变量 (静态局部变量)
  • 静态全局变量
  • 全局变量

创建 Block 作为变量使用的语法:

1
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...}

举个例子🌰 :

1
2
3
4
5
6
7
8
9
//将 Block 赋值为 Block 类型变量
int (^blk)(int) = ^ (int count){
return count + 1;
};

int (^blk1)(int) = blk;

int (^blk12);
blk12 = blk1;

typedef 声明 Block 类型变量

Block 在作为函数参数返回值时,写起来比较冗长:

1
2
3
4
5
6
7
8
9
10
11
//Block 作为函数参数
void func(int (^blk_t)(int))
{
//do something
}

//Block 作为函数返回值
int (^func())(int)
{
return ^(int count){ return count + 1;};
}

我们能通过 typedef 来简化 Block 的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef int (^blk_t)(int)

//Block 作为函数参数
void func(blk_t blk)
{
//do something
}

//Block 作为函数返回值
blk_t func()
{
return ^(int count){ return count + 1;};
}

Block 的指针类型变量

Block 类型变量可以使用指向 Block 类型变量的指针来访问:

1
2
3
4
5
6
7
typedef int (^blk_t)(int)

blk_t blk = ^(int count){ return count+1 };

blk_t *blkptr = &blk;

(*blkptr)(10);

Block 截获变量

通过实例看一下什么是 Block 截获变量:

1
2
3
4
5
6
7
8
9
10
11
int val = 10;
const char *fmt = "val = %d \n";

void (^block)(void) = ^{
printf(fmt,val);
};

val = 2;
fmt = " the values were changed.val = %d \n";

block();

结果的输出:

val = 10

说明代码中的 val / fmt 在执行 Block 语法时的 瞬间值被截获(被保存),然后在执行时会使用截获的变量。

Block 的实现

截获变量 作为 Block 的重要特性,下面的篇幅,也将回答 3 个截获变量问题:

  • 为什么可以截获变量
  • 为什么 Block 内给截获变量赋值会报错
  • Block 如何实现修改截获的变量

在文件 main.m 的代码:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(int argc, const char * argv[]) {
void(^block)(void) = ^()
{
printf("hello");
};
block();
return 0;
}

通过命令:

1
clang -rewrite-objc  main.m

在转换后的文件 main.cpp 中,找到 main 方法关联的实现:

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
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

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) {

printf("hello");
}

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

int main(int argc, const char * argv[]) {
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}

对于上面代码里的 名称规则,以 __main_block_impl_0 为例介绍一下:

image-20210328090125407

Block 结构

根据上面代码,整理出来 Block 的结构和对应的关系:

生成一个类型为 __main_block_impl_0 结构体变量 block ,构成也比较简单,由 2 部分组成:

  • 类型为 __block_impl 的结构体 impl
  • 类型为 __main_block_desc_0 的结构体 Desc

如图:

image-20210328083717302

这里最重要的就是 impl 了,它是一个 __block_impl 结构体:

image-20210328084813685

Desc 是一个 __main_block_desc_0 结构体:

而 Block 通过构造函数,将 __main_block_func_0__main_block_desc_0_DATA 作为参数传入,来生成结构体实例:

1
2
3
4
5
6
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;//flags = 0
impl.FuncPtr = fp;//__main_block_func_0
Desc = desc;//__main_block_desc_0_DATA
}

Block 执行调用

对于 Block 的执行调用为下面的代码:

1
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

这里需要注意,就是__main_block_impl_0类型的 block 被强转换为了__block_impl

这在C语言是可行的,因为__block_impl位于__main_block_impl_0的最顶部,就相当于__block_impl的变量直接排列在__main_block_impl_0的顶部。

上面代码去掉多余的转换部分就变成:

1
(*block->impl.FuncPtr)(block)

其实就是简单使用 函数指针 调用函数 __main_block_func_0

再看一下对应函数 __main_block_func_0 的实现定义:

1
2
3
4
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

printf("hello");
}

函数实现就是 Block 语句内的实现,且 block 本身作为 参数 __cself 进行了传递。

1
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

Block 为什么能截获自动变量值

下面开始来分析 Block 的重点 – 截获自动变量值

因为需要基于实际分析,仍然会把所有代码都贴出来…但建议大家不要直接全看,看多了确实头晕..
等需要对照具体的实现代码去理解,再来看。
重点关注在 Block 截获自动变量值与未截获的 差异

将这一段代码 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int main(int argc, const char * argv[]) {
int val = 10;
const char *fmt = "val = %d \n";

void (^block)(void) = ^{
printf(fmt,val);
};

val = 2;
fmt = " the values were changed.val = %d \n";

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
34
35
36
37
38
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;
int val;

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *fmt = __cself->fmt; // bound by copy
int val = __cself->val; // bound by copy

printf(fmt,val);
}

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

int main(int argc, const char * argv[]) {
int val = 10;
const char *fmt = "val = %d \n";

void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));

val = 2;
fmt = " the values were changed.val = %d \n";

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}

经过对比,我们可以发现,__main_block_impl_0 的实现已经发生了变化,代码如下:

1
2
3
4
5
6
7
8
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;// 多出来的部分,Block 内要使用的变量 fmt
int val;// 多出来的部分,Block 内要使用的变量 val

//构造函数..
};

上面的代码里,多出来了 Block 内要用到的 2 个变量:

image-20210328115748440

然后通过 __main_block_impl_0 初始化的构造函数,对 fmt / val 进行截取和存储:

1
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));

Block 使用截取的自动变量值

因为截取自动变量后的 Block 执行调用没有变化,仍然是通过函数指针调用函数,并传递 __main_block_impl_0 结构体实例作为参数,我们就不再做分析了。

重点放到新的函数实现上:

1
2
3
4
5
6
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *fmt = __cself->fmt; // bound by copy
int val = __cself->val; // bound by copy

printf(fmt,val);
}

通过代码发现,函数执行时使用的自动变量值,是通过访问 __cself 上捕获的自动变量,再赋值给函数内部的局部变量,用于在执行时使用。

Block 捕获和使用自动变量的流程

总结一下 Block 捕获和使用自动变量的流程:

image-20210328141211671

Block 为什么不能直接对截获的自动变量赋值?

当我们尝试对捕获的自动变量进行 赋值操作,一般都会提示编译错误:

1
2
3
4
5
int a = 0 ;
int (^blk)(void) = ^{
a = 9;//❌Variable is not assignable (missing __block type specifier)
return a+1;
};

这是为什么呢?

在截获自动变量值的 __main_block_func_0 实现里,能看到系统自动加上的注释: bound by copy。Block 对它引用的局部变量做了只读拷贝,也就是说block引用的是局部变量的副本。

自动变量 val 虽然被捕获进来了,但是 Block仅仅捕获了val的值,并没有捕获val的内存地址。在 __main_block_func_0 中修改自动变量的值,依旧不能改写 Block 外面的自动变量值。

基于上面原因,Block 内的修改无法影响外部变量的值。

所以在编译层面检测出被截获的自动变量的赋值操作时,就会报编译错误。

另外会提示错误的还有这种情况:

1
2
3
4
5
6
const char text[] = "hello";

void (^blk)(void) = ^{
//错误提示:Cannot refer to declaration with an array type inside block
NSLog(@"num %c ",text[2]);
};

block 不能直接对 C 语言数组做的截获。

Block 如何做到修改自动变量值

想要在 Block 中做到修改变量,在函数内可以使用:

  • 静态全局变量
  • 全局变量

因为 Block 的匿名函数部分的实现还是变换成 C 语言函数,在其中访问 静态全局变量/全局变量 并没有任何改变,可以直接使用。

除此之外,可以用两种方式在 Block 修改变量,一种是 静态变量,一种是利用 __block 说明符 修饰。

静态变量在 Block 中的实现

在 Block 中想修改变量值,除了 静态全局变量/全局变量 之外,使用静态变量也能办到修改变量值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
int main(int argc, const char * argv[]) {
static int val = 10;//原来为:int val = 10;
const char *fmt = "val = %d \n";

void (^block)(void) = ^{
val = 3; // 对变量进行修改
printf(fmt,val);
};

val = 2;
fmt = " the values were changed.val = %d \n";

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
struct __main_block_impl_0 {
...
int *val;//原来为 int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int *_val, int flags=0) : fmt(_fmt), val(_val) {
...
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
...
int *val = __cself->val; // bound by copy,原来为 int val = __cself->val;
(*val) = 3;//修改变量
printf(fmt,(*val));// 原来为 printf(fmt,val);
}

int main(int argc, const char * argv[]) {
static int val = 10;
...
//原来为直接传递 val ,使用 static 声明后用 &val 传递
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &val, fmt));
...
return 0;
}

之前我们说到 Block 不能直接修改捕获的自动变量原因,就是因为只是捕获了值,并没有保存指针,所以无法修改外部的变量。

采用静态变量之后,Block 在捕获的时候传递了指针地址。修改的时候也是使用指针变量。

所以使用静态变量可以做到在 Block 内修改变量的值。

__block 说明符的实现

除了静态变量,使用 __block 修饰也可以在 Block 内修改变量的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc, const char * argv[]) {
__block int val = 10;
const char *fmt = "val = %d \n";
void (^block)(void) = ^{

val = 3;
printf(fmt,val);
};

val = 2;
fmt = " the values were changed.val = %d \n";
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, __Block_byref_val_0 *_val, int flags=0) : fmt(_fmt), 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
const char *fmt = __cself->fmt; // bound by copy

(val->__forwarding->val) = 3;
printf(fmt,(val->__forwarding->val));
}

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

int main(int argc, const char * argv[]) {

__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};

const char *fmt = "val = %d \n";

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

(val.__forwarding->val) = 2;
fmt = " the values were changed.val = %d \n";

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

return 0;
}
__Block 变量使用的结构体

看到一下子这么多代码,有点头晕,实际上最核心的就在于 __Block_byref_val_0 结构体的增加与使用。

整理代码,我们发现被 __block 修饰的 val , 类型从 int 变成了 __Block_byref_val_0 结构体:

image-20210328182239400

生成 __Block_byref_val_0 结构体变量 val

1
2
3
4
5
6
7
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {
(void*)0,// isa
(__Block_byref_val_0 *)&val, //forwarding,指向自身的指针
0, //flags
sizeof(__Block_byref_val_0),//size
10//val,相当于原来的自动变量
};

Block 进行捕获,则传递 val 指针作为参数:

1
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, (__Block_byref_val_0 *)&val, 570425344));
__Block 变量的访问和修改

Block 内对 val 做访问和修改:

1
2
3
4
5
6
7
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
const char *fmt = __cself->fmt; // bound by copy

(val->__forwarding->val) = 3;
printf(fmt,(val->__forwarding->val));
}

通过 __cself->val 取出来变量,并且注释变为了bound by ref,说明是引用传递。

(val->__forwarding->val) 很容易看出是通过访问 val 结构体指针来做修改的:

不过确实存在疑问:

block 变量的实现,为什么要通过 `(val->forwarding->val)` 来访问和修改 ?

直接 val->val 应该也是可以的,通过 __forwarding 要多访问一步。

这里就涉及到 Block 的存储域问题了。

Block 存储域

一般 Block 根据存储位置分为3种:_NSConcreteStackBlock_NSConcreteMallocBlock_NSConcreteGlobalBlock

  • _NSConcreteGlobalBlock
    没有捕获变量,或者只用到全局变量/全局静态/静态变量的 Block,生命周期从创建到应用程序结束。与全局变量一样,GlobalBlock 类对象设置在程序的数据区域(.data区)
  • _NSConcreteStackBlock
    有捕获外部变量,但没有被强引用的 Block 。生命周期由系统控制。StackBlock 类对象设置在栈上。
  • _NSConcreteMallocBlock
    有被强引用或者 copy 修饰的属性的 Block 会被复制一份到堆中成为 MallocBlock,生命周期由程序员控制。MallocBlock 类对象设置在由 malloc 函数分配到内存块(即堆)中。

NSConcreteStackBlock 和 NSConcreteMallocBlock 的关系

NSConcreteMallocBlock 是由 NSConcreteStackBlock 复制而来。

栈 Block 通过什么复制到堆 Block

赋值 Block 使用 objc_retainBlock,而 objc_retainBlock 实际上就是 Block_copy 函数。

一般来说,编译器会自动判断。

为什么要有 NSConcreteMallocBlock ?

NSConcreteMallocBlock 的使用,是为解决这么一种情况:

设置在栈上的 Block,即 StackBlock,当 StackBlock 所属的变量作用域结束,该 Block 和栈上的 __block 变量也会随之被废弃。

StackBlock 作为栈对象的好处是内存分配快,有确切的生命周期,因为函数执行完就会自动销毁。但也因此无法法在函数之外再使用它。

Blocks 于是提供了将 Block/__block 变量从栈上复制到堆上来解决这个问题,即使 Block 语法记述的变量作用域结束,堆上的 Block 还可以继续存在和被使用。

将栈 Block 复制的到堆上面创建堆 Block 对象,就可以通过引用计数来管理它。

而在 ARC 下,大多数情况编译器会恰当的判断,自动生成将 Block 从栈上复制到堆上的代码。

所以 NSConcreteMallocBlock 也是 Block 超出变量作用域还能存在的原因。

概念说起来可能不具体,下面通过 MRC / ARC 的两种情况来看一下 NSConcreteStackBlock 废弃的情况

MRC 下 NSConcreteStackBlock 废弃的情况

要理解为什么需要 MallocBlock ,就要理解 NSConcreteStackBlock 和栈上的 __block 变量被废弃的情况

因为上面说到了 ARC 下会做对 Block 一些自动的处理,现在来做一个对比,如果在 MRC 下不处理 Block,和 ARC 下处理 Block 的结果。

对下面代码分别在 ARC/MRC 下来进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef int (^blk_t)(int);

blk_t func(int rate) {
blk_t blk = ^(int count){
return rate * count;
};
return blk;
}

int main(int argc, const char * argv[]) {

blk_t blk = func(3);
int total = blk(6);
printf("total = %d",total);
return 0;
}

先在 ARC 下运行 ,下面的图里可以看到在 func 函数返回的为 NSConcreteMallocBlock

运行完成的结果如图:

可以看到在 ARC 下,main 函数里的 blk 为 NSConcreteMallocBlock ,total 的结果为18。正常。

然后使用 -fno-objc-arc 对文件做处理,变成 MRC 运行:

从上面的图里看到在 func 函数返回的为 NSConcreteStackBlock

运行完成的结果如图:

我们发现 blk 的类型 isa 变成了 0x0,确实属于 NSConcreteStackBlock 随着变量作用域结束而被废弃的情况。 total 得到也是一个奇怪的值。

解决方式就是把 StackBlock 变成 MallockBlock ,所以 ARC 下,编译器自动判断后帮我们做了一次转换

ARC 下 NSConcreteStackBlock 废弃的情况

大多数情况下 ARC 下会自动复制到堆上,但也存在例外,这时需要 手动copy 将 Block 复制到堆上。

需要手动 copy 的场景

有一种情况需要我们手动 copy ,是编译器不能判断的状况:

  • 向方法或函数传递 Block

下面是一个 Block 作为 参数传递 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
id getBlockArray()
{
NSString *val = @"test";
NSArray *blockList =[[NSArray alloc] initWithObjects:
^{NSLog(@"block0 %@",val);},
^{NSLog(@"block1 %@",val);},
^{NSLog(@"block2 %@",val);},
nil];
return blockList;
}

int main(int argc, const char * argv[]) {

NSArray *array = getBlockArray();
blk_t blk = (blk_t)[array objectAtIndex:1];
blk();
return 0;
}

将代码在 ARC 下运行,发现数组里除了第 0 个是 NSMallocBlock ,后面的都是 NSStackBlock :

image-20210329194859630

接下来在 main 取数组里的 Block ,会发现崩溃了,原因是: StackBlock 随着函数作用域的结束而跟着被废弃了

image-20210329195115527

怎么解决 StackBlock 废弃呢?

那就是把 Block 复制到堆上,变成 MallocBlock 之后,即使超出了函数的作用域也可以被访问和使用。

对应到这段代码,就是代码手动调用 copy :

1
2
3
4
5
NSArray *blockList =[[NSArray alloc] initWithObjects:
^{NSLog(@"block0 %@",val);},
[^{NSLog(@"block1 %@",val);} copy],
[^{NSLog(@"block2 %@",val);} copy],
nil];

这也验证了需要 NSConcreteMallocBlock 在超出函数作用域时使用的原因,否则当 StackBlock 废弃,再去访问就会发生异常。

三种 Block 的 copy 效果

另外这里直接总结一下三种 Block 调用 copy 的效果:

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

__block 变量从栈上复制到堆上

当 Block 被复制到堆上,对应使用的 __Block 变量都是会一起复制到堆上的。

需要注意的是多个 Block 持有同 1 个 Block 变量 的情况是如何复制的。

例如:

栈上的 block1和 block2 同时都用到了栈上的 __block 变量

block1 复制到堆上,__block 变量也跟着被复制到了堆上,并且堆上的__block 变量被堆上的 block1 持有。如图:

image-20210403232407753

接着 block2 也复制到堆上,__block 变量 此时不会再复制 1 份,被复制到堆上的 block2 持有 __block 变量 ,增加 __block 变量 的引用计数。__block 变量 同时被堆上的 block1 和 block2 一起所持有。

__block 结构体使用 forwarding 成员变量的原因

之前提到了,使用 __block 变量 的修改变量值没直接用 var->var 的方式,而是使用 var->__forwarding->var 绕了一下来做访问。

例如下面的情况:

1
2
3
4
5
6
7
8
9
10
{
__block int val = 0;

void (^blk) (void) = [^{++val;} copy];

++val;
blk()

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

代码里 2 处的 ++val 最终同样都用 val.__forwarding->val 来做修改。

重点在于,实际上这 2 处是不同的 __block 变量 类型:

  • Block 语法内的,因为被 copy 了,所以 Block 执行时是 堆上的 __block 变量

  • Block 语法外的,仍旧还是 栈上的 __block 变量

当栈上的 Block 被复制到堆上,此时可以 同时 访问栈上 __block 变量堆上__block变量

如果直接使用 val->val 来访问,那么此时 2 个不同的 __block 变量 ,指向的是栈上和堆上不同的地址,显然会产生 错误的结果

关键的解决方式,就是使用 __forwarding 来做指向:

如图,有了 __forwarding 来做指向的话,那么在栈上的__block 变量 ,最终也会指向 堆上的__block 变量,来保证访问到的都是同一个变量,即此时访问栈 __block 变量 实际访问的是堆 __block 变量

截获对象的使用和废弃

被 MallocBlock 所截获的对象(这里指 OC 对象),就算没有使用 __block 修饰,由于被堆上的 Block 持有,也会延长对象的生命周期,使该对象能够超出其变量作用域而存在。等到堆上的 Block 释放才会调用 dispose 函数去释放对象。

例如下面的情况:

1
2
3
4
5
6
7
8
9
10
11
12
blk_t blk ;
{
id array = [[NSMutableArray alloc] init];
blk = [^(id obj){
[array addObject:obj];
NSLog(@"array count = %ld",[array count]);
} copy];
}

blk([NSObject new]);
blk([NSObject new]);
blk([NSObject new]);

输出:

1
2
3
array count = 1
array count = 2
array count = 3

array 按照道理应该是随着变量作用域结束就会被废弃了,但是调用 blk([NSObject new]) 发现 array 仍旧存在,且运行正常。

原因就是 Block 在堆上 ,对象因为被堆上的 Block 持有了,所以还能超出其变量作用域存在。

copy / dispose 函数

截获对象的一个关键点,就是用 copy 函数和 dispose 函数来进行引用和释放:

  • 栈上的 Block 复制到堆时,会调用 copy 函数
  • 堆上的 Block 被废弃时,会调用 dispose 函数

截获对象截获 __block 变量时,copy / dispose 函数的内部实现是不一样的。

截获 __block 变量:

1
2
3
4
5
6
7
// __block 变量使用  BLOCK_FIELD_IS_BYREF (值为8)
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*/);
}

截获对象:

1
2
3
4
5
6
7
8
// 对象使用  BLOCK_FIELD_IS_OBJECT (值为3)
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, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->val, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
栈上 Block 复制到堆

在下列情况,栈上的 Block 会自动复制到堆:

  • 调用Block 的 copy 实例方法
  • Block 作为函数返回值返回时
  • 将 Block 赋值给附有 __strong 修饰符 id 类型的类或Block 类型成员变量时
  • 在方法名含有有 usingBlock 的 cocoa 框架方法或 GCD 的 API 传递 Block 时 (会在方法内部调用 copy)

总结

这次将 Block 的相关知识梳理了一下,主要针对 Block 的实现,__block 变量的实现, Block/__block 变量 怎么从栈复制到堆,以及回答为什么需要堆 Block,为什么 __block 变量 的结构体要通过 __forwarding 访问等问题。

在整理和画图的同时,相比只读一遍文字和代码,记忆更加深刻了。建议大家也可以在读书时画图整理关系。

如有错误,欢迎指正 !