(一)Block基础回顾
1.Block定义
带有局部变量的匿名函数,差不多就与C语言中的函数指针类似,可以当做参数传来传去,而且可以没有名字。
2.Block语法完整的形式的Block语法如下
参数类型(^函数名)(形参) = ^(实参){ /代码块/};
并且与一般的C语言函数定义相比,仅有两点不同:
(1)没有函数名
(2)带有"^"符号所以根据前面的语法格式可以写出如下例子:
^int(int count) {return count+1};
当然,也可以有很多的省略格式,省略返回值如下
^(int count) {return count+1};
省略返回值类型时,如果表达式中有return语句时,block语句的的返回值类型就使用return返回的类型;如果return中没有返回类型,就使用void类型。再省略参数列表,参数列表和返回值都省略是最简洁的方式,同时将参数和返回值省略如下:
^{printf("good!");}
3.Block类型变量在Block语法下,一旦使用了Block语法就相当于生成了可赋值给Block类型变量的值,"Block"既指源代码中的Block语法,也指由Block语法所生成的值即:
int (^blk)(int) = ^(int count){return count +1};
int (^blk1)(int) = blk;
int (^blk2)(int);
blk2 = blk1;
从上面看出,Block确实代表了一种语法,但在这里,对于blk,他也是一个Block类型变量的值。但是,当Block作为函数的参数或者返回 值的时候若传来传去,写法上难免有点复杂,毕竟都是那么长一串儿,此时,就可以像C语言那样使用typedef了:
typdef int(^blk_t)(int);
这时,blk_t就变成了一种Block类型了,例如:
typef int(^blk_t)(int);
blk_t bk = ^(int count){return count+1};
//很明显省略了返回值
4.截获的自动变量(自动变量==局部变量)
通过前面的知识,我们已经大部分理解了Block了,这里引入截获的自动变量,什么是截获的局部变量?先看一段代码:
int main()
{
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{printf(fmt, val);};
val = 2;
fmt = "These values were changed. val = %d\n";
blk();
return 0;
}
执行结果:val = 10
解释:在该源代码中,Block语法的表达式使用的是它之前声明的自动变量fmt 和val.Block语法中,Block表达式截获的自动变量,即保存该自动变量瞬间的值。因为Block表达式保存了自动变量的值,所以在执行Block语法之后,即使改变Block中的自动变量的值也不会影响Block执行时自动变量的值。这就是所谓的截获
5._ _block修饰符咱们来尝试着,在Block中修改自动变量的值:
int val = 0;
void (^blk)(void) = ^{val = 1;};
blk();
printf("val = %d\n", val);
```
执行结果:`error: variable is not assignable (missing __block type specifier) void (^blk)(void) = ^{val = 1;};~~~ ^`
很显然,光这样的话是不允许在Block内部修改外面的自动变量的值的。如果强势要改呢,所以这会儿就该 __block出场了:若想在Block语法的表达式中将赋值给在Block语法外声明的自动变量,需要在该自动变量上加上 _block修饰符,如下:
```objectivec
__block int val = 0;
void (^blk)(void) = ^{val = 1;};
blk();
printf("val is %d",val);
执行结果:val is 1
所以,使用 _block修饰的变量,就可以在Block语法内部进行修改了,该变量称为 _block变量。但这里还有另一种情况,见如下代码:
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{id obj = [[NSObject alloc] init];
[array addObject:obj]; };
这会出错吗?其实是不会的,咱们在这里是没有向arry赋值,向他赋值才会产生编译错误,在这里,咱们截获到了NSMutableArray类对象的一个结构体指针(后面会讲),咱们没有对它赋值,只是使用而已,所以不会出错。
(二)Block存储域
( _NSConcreteStackBlock,_NSConcreteGlobalBlock,_NSConcreteMallocBlock)
通过前面的学习,了解到Block转换为Block的结构体类型的自动变量,__block修饰符修饰的变量转换为block变量的结构体类型的自动变量,所谓结构体类型的自动变量,即栈上生成的该结构体的实例变量。:
根据咱们之前提到的,其实Block也是一种对象,并且Block的类是_NSConcreteStackBlock,和他类似的还有两个如:
- _NSConcreteMallocBlock
- _NSConcreteGlobalBlock三个不同的类名称决定了三个Block类生成的Block对象存在内存中的位置:
到目前为止,出现的Block例子都是_NSConcreteStackBlock类,所以都是设置在栈上,但实际上并非是这样,在记述全局变量的地方使用Block语法时生成的Block为_NSConcreteGlobalBlock对于Block对象分配在数据区的情况,略过分析过程,直接总结:
- 当把Block声明为全局变量的时候,Block分配在数据区:
void (^blk)(void) = ^{printf("Global Block\n");};
int main() {}
- Block语法表达式中不使用截获的自动变量的时候:
typedef int (^blk_t)(int);
for (int rate = 0; rate < 10; ++rate) {
blk_t blk = ^(int count){return count;};
}
以上两种情况,Block分配在数据区。最后一个问题,什么时候Block会分配在堆上呢?此时可以引出之前说的一个问题,“Block可以超出变量作用域而存在”,换句话说就是,Block倘若作为一个局部变量存在,结果他居然可以在超出作用域之后不被废弃,同样的,由于block修饰的变量也是放在栈上的,如果其变量作用域结束,那么block修饰符修饰的变量也应该结束。解决方案如下:
将Block和__block修饰的变量从栈上复制到堆上来解决,将配置在栈上的Block复制到堆上,这样即使Block语法记述的变量作用域结束时,堆上的Block还可以继续存在
复制到堆上之后,将Block内部的isa成员变量进行改变:
impl.isa = &_NSConcreteMallocBlock;
当ARC有效时,大多数情况下编译器会进行恰当地进行判断,自动生成将栈上复制到堆上的代码,并且最后复制到堆上的Block会自动的加入到autoRealeasePool中,编译器不能进行判断的情况便是:
向方法或函数的参数中传递Block时但是在向方法或函数的参数中传递Block时也有不需要手动复制的情况如下:
- Cocoa框架的方法且方法名中含有usingBlock等时
- GCD中的API
举个栗子:在使用NSArray类的enumeratObjectsUsingBlock实例方法以及dispatch_async函数时,不用手动复制,相反的,在NSArray类的initWithObjects实例方法上传递时需要手动复制,看代码:
- (id) getBlockArray {
int val = 10;
return [[NSArray alloc] initWithObjects: ^{NSLog(@"blk0:%d", val);}, ^{NSLog(@"blk1:%d", val);}, nil];
}接下来,调用:
id obj = getBlockArray();
typedef void (^blk_t)(void);
blk_t blk = (blk_t)[obj objectAtIndex:0];
```
blk(); 结果就是Block在执行时发生异常,应用程序强制结束,这是由于在getBlockArray函数执行结束时,栈上的Block被废弃的缘故。而此时编译器恰好又不能判断是否需要复制。 注:但将Block从栈上复制到堆上是相当消耗CPU的,当Block设置在栈上也能够使用时,将Block从栈上复制到堆上只是在浪费CPU资源,能少复制,尽量少复制。
将以上代码修改一下即可运行:
```objectivec
- (id) getBlockArray {
int val = 10;
return [[NSArray alloc] initWithObjects: [^{NSLog(@"blk0:%d", val);} copy], [^{NSLog(@"blk1:%d", val);} copy], nil];
}
小结:
(三)block变量存储域(Block移动对block变量的影响)
使用block变量的Block从栈复制到堆上时,block修饰的变量也会受到影响。
- 1.多个Block使用一个block变量时,因为会将所有的Block配置在栈上,所以block变量也会配置在栈上,其中任何一个Block从栈复制到堆时,block变量也会一并从栈复制到堆并被该Block持有,当剩下的Block从栈复制到堆时,被复制的Block会依次持有block变量,并增加__block变量的引用计数。
- 2.在这里,读者可以采用Objective-C的引用计数的方式来考虑。使用block变量的Block持有block变量,如果Block被废弃,它所持有的block变量也就被释放在这里,回到之前讲到的“block变量使用结构体成员变量forwarding的原因”,不管block变量配置在栈上还是在堆上,都能够正确的访问该变量(通过指针),通过Block的复制,block变量也从栈复制到堆,此时可同时访问栈上的block变量和堆上的block变量,下面解释一下:看代码:
__block int val = 0;
void (^blk)(void) = [^{++val;} copy];
++val;
blk();
NSLog(@"%d", val);
结果是:2
^{++val;}和++val;都可以转化为++(val.__forwarding->val);
在变换Block语法的函数中,该变量val为复制到堆上的block变量结构体实例,而另外一个(++val)与Block无关的变量val,为复制前栈上的block变量结构体实例。但是栈上的block变量结构体实例在block变量从栈复制到堆上时,会将成员变量forwarding指针替换为复制目标堆上的block变量用结构体实例的地址
下面总结栈上的Block复制到堆的情况:
- 调用Block的copy实例方法时
- 将Block作为函数返回值时
- 将Block赋值给附有__strong修饰符id类型的类或者Block类型成员变量时
- 在方法名中含有usingBlock的Cocoa框架方法或者GCD的API中传递Block时
在调用Block的copy方法时,如果Block配置在栈上,那么该Block会从栈上赋值到堆;将Block作为函数返回值时、将Block赋值给附有__strong修饰符id类型的类或者Block类型成员变量时,编译器将自动地将对象的Block作为参数并调用_Block_copy函数,这与调用Block的copy实例方法的效果相同;在方法名中含有usingBlock的Cocoa框架方法或者GCD的API中传递Block时,在该方法或函数内部对传递过来的Block调用Block的copy实例方法或者_Block_copy函数。