Block对象
block这一部分的内容真的是太难啃了,看的我脑壳疼,尽管网上很多人表示这本书的翻译很好,但看完这一章之后我是真的不敢苟同,也许是原版是日本人写的缘故,有些句子真的是九曲十八弯,譬如“所谓‘截获自动变量值‘意味着在执行Block语法时,Block语法表达式所使用的自动变量值被保存到Block的结构体实例中”、“在由Block语法生成的值Block上,可以存有超过其变量作用域的被截获对象的自动变量”。碰上这样的句子我不得不用笔来手动断句,不然只读一两遍是肯定看不明白的,另外Block部分的内容我也只是一知半解,毕竟最重要的是实践,我还没有在实践中感知到Block的妙处,只能写一写粗浅的理解
形态
Block是OC中的一种特殊对象,既然说是对象,我们就来看一看它是如何声明和初始化的。
Block的声明:
int (^blockname)(int);
从左至右,int代表block的返回值、blockname是Block的名称、int是block的参数类型。
可以看到与普通的对象声明不同,block的声明更接近函数声明。
-(int) func:(int)param;
Block的定义:
int (^blockname)(int) = ^int (int num){ return num; };
赋值符号右侧的部分我们称为Block表达式。
注意其中的 ^ 字符,可以称为“脱字符”,在声明部分中,它写在block名称的前面,在Block表达式中它写在最前面,因为Block在OC中会大量应用到,因此这个字符可以使Block更容易识别和查找。
与函数不同的是,在Block的声明中并不需要写出参数的名称,参数名是在Block表达式中写出的。
以上的Block表达式是完整的写法,事实上有两个部分在某些条件下是可以省略的。
- 返回值类型:当省略返回值类型的时候,系统会自动判断Block表达式的返回值类型,唯一需要注意的是当有多个返回值时,必须要保证返回值类型是相同的。
- 参数列表:当Block没有传参时可以省略参数列表。
而声明则不能省略部分,即使为空也要写上void,即:
void (^blockname)(void)
如果省略传参则会得到一个警告
This block declaration is not a prototype
我们可以将Block视为一个普通的变量来使用,例如将它作为函数传参、返回值等等。
那么到现在为止,Block看上去并没有多么“特别”,当然,它还有一些特性。
捕获局部变量
C语言基本数据类型
先来看一小段代码
int val = 10;
void (^blk)(void) = ^{NSlog(@"%d", val);};
val = 20;
blk();
NSlog(@"%d", val);
输出结果:
10
20
可以看到,NSlog(@”%d”, val)这句话在Block内外执行得到了不同的结果,我们猜想,在执行到定义Block表达式的那一行,Block内的val实际上已经在某种程度上与外部的val分离了。
我们再来修改一下这段代码。
int val = 10;
void (^blk)(void) = ^{val = 30; NSlog(@"%d", val);};
val = 20;
blk();
NSlog(@"%d", val);
发现此时xcode编译报错
Variable is not assignable (missing __block type specifier)
我们发现无法对Block外界的局部变量赋值,除非在声明它的时候加上__block说明符。
int __block val = 10;
void (^blk)(void) = ^{val = 30; NSlog(@"%d", val);};
val = 20;
blk();
NSlog(@"%d", val);
输出结果
30
30
可以看到,Block内部的val并非和外部完全分离,当我们在Block中重新赋值时,还是会影响到外部的val。
C语言数组类型
那C语言中的数组可以捕获吗,答案是不行,编译时会提示
Cannot refer to declaration with an array type inside block
我们只能用指针代替数组。
OC对象
那么对象呢?
NSString *str = @"2";
NSMutableArray *arr = [[NSMutableArray alloc]init];
a[0] = 1;
void (^blk)(void) = ^{
[arr addObject:str];
for (NSString *str1 in arr) {
NSLog(@"blk_%@", str1);
}
};
str = @"1";
[arr addObject:str];
blk();
for (NSString *str1 in arr) {
NSLog(@"%@", str1);
}
输出结果
blk_1
blk_2
1
2
可以看到,我们在Block中不直接改变数组的指向,只是使用addobject是可以的。
但如果要改变arr数组的指向,那么就必须在arr前加上__block说明符。
被捕获对象的作用域
void (^blk)(void);
if(true)
{
int val = 1;
blk = ^{
NSLog(@"%d", val);
};
}
blk();
输出结果
1
可以看到在上述片段中, blk的生存周期要大于val, 在if语句结束的时候val变量就已经被回收了,但blk仍然保存了它。
总结一下上述现象:
-
block中不可以使用c语言的数组类型局部变量。
-
block中的局部变量值在定义Block表达式的那一刻就已经确定,之后在内部无法重新对它的赋值,在外部的修改也不影响内部的赋值。
-
要在Block内重新对捕获的局部变量赋值,必须要在它们声明时加上__block说明符。
-
Block捕获的局部变量,即使在外部的生命周期已经结束,在内部仍然得以保存。
以上只是现象,要想深入了解Block,必须要知道Block的实质,由于实质部分涉及大量C语言代码,我只能在这里做简单的介绍,如果有时间日后会补充。
实质简述
Block如何捕获对象
我们知道Block是一种对象,OC的对象在运行时对应的实际上是c语言中的一系列结构体。
在Block中用到外部的局部变量时,Block的结构体成员中会追加所有用到的局部变量,并在Block的方法体内定义同名的局部变量,将结构体中捕获的局部变量成员的值赋给内部的局部变量,所以事实上方法体内使用的局部变量是Block自己在内部定义的变量。
这样一来我们就可以解决上面的三个问题
- block中不可以使用c语言的数组类型局部变量。
- block中的局部变量值在定义Block表达式的那一刻就已经确定,之后在内部无法重新对它的赋值,在外部的修改也不影响内部的赋值。
- Block捕获的局部变量,即使在外部的生命周期已经结束,在内部仍然保存。
由于捕获局部变量包含了一个从结构体成员到局部变量的赋值过程,因此如果Block要捕获c语言数组时,它要这么做
int a[10] = __cself->a;
其中__cself代表Block的结构体实例,然而这种操作在c语言规范中是不允许的,因此编译时系统就会告诉你不要在Block中使用c数组。
在一般情况下,Block实际上在内部复制了它捕获的变量,因此内外的变量看似相同实际上已经没有关联,互不影响,因此Block也不允许在内部修改捕获到的变量,因为这种操作没有意义。
那为什么加上__block后的局部变量可以在Block中修改呢?
__block说明符的作用
当Block捕获到__block修饰的局部变量时,结构体成员中就不再是简单的追加这个变量,而是追加了一个结构体,这个追加的结构体中保存了被捕获的局部变量的值,以及它的地址,因此Block可以通过这个地址访问到原本的局部变量,当我们在Block内部修改被捕获的局部变量值时,Block还会自动通过地址去修改外部的局部变量值。
Block的存储域
我们在内存管理的第一篇文中提到过,局部定义的OC对象都是存储在堆上的,然而Block作为一种特殊的对象,它在默认情况下是存储在栈上的。
我们知道栈上的变量在超过作用域后系统会自动回收它们。但在
void (^blk)(void);
if(true)
{
int val = 1;
blk = ^{
NSLog(@"%d", val);
};
}
blk();
这段代码中,我们看到Block超过了作用域但仍然存在,原因是此时Block已经被复制到了堆上,注意是复制,而不是移动,事实上,在以下四种情况中,Block都会从栈复制到堆上。
- 调用Block的copy实例方法时
- Block作为函数返回值返回时
- 将Block赋值给附有__strong修饰符id类型的类或者Block类型成员变量时
- 在方法名中含有usingBlock的Cocoa框架方法或Grand Central Dispatch 的API中传递Block时
由于我们在声明blk时,系统默认加上了__strong修饰符,因此Block被复制到了堆上。
实际上因为这条性质,在ARC下使用copy修饰Block变量以及用copy复制Block到堆上都是不必要的,这些是MRC时代的遗留,我们不需要再关心。