block的使用和探究

摘要

这篇文章是我自己对 block 的使用和理解的总结吧,算是笔记。

什么是 Block?

       介绍 block 之前,先讲一下什么是闭包吧。在wikipedia 上,闭包 的定义是:

In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.

翻译一下就是:

       闭包是一个函数(或指向函数的指针),再加上该函数执行的外部的上下文变量(有时候也称作自由变量)。

而 block 实际上就是 Objective-C 语言对于闭包的实现。

我们要明确两点:

  • Block可以访问Block函数以及语法作用域以内的外部变量。也就是说:一个函数里定义了个block,这个block可以访问该函数的内部变量(当然还包括静态,全局变量)-即block可以使用和本身定义范围相同的变量。
  • Block其实是特殊的Objective-C对象,可以使用copy,release等来管理内存,但和一般的NSObject的管理方式有些不同,下面会稍加说明。

Block 的语法

Block很像函数指针,这从Block的语法上就可以看出。

Block的原型:

返回值 (^名称)(参数列表)

Block的定义

^ 返回值类型 (参数列表) { 表达式 }

其中返回值类型和参数列表都可以省略,最简单的Block就是:

1
^{ ; };

一般的定义就是:

返回值 (^名称)(参数列表) = ^(参数列表){代码段};

为了方便通常使用typedef定义:

1
typedef void (^block) (void);

Block 根据储存的区域分为三种类型

       每一种类型的 block,我都会举一下个例子。为了跟好的理解 block,下面的代码都是不使用 ARC 的(即,使用 MRC)

_NSConcreteGlobalBlock-存储在全局数据区域(和全局变量一样)

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
#import <Foundation/Foundation.h>

typedef void(^myBlock)(void);

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

static int a;

myBlock block1 = ^{
NSLog(@"Hello Zen3~");
};

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

myBlock block2 = ^{
NSLog(@"Access staticValue %d",a);
};

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

myBlock block3 = ^{
int a;
NSLog(@"Access localValue %d",a);
};

NSLog(@"block3:%@",block3);
}
return 0;
}

输出结果:

解释:

       当一个 block 没有访问外部变量或者只访问了全局变量(或block内变量)时,block 的类型是 _NSConcreteGlobalBlock。虽然 block1 block2 block3 都是 储存在全局的数据区,但是他们在结构上还是有些不一样的。

       clang 提供一个命令,可以将 Objetive-C 的源码改写成 c 语言的,我们利用 clang 查看 block 具体的源码实现方式。在命令行中输入:

1
clang -rewrite-objc <文件名.m>

可以在相同的目录下面看到对应的 cpp 文件。
main函数:

       main函数中对block的创建和调用,可以看出执行block就是调用一个以block自身作为参数的函数,这个函数对应着block的执行体。

block1:

       block1由结构体 __main_block_impl_0 __main_block_func_0函数 和 __main_block_desc_0结构体 组成。

结构体 __main_block_impl_0,可以看出是根据所在函数(main函数)以及出现序列(第0个)进行命名的。 __main_block_impl_0中包含了两个成员变量和一个构造函数,成员变量分别是 __block_impl结构体和描述信息Desc,之后在构造函数中初始化block的类型信息和函数指针等信息。

__main_block_func_0函数,即block对应的函数体。该函数接受一个__cself参数,即对应的block自身。

再下面是__main_block_desc_0结构体,其中比较有价值的信息是block大小。

block2:

       而 block2 相比于我后面写的 block4 ,差别主要在于main函数里传递的是a的地址(&a),以及 __main_block_impl_1结构体中成员a变成指针类型(int*),实际是进行了指针的拷贝,将结构体传入 __main_block_func_1 函数中也是用一个临时变量 (int*)来储存 a 的内存地址。

       然后在执行block时,通过指针修改值。

block3:

       block3 中有 int 变量 a,因为a 存在于 block 的内部,所以不用像 block 外部参数是一样被拷贝到结构体 __main_block_impl_2 之中,只是将 block 内的临时变量 a 存在 __main_block_func_0 之中。

_NSConcreteStackBlock-存储在栈上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#import <Foundation/Foundation.h>

typedef void(^myBlock)(void);

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

int a;

myBlock block4 = ^{
NSLog(@"Access a_Value %d",a);
};

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

}
return 0;
}

输出结果:

解释:

       简单一句话来说就是:当一个 block 访问了外部变量时,那它就是_NSConcreteStackBlock 类型的block。

block4源码实现:

       可以结合 block2 和 block3 进行理解,block4 将外部变量拷贝进 结构体__main_block_impl_0,而在 __main_block_func_0 中用临时变量 a 来存储由结构体传过来外部变量 a。

       那如果不只是访问外部变量,还要对外部变量进行修改呢?

       尝试一下后会发现是不可行,objective-C 会友善的提醒你:

