第三十七条 理解“块”这一概念

2017/9/2 posted in  第六章 块与GCD

首先,块与函数类似,只不过是直接定义在另一个函数里的,和定义它的那个函数共享同一 个范围内的东西。块用“^”符号来表示,后面跟着一对花括号,括号里面是块的实现代码。 例如,下面就是个简单的块:

^{
    //Block implementation here
}

块其实就是个值,而且自有其相关类型。与intfloatObjective-C对象一样,也可以把块赋给变量,然后像使用其他变量那样使用它。块类型的语法与函数指针近似。下面列出的这个块很简单,没有参数,也不返回值:

void (^someBlock) () = A {
    //Block implementation here
};

这段代码定义了一个名为someBlock的变量。由于变量名写在正中间,所以看上去也许 =有点怪,不过一旦理解了语法,很容易就能读懂。块类型的语法结构如下:

return_type (^block_name)(parameters)

我们来举个例子,下面这种写法所定义的块,返回int值,并且接受两个int做参数:

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

定义好之后,就可以像函数那样使用了。比方说,addBlock块可以这样用:

int add = addBlock (2, 5) ;  //< add = 12

块的强大之处是:在声明它的范围里,所有变量都可以为其所捕获。这也就是说,那个范围里的全部变量,在块里依然可用。比如,下面这段代码所定义的块,就使用了块以外的变量:

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

int add = addBlock (2, 5);  //< add = 12

默认情况下,为块所捕获的变量,是不可以在块里修改的。在本例中,假如块内的代码改动了additional变量的值,那么编译器就会报错。不过,声明变量的时候可以加上__block 修饰符,这样就可以在块内修改了。

例如:

__block int additional = 5;
int (^addBlock) (int a, int b) = ^(int a, int b){   
    additional++;
    return a + b + additional;
};

int add = addBlock (2, 5);  //< add = 13

块的另一个用法是“内联块”(inline block),例如:

NSArray *array = @[@0, @1, @2, @3, @4, @5];
_block NSInteger count = 0;
[array enumerateObjectsUsingBlock:
    ^(NSNumber *number, NSUInteger idx, BOOL *stop){
        if([number compare:@2] == NSOrderedAscending) { 
        count++;
    }
}];
//count = 2

这段范例代码也演示了“内联块”(inline block)的用法。传给“numerateObjectsUsingBlock:”方法的块并未先賦给局部变量,而是直接内联在函数调用里了。

然后我们在声明和使用块的时候,要注意它的作用范围。定义块的时候,其所占的内存区域是分配在栈中的。这就是说,块只在定义它的那个范围内有效。比如下面这个:

void(^block)();
if ( /* some condition */ ){
    block = ^{
        NSLog(@"Block A");
    };
} else {
    block = ^{
        NSLog(@"Block B");
    };
}
block();

定义在ifelse语句中的两个块都分配在栈内存中。编译器会给每个块分配好栈内存, 然而等离开了相应的范围之后,编译器有可能把分配给块的内存覆写掉。于是,这两个块只 能保证在对应的ifelse语句范围内有效。这样写出来的代码可以编译,但是运行起来时而正确,时而错误。若编译器未覆写待执行的块,则程序照常运行,若覆写,则程序崩溃。

我们为了解决这个问题可以给块对象发送copy消息以拷贝之。这样的话,就可以把块从栈复制到堆了。

拷贝后的块,可以在定义它的那个范围之外使用。而且,一旦复制到堆上,块就成了带引用计数的对象了。后续的复制操作都不会真的执行复制,只是递增块对象的引用计数。如果不再使用这个块,那就应将其释放,在ARC环境下会自动释放。

改动后跟下面一样:

void (^block)();
if (/* some condition */ ){ 
    block = [^{
        NSLog(@,fBlock Aw);
    } copy];
} else {
    block = [^{
        NSLog(@"Block B");
    } copy];
}
block();

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

void (^block)() = ^{
    NSLog(@"This is a block");
};

由于运行该块所需的全部信息都能在编译期确定,所以可把它做成全局块。这完全是种优化技术:若把如此简单的块当成复杂的块来处理,那就会在复制及丢弃该块时执行一些无谓的操作。

要点

  • 块是C、C++、Objective-C中的词法闭包。
  • 块可接受参数,也可返回值。
  • 块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样的话,就和标准的Objective-C对象一样,具备引用计数了。