0%

Block 笔记

Blocks

Blocks 是 C 语言的扩展功能,是带有自动变量(局部变量)匿名函数。这一概念在其他程序语言中也称为闭包(Closure)、Lambda等。
C 语言标准不允许不带有名称的函数,想要不通过函数名来调用函数可以如下所示:

1
2
3
4
5
6
int func(int count) {
return count + 1;
}

int (*funcptr)(int) = &func;
int result = (*funcptr)(10);

但是其实在赋值函数指针时,还是用到了函数的名称。

C语言的函数中使用的变量有一下几种:

  • 函数的参数 - 存储在栈中
  • 自动变量(局部变量)- 存储在栈中
  • 静态变量(静态局部变量)- 作用域与局部变量相同,但是改变了存储方式,从而改变了生存周期
  • 全局变量
  • 静态全局变量 - 与全局变量一样是静态存储方式,但是将其作用域限制在了定义该变量的源文件内

进程在虚拟内存中的结构

能够在函数的多次调用中传递值的变量有:静态局部变量、静态全局变量和全局变量,这几种变量虽然作用域不同,但是在整个程序中,一个变量总是保持在一个内存区域中,函数对其多次访问也是访问同一个值。

Blocks 提供了类似 C++ 和 Objective-C 类生成实例或对象来保持变量值的方法,它保持局部变量的值。同时避免了声明类的大量代码,也没有使用静态变量、静态全局变量或全局变量时,访问的总是同一个值的问题。

Blocks的语法

我们看一个简单的Block的定义:

1
2
3
^int (int count){ 
return count + 1;
};

其实它与一般的C语言函数只有两点不同:1、返回值类型前面带有”^”符合,2、返回值类型后面没有函数名。

1
2
3
int testFunc (int count){ 
return count + 1;
};

我们在通常使用中会省略Block的返回值类型,所以上面的表达式通常写为:

1
2
3
^(int count){ 
return count + 1;
};

在省略返会在类型时,如果表达式中没有return语句,就使用void类型,有return语句就使用该返回值的类型,若有多个return语句,那么所有的语句返回值类型应该相同。
如果不使用参数的话,参数列表也可以省略。

Block类型变量

在 Block 语法下,可将 Block 语法赋值给声明为 Block 类型的变量中。即源代码中一旦使用 Block 语法就相当于生成了可赋值给 Block 类型变量的“值”。Block 类型变量完全可以与一般的 C 语言变量一样使用。
我们将上文中的 Block 赋值给 Block 类型的变量:

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

但是在函数参数或返回值中使用 Block 类型变量时,记述起来很复杂,因此常用 typedef 来解决:

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

Blocks的实现

Block的实质

Block的本质就是 C 语言结构体的实例,Block 就是 Objective-C 对象。
Block 的语法看上去很特别,但实际上是作为普通的 C 语言源代码来处理的。我们可以用 clang 编译器的“clang -rewrite-objc 源代码文件名”指令将源代码转化为我们可读的源代码。

下面我们看如下这段代码转化为 C++ 源代码:

1
2
3
4
5
6
7
int main()
{
void (^blk)(void) = ^{
printf("Block\n");
};
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
36
37
38
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;//函数指针
};

//Block结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;//用于描述这个Block的附加信息

//Block构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;//isa指针指向这个Block所属的类 _NSConcreteStackBlock
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

//通过Block使用的匿名函数其实被转化为简单的C的函数代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");
}

static struct __main_block_desc_0 {
size_t reserved;//为今后版本升级保留的区域
size_t Block_size;//Block的大小

} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

//main 函数
int main()
{
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
return 0;
}

Block截获自动变量值

Block 是带有自动变量(局部变量)匿名函数,”带有自动变量值”在 Block 中表现为截获 Block 中使用到的自动变量值。
我们看一个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main()
{
int dmy = 256;
int val = 10;
const char *fmt = "var = %d\n";
void (^blk)(void) = ^{
printf(fmt,val);
};

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

blk();//打印出var = 10,因为Block保存了该自动变量的瞬间值,在执行Block后即使改变该自动变量值也不会影响Block执行时的自动变量值。
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
36
37
38
39
40
41
42
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;

const char *fmt; //Block中访问的自动变量被作为成员变量追加到了Block结构体实例中
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; // Block实际执行的函数中访问的自动变量来自Block结构体实例
int val = __cself->val; //

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 dmy = 256;//没有在Block中访问,所以没有被截获
int val = 10;

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

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

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

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

return 0;
}