Variable is not assignable (missing __block type specifier)

       意思是让我们加上 __block (注意是两个相连的下滑线) 关键字,在 block 内相对 block 外的变量做修改(单纯访问不用),必须对变量加上 __block 关键字。

       至于 block 不能修改局部变量的原因,我们可以简单的进行一下推理,a 和 __main_block_func_0 不在一个作用域,调用过程中也只是进行的值得传递。修改 a 的值之后并不会对实际存在内存上的 a 的值有任何影响。那么为什么不用指针传递的方式,获取 a 的内存地址,来达到修改 a 的目的呢?在我上面例子里(block4),确实可以这么做,因为 main 函数在我们访问 a 地址的时候还没有完全展开,也就是说 a 的内存地址还是存在函数栈上面的。但是,block 更多情况下是用于作为参数的传递以供回调执行。当你的 block 要被执行时,你需要去访问 a 的内存地址的时候,定义所在的函数栈早就已经展开完毕销毁了,所以对于 a 的内存地址,我们是无从得知的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import <Foundation/Foundation.h>

typedef void(^myBlock)(void);

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

__block int a;

myBlock block5 = ^{
a++;
NSLog(@"Access a_Value %d",a);
};

block5();

NSLog(@"block5:%@",block5);
}
return 0;
}

输出结果:

block5 的源码:

       带 __block 关键字变量的 block 中间的代码会比较复杂,分析起来也没有前面那么简单,我说一下自己的看法,如果有不对的地方,还望指正。

       首先,__block 将我们所定义的 (int)a 封装成一个__Block_byref_a_0 结构体。在这个结构体有三个比较关键的变量:

  • void *__isa;

  • __Block_byref_a_0 *__forwarding;

    定义一个指向与自己相同类型,即 __Block_byref_a_0 类型的指针。

  • int a;
    储存目标变量 a

       下面的 __main_block_impl0 函数,它的成员变量 a 变成了\_Block_byref_a_0 *类型。

       通过这样看起来有点复杂的改变,我们可以修改变量i的值。但是问题同样存在:__Block_byref_a_0 类型变量 a 仍然处于栈上,当block被回调执行时,变量i所在的栈已经被展开完毕,怎么办?

       在这种关键时刻,__main_block_desc_0 buibiubiu闪亮登场:

       此时,main_block_desc_0多了两个成员函数:copy和dispose,分别指向main_block_copy_0和__main_block_dispose_0。

       当block从栈上被copy到堆上时,会调用main_block_copy_0将block类型的成员变量i从栈上复制到堆上;而当block被释放时,相应地会调用main_block_dispose_0来释放block类型的成员变量i。

       一会在栈上,一会在堆上,那如果栈上和堆上同时对该变量进行操作,怎么办?

       这时候,forwarding的作用就体现出来了:当一个block变量从栈上被复制到堆上时,栈上的那个Block_byref_i_0结构体中的forwarding指针也会指向堆上的结构。

       所以,以下这一段代码的结果也就不难推测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#import <Foundation/Foundation.h>

typedef void(^myBlock)(void);

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

int a;
__block int b;

myBlock block7 = ^{
b++;
NSLog(@"Access a_Value %d",a);
NSLog(@"Access b_Value %d",b);
};

a++;
b++;

block7();

}
return 0;
}

输出结果:

_NSConcreteMallocBlock-存储在堆上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#import <Foundation/Foundation.h>

typedef void(^myBlock)(void);

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

int a;

myBlock block6 = [^{
NSLog(@"Access a_Value %d",a);
} copy];

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

}
return 0;
}

输出结果:

解释

       NSConcreteMallocBlock其实就是一个_NSConcreteStackBlock被copy时,将生成 _NSConcreteMallocBlock(block没有retain)。

       NSConcreteMallocBlock 类型的 block 通常不会在源码中直接出现, 因为他的结构体和函数等都与被 copy 的 _NSConcreteStackBlock 一样,他们主要的区别在于一个存储在栈上,一个存储在堆上。所以我这里没有做单独的源码分析。

       等等!!难道你们没有和我一开始一样感到十分震惊吗?!!

       在经过copy之后,对象的类型从 NSStackBlock 变为了NSMallocBlock !!

       在Objective-C的设计中,copy 一下,TM的类型都变了!不过这也说明了block 是一种特殊的对象。

