重学 Block & Closure 系列之 Block 篇 | Soledad 

JerryXia 发表于 , 阅读 (0)

重学 Block & Closure 系列是关于 Objective-C 中的 Block(块) 和 Swift 中的 Closure(闭包)。

我总是想弄清楚一些事情并把它们记录下来,那样会帮助我更好的掌控它们,自己每天也会更加安心。

本文即是关于 Block 的个人小结。另一篇关于 Closure 的在这里

基本用法

Block 最早是和 GCD 一起被苹果公司引入的,最初主要是用来解决多线程编程的问题。Block 十分有用,借由它,开发者可以将代码像对象一样传递,令其在不同环境下运行。还有一个关键点,在定义 Block 的范围内,它可以访问到其中的全部变量

Block 的声明格式如下:
return_type (^block_name)(parameters)

格式看上去有点奇怪。因为正常的声明变量都是 NSString *str; ,变量类型在左,变量名在右。而 Block 的变量名在中间,这一点值得注意一下。

来看一段例子:

1
2
3
void (^someBlock)() = ^{
};

这段代码中定义了一个名为 someBlock 的 Block。它不接受参数,返回类型为 void。

例子二:

1
2
3
int (^addBlock)(int a, int b) = ^(int a, int b) {
return a + b;
};

定义好之后,就可以像函数那样使用了:int add = addBlock(3, 5)

Block 的威力还不止于此,在它声明的范围里,所有变量都可以为其所捕获。例如:

1
2
3
4
int additional = 5;
int (^addBlock)(int a, int b) = ^(int a, int b) {
return a + b + additional;
};

默认情况下,被 Block 所捕获的变量,是不可以在块内被修改的。如果想要修改,那么必须在声明变量的前面加上 __block 关键字。(实例变量除外,不加 __block 同样可以被修改,具体见下段)
另外,如果被 Block 捕获的变量是对象类型,那么会自动保留它。系统在释放这个块的时候,也会将其一并释放。

特别的,如果将块定义在 Objective-C 类的实例方法中,那么除了可以访问类的所有实例变量之外,还可以使用 self 变量。块总能修改实例变量,所以在声明时不需要加 __block。看个例子:

1
2
3
4
5
-(void)instanceMethod {
void(^someBlock)() = ^{
_anInstanceVarible = @"Something";
}
}

分析一下,如果这时某个实例正在执行该实例方法,那么 self 变量就指向此实例,从而引用了 someBlock。另外,_anInstanceVarible 实际上是 self -> _anInstanceVarible。因此,someBlock 捕获了 self。因此会出现循环引用的情形。

内部结构

58bbfd797dd12.png
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa; // 所有对象都有的 isa 指针
int flags;
int reserved;
void (*invoke)(void *, ...); // 指向具体的 block 实现的函数调用地址
struct Block_descriptor *descriptor; // 附件描述信息
/* Imported variables. */ // Block 捕获来的变量
};

值得一提的有,对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的,如下图所示:

58bbfd7aa5b49.jpg

对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的,如下图所示:

58bbfd7b9ecbc.jpg

Block 的类型

栈块

定义 Block 的时候,其所占的内存区域是分配在栈中的。也就是说,Block 只在定义它的那个范围内有效。例如,下面的代码就很危险:

1
2
3
4
5
6
7
8
9
10
11
void (^block)();
if (/* some condition */) {
block = ^{
NSLog(@"Block A");
}
} else {
block = ^{
NSLog(@"Block B");
}
}
block();

定义在 if或else 语句中的两个块都是分配在栈内存中的。等离开了相应的范围(if或else语句)之后,编译器有可能把分配的内存复写掉(栈内存本身也会自动回收)。

堆块

解决的方法是,可以给块对象发送 copy 消息,将块从栈上复制到堆上。拷贝后的块可以在超出它的范围之外安全使用。而且,一旦复制到堆上,块就成了带引用计数的对象了。之后的复制操作都不会真的复制,只是会增加引用计数。代码:

1
2
3
4
5
6
7
8
9
10
11
void (^block)();
if (/* some condition */) {
block = [^{
NSLog(@"Block A");
} copy];
} else {
block = [^{
NSLog(@"Block B");
} copy];
}
block();

全局块

还有一种 Block ,不会捕捉任何状态(比如外围的变量等),运行时也无须有状态来参与。块所使用的内存区域,在编译时就已经完全确定了,因此,全局块声明在全局内存里,而不需要每次用到的时候在栈中创建。另外,对其拷贝的操作是个空操作,因为全局块绝不可能为系统所回收。这种块实际上相当于单例。例:

1
2
3
void (^block)() = ^{
NSLog(@"Block A");
};

由于运行该块的全部信息都能在编译期确定,所以可把它做成全局块。这完全是一种编译器级别上的优化。

另外,有关 Block 类型的讨论,建议看这篇实践文章,写的很仔细。

为常用的块类型创建 typedef

一般声明与定义 Block 的做法是这样:

1
2
3
int (^blockName)(BOOL flag, int value) = ^(BOOL flag, int value) {
return someInt;
};

显然,这种做法很麻烦,并且由于 blockName 在中间,也不易读。比方说在这里,如果我们要表示接受 BOOL 以及 int 参数并返回 int 值的块,可以这样:

1
typedef int(^SomeBlock)(BOOL flag, int value);

这样相当于向系统中新增了一个名为 SomeBlock 的类型。以后,创建变量的方式改用这样就行了:

1
2
3
SomeBlock blockName = ^(BOOL flag, int value){
// Implementation
};

拿平时最常用的 completionHandler 来举例。如果不用 typedef,那么 completionHandler 会是这个样子:

1
- (void)startWithCompletionHandler:(void(^)(NSData *data, NSError *error))completion;

类似的,我们可以使用 typedef 改写:

1
2
typedef void(^SomeCompletionHandler)(NSData *data, NSError *error);
- (void)startWithCompletionHandler:(SomeCompletionHandler)completion;

参考文献

关注我


本文版权所有,如需转载,请告知原作者并注明出处