__block说明符

实际上,自动变量值截获只能保存执行 Block 语法瞬间的值,保存后就不能在 Block 中改写该值。试图在 Block 中修改自动变量会导致编译错误。
如果我们要改变 Block 中截获的自动变量值,有以下两个方法:

  • 使用 static 修饰自动变量值将其变为静态变量,修改其存储域。
  • 使用 __block 修饰符

我们看一下下面的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int global_val = 1;//全局变量
static int static_global_val = 2;//全局静态变量

int main()
{
static int static_val = 3;//静态变量

void (^blk)(void) = ^{
global_val *=1;
static_global_val *=2;
static_val *=3;
};
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
36
int global_val = 1;//全局变量与全局静态变量并没有被捕获到Block中
static int static_global_val = 2;

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_val;//截获了静态变量的指针
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

int *static_val = __cself->static_val; // 从结构体中获取该指针

global_val *=1;//对于全局变量和静态全局变量的访问与转换前完全相同
static_global_val *=2;//

(*static_val) *=3;//使用静态变量的指针对其进行访问
}

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()
{
static int static_val = 3;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
return 0;
}

可以看到对于静态变量,Block 会捕获其指针对其进行访问。这种方法似乎对于自动变量也可以,为什么不截获自动变量的指针呢?因为自动变量再其作用域结束时已经被废弃了,Block调用时再去通过指针访问会出错。

我们在来看看使用“__block存储域类说明符”的例子:

1
2
3
4
5
6
7
int main(){
__block int val = 10;
void (^blk)(void) = ^{
val = 1;
};
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
36
37
38
39
40
41
42
struct __Block_byref_val_0 {//被捕获的__block变量生成的结构体并不在Block用__main_block_impl_0结构体中,这样做是为了能再多个Block中使用同一个__block变量
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;
__Block_byref_val_0 *val; // 结构体指针。__block将自动变量变成了栈上生成的结构体实例

__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; // 获取结构体指针

(val->__forwarding->val) = 1;//通过__forwarding指针访问变量值
}
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()
{
__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, 0x22000000));
return 0;
}

__block 说明符将自动变量变成了栈上生成的结构体实例 __Block_byref_val_0,且这个结构体的指针被 Block 结构体所捕获。被捕获的 __block变量生成的结构体并不在Block用__main_block_impl_0结构体中,这样做是为了能在多个Block中使用同一个 __block变量。
__Block_byref_val_0 结构体实例的成员变量 __forwarding 持有该结构体实例自身的指针,通过这个 __forwarding 指针访问成员变量 val。 如下图所示

访问__block变量

为什么要使用指向结构体自身的 __forwarding 指针来访问其val值呢?为什么 Block 作为返回值时可以超出其变量作用域而存在呢?
其实,Block 所属类有以下几种:

  • __NSConcreteStackBlock –该类的对象分配在栈上
  • __NSConcreteGlobalBlock –该类的对象分配在.data区
  • __NSConcreteMallocBlock –该类的对象分配在堆中

在记述全局变量的地方使用Block变量时,实际的Block对象类为 __NSConcreteGlobalBlock。还有一种情况当 Block 语法的表达式中不使用应截获的自动变量时,Block也是 __NSConcreteGlobalBlock 类对象。

Blocks提供了将 Block 和 __block 变量从栈上复制到堆上的方法来使 Block 可以超出其作用域而存在,这时候在堆上分配的 Block 就是 __NSConcreteMallocBlock 类对象。栈上的 Block 会在以下几种情况时复制到堆:

  • 调用Block的 copy 方法时(除以下3种情况时均推荐自己调用copy方法,防止Block被废弃)
  • Block作为函数返回值返回时
  • 将Block赋值给附有 __strong修饰符 id 类型的类或Block类型成员变量时
  • 在方法名中含有 usingBlock的Cocoa框架方法或者GCD的API中传递Block时