block 需要 copy 的原因其实也很简单:

       配置在栈上的Block也就是NSConcreteStackBlock类型的Block,如果其所属的变量作用域结束该Block就会被释放。这个时候如果继续使用该Block,就应该使用copy方法,将NSConcreteStackBlock拷贝为_NSConcreteMallocBlock。

       NSMallocBlock对象再次copy,不会再产生新的对象而是对原有对象进行retain,也就是说当一个 block 为 _NSConcreteMallocBlock 它的内存管理也就相当于我们正常的对象了。即,当_NSConcreteMallocBlock的引用计数变为0,该_NSConcreteMallocBlock就会被释放。

下面是一张总结的表:

block 在 MRC 和 ARC 环境下的区别

关于 block copy

如果是非ARC环境,需要显式的执行copy或者antorelease方法

       而当ARC有效的时候,实际上大部分情况下编译器已经为我们做好了,自动的将Block从栈上复制到堆上。包括以下几个情况:

1,Block作为返回值时

类似在非ARC的时候,对返回值Block执行[[returnedBlock copy] autorelease];

2,方法的参数中传递Block时

3,Cocoa框架中方法名中还有useringBlock等时

4,GCD相关的一系列API传递Block时。

比如:[mutableAarry addObject:stackBlock];这段代码在非ARC环境下肯定有问题,而在ARC环境下方法参数中传递NSConcreteStackBlock会自动执行copy。

下面是在在网上看到的一些代码,复制过来,方便学习:

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
//Test1
void exampleB_addBlockToArray(NSMutableArray *array) {
char b = 'B';
[array addObject:^{
printf("%c\n", b);
}];
}
void exampleB() {
NSMutableArray *array = [NSMutableArray array];
exampleB_addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block();
}

//Test2
void exampleC_addBlockToArray(NSMutableArray *array) {
[array addObject:^{
printf("C\n");
}];
}
void exampleC() {
NSMutableArray *array = [NSMutableArray array];
exampleC_addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block();
}

//Test3
typedef void (^dBlock)();
dBlock exampleD_getBlock() {
char d = 'D';
return ^{
printf("%c\n", d);
};
}
void exampleD() {
exampleD_getBlock()();
}

Test1:
exampleB_addBlockToArray添加的Block为NSConcreteStackBlock,在非ARC环境下,执行该Block时,栈上的数据已经被清除了,错误。而在ARC环境下,作为参数的Block会自动copy,不会出现问题;

Test2:
exampleC_addBlockToArray添加的Block为_NSConcreteGlobalBlock,存储在程序的数据区,相当于一个不使用外部环境的函数,没有用到栈上的数据,所有无论是ARC还是非ARC都能正常执行。

Test3
ARC中作为返回值的NSConcreteStackBlock会被执行copy操作。所有ARC运行正常,非ARC错误。

关于 block 的类型

网上有些文章会有提出说:

在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。

       其实仔细思考一下后,发现并非如此。这是因为本身我们常常将 block 赋值给变量,而ARC下默认的赋值操作是 strong 的,到了block身上自然就成了copy,所以常常打印出来的block就是 NSConcreteMallocBlock 了。在ARC模式下,block 三种类型也都是存在的。

关于 __block 的问题

       上面说过,__block 是将对象封装为一个 Block_byref_a_0 结构来进行管理的,我们从上面的 MRC 代码也可以看出,block 根本不会对指针所指向的结构执行copy操作,而只是把指针进行的复制。(这里还要区别一下普通对象)。
不管,来一段代码:

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
#import <Foundation/Foundation.h>

typedef void(^myBlock)(void);

int main(int argc, const char * argv[]) {
@autoreleasepool {
//MRC 环境下

__block NSObject *a = [[NSObject alloc] init] ;
NSObject *b = [[NSObject alloc] init];

NSLog(@"最初的自动引用计数:a:%lu b:%lu",(unsignedlong)a.retainCount,(unsigned long)b.retainCount);

myBlock block8 = Block_copy(^{
NSLog(@"copy后自动引用计数:a:%lu b:%lu",(unsignedlong)a.retainCount,(unsignedlong)b.retainCount);
});

block8();

NSLog(@"block8的类型:%@",block7);

Block_release(block8);

NSLog(@"release后自动引用计数:a:%lu b:%lu",(unsigned long)a.retainCount,(unsigned long)b.retainCount);

}
return 0;
}

输出结果:

       而在 ARC 底下,对这个结构体或者说对象的管理机制就是完全遵从 ARC 的机制了,也就是自动引用计数。所以在 ARC 底下要特别注意一下, 避免循环引用。关于这个问题以及解决方法,网上已经有很多文章了,我就不再累述,推荐给大家一篇博文:iOS容易造成循环引用的三种场景,就在你我身边!

最后

不知不觉写了这么长了,关于block 的了解基本就是我以上写的着一些了。如果有不对的地方还望指正。最后,附上女神照片养下眼,码字码到眼睛要瞎了。