当前多线程编程的核心就是"块"(block)与"大中枢派发"(Grand Central Dispatch, GCD)。这虽然是两种不同的技术,但它们是一并引入的。在定义"块"的范围内,它可以访问到其中的全部变量。GCD是一种与块有关的技术,它提供了对线程的抽象,而这种抽象则基于"派发队列"(dispatch queue)。开发者可将块排入队列中,由GCD负责处理所有调度事宜。GCD会根据系统资源情况,适时地创建、复用、摧毁后台线程任务,比如编写"只执行一次的线程安全代码",或者根据可用的系统资源来并发执行多个操作。
块的基础知识
块与函数类似,只不过是直接定义在另一个函数里的,和定义它的那个函数共享同一个范围内的东西。块用"^"符号来表示,后面跟着一对花括号,括号里面是块的实现代码。
简单的块:
^{
//Block implementation here
}
块其实就是个值,而且自有其相关类型。与int、float或Objective-C对象一样,也可以把块赋给变量,然后像使用其他变量那样使用它。块类型的语法与函数指针近似。
没有参数 也没有返回值:
void (^someBlock)() = ^{
//Block implementation here
};
定义了一个名为someBlock的变量。
块类型的语法结构如下:
return_type(^block_name)(parameters)
返回int值,并且接受两个int做参数:
int (^addBlock)(int a, int b) = ^(int a, int b){
return a + b;
};
使用:
int add = addBlock(2, 5);
块的强大之处是: 在声明它的范围里,所有变量都可以为其所捕获。这也就是说,那个范围里的全部变量,在块里依然可用。
int additional = 5;
int (^addBlock)(int a, int b) = ^(int a, int b){
return a + b + additional;
};
int add = addBlock(2, 5);
默认情况下,为块所捕获的变量,是不可以在块里修改的。不过声明变量的时候可以加上__block修饰符,这样就可以在块内修改了。
如果块所捕获的变量是对象类型,那么就会自动保留它。系统在释放这个块的时候,也会将其一并释放。这就引出了一个与块有关的重要问题,块本身可视为对象。实际上,在其他Objective-C对象所能响应的选择子中,有很多是块也可以响应的。而最重要之处则在于,块本身也和其他对象一样,有引用计数。当最后一个指向块的引用移走之后,块就回收了。回收时也会释放块所捕获的变量,以便平衡捕获时所执行的保留操作。
如果将块定义在Objective-C类的实例方法中,那么除了可以访问类的所有实例变量之外,还可以使用self变量。块总能修改实例变量,所以在声明时无须加__block。不过,如果通过读取或写入操作捕获了实例变量,那么也会自动把self变量一并捕获了,因为实例变量是与self所指代的实例关联在一起的。例如,下面这个块声明在EOCClass类的方法中:
@interface EOCClass
- (void)anInstanceMethod
{
//...
void (^someBlock)() = ^ {
_anInstanceVariable = @"Something";
NSLog(@"_anInstanceVariable = %@", _anInstanceVariable);
};
//...
}
@end
如果某个EOCClass实例正在执行anInstanceMethod方法,那么self变量就指向此实例。由于块里没有明确使用self变量,所以很容易就会忘记self变量其实也为块所捕获了。直接访问实例变量和通过self来访问是等效的:
self -> _anInstanceVariable = @"Something";
一定要记住: self也是个对象,因而块在捕获它时也会将其保留。如果self所指代的那个对象同时也保留了块,那么这种情况通常就会导致"保留环"。
全局块、栈块及堆块
定义块的时候,其所占的内存区域是分配在栈中的。这就是说,块只在定义它的那个范围内有效。下面这段代码就有危险:
void (^block)();
if (/* some condition */) {
block = ^{
NSLog(@"Block A");
};
} else {
block = ^{
NSLog(@"Block B");
};
}
block();
定义在if及else语句中的两个块都分配在栈内存中。编译器会给每个块分配好栈内存,然而等离开了相应的范围之后,编译器很有可能把分配给块的内存覆写掉。于是,这两个块只能保证在对应的if或else语句范围内有效。这样写出来的代码可以编译,但是运行起来时而正确,时而错误。若编译器未覆写待执行的块,则程序照常运行,若覆写,则程序崩溃。
为解决此问题,可给块对象发送copy消息以拷贝之。这样的话,就可以把块从栈复制到堆了。拷贝后的块,可以在定义它的那个范围之外使用。而且,一旦复制到堆上,块就成了带引用计数的对象了。后续的复制操作都不会执行复制,只是递增块对象的引用计数。如果不再使用这个块,那就应该将其释放,在ARC环境下会自动释放,而手动管理引用计数时则需要自己来调用release方法。当引用计数将为0后,"分配在栈上的块"则无须明确释放,因为栈内存本来就会自动回收,刚才那段代码之所以危险,原因在在于此。
明白这一点后,只需给代码加上两个copy方法调用,就可另其变得安全了:
void (^block)();
if (/* some condition */) {
block = [^{
NSLog(@"Block A");
} copy];
} else {
block = [^{
NSLog(@"Block B");
} copy];
}
block();