第六章 块与大中枢派发—第37条:理解"块"这一概念

当前多线程编程的核心就是"块"(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();
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容