复制__block变量

将 Block 复制到堆之后,栈上的Block 的 __forwarding 指针指向了堆上的Block,从而保证了任何时候都可以正确的访问到val值。

Block截获对象

如果Block捕获的自动变量是 Objective-C 对象,会是什么情况呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
blk_t blk;
{
id array = [NSMutableArray new];
blk = [^(id object){
[array addObject:object];
NSLog(@"array count = %ld",[array count]);
//array = [NSMutableArray array];//不能给array重新赋值,因为捕获的是指向该对象的指针,而不是对象本身
} copy];//调用copy方法将block复制到堆上,才会对截获的array调用retain方法,否则array会被释放
}

blk([NSObject new]);//打印array count = 1
blk([NSObject new]);//打印array count = 2
blk([NSObject new]);//打印array count = 3

可以看到在作用域结束后,array并没有没废弃,这个对象被Block截获,并且在Block复制到堆时对其进行了retain操作,从而持有了这个对象。
我们看看上面代码的 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
36
37
38
39
40
41
42
43
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
id __strong array;//截获的对象

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
id __strong array = __cself->array;
[array addObject:obj]

NSLog(@"array count = %ld",[array count]);
}
//当block复制到堆时调用
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);//相当于调用retain方法
}
//当堆上的block被废弃的时候调用
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);//相当于调用release方法
}

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

blk_t blk;
{
id __strong array = [[NSMutalbleArray alloc] init];
blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000));
blk = [blk copy];
}
(*blk->impl.FuncPtr)(blk,[[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk,[[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk,[[NSObject alloc] init]);

在OC中,C语言结构体不能含有__strong修饰符的变量,因为编译器不知道合适进行C语言结构体的初始化喝废弃操作,不能很好的管理内存,但是运行时库能准确把我 Block 从栈复制到堆以及堆上的Block被废弃的时机,因此Block结构体中可以含有附有 __strong或__weak修饰的变量。

__block修饰对象

当我们想要在block中修改被捕获的对象时,可以用__block修饰,其代码如下:

1
__block id obj = [[NSObject alloc] init];//obj默认是__strong

该代码经过clang转换后的__block变量结构体部分如下:

1
2
3
4
5
6
7
8
9
struct __Block_byref_obj_0 {
void *__isa;
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);//如果__block对象变量从栈复制到堆时,使用_Block_object_assign函数
void (*__Block_byref_id_object_dispose)(void*);//堆上的__block对象变量被废弃时,使用_Block_object_dispose函数
__strong id obj;
};

在Block中使用__strong修饰符的对象类型自动变量的情况下,当Block从栈复制到堆时,使用__Block_object_assign函数,持有Block截获的对象。
需要特别注意的是如果对象是__weak修饰的,则不论是否有__block修饰,Block复制到堆时,均不持有该对象。

Block循环引用

在Block内部使用__strong修饰符的对象类型的自动变量时,当Block从栈复制到堆的时候,该对象就会被Block所持有。如果这个对象还同时持有Block的话,就容易发生循环引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef void(^blk_t)(void);
@interface Person : NSObject
{
blk_t blk;
}

@implementation Person

- (instancetype)init
{
self = [super init];
blk = ^{
NSLog(@"self = %@",self);
};
return self;
}

@end

Person 类对象持有Block类型成员变量blk,init实例方法中Block语法使用了附有__strong修饰符的对象self,并且由于Block语法赋值在了成员变量blk中,因此该栈上生成的Block会复制到堆,从而持有self,导致了循环引用。
可以使用__weak修饰符打破循环:

1
2
3
4
5
6
7
8
9
- (instancetype)init
{
self = [super init];
id __weak temp = self;
blk = ^{
NSLog(@"self = %@",temp);
};
return self;
}

有时候也可以使用__block修饰符来打破循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (instancetype)init
{
self = [super init];
__block id temp = self;
blk_= ^{
NSLog(@"self = %@",temp);
temp = nil;//需要再blk中将temp置空。
};
return self;
}
- (void)execBlock {
blk();//保证blk被执行。
}

使用__block来打破循环的话,需要再block中将对象置为nil,并且保证block执行来打破循环。