理解“块”这一概念

《Effective Objective-C 2.0》读书笔记

Posted by Japho on August 12, 2018

块的基础知识

块与函数类似,只不过是直接定义在另一个函数里的,和定义它的那个函数共享同一个范围内的东西。用“^”符号表示。简单的块:

^{
//Block implementation here
}

块其实就是个值,而且自有其相关类型。与intfloat或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); // add = 7;

块的强大之处:在声明的范围内,所有变量都可以为其所捕获。也就是说,那个范围里的全部变量,在块里依然可以使用。例如:

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

默认情况下,为块所捕获的变量,是不可以在块里修改的。编译器会报错,声明变量时加上__block修饰符,这样就可以在块内修改了。

如果块所捕获的变量时对象类型,那么就会自动保留它。系统在释放这个块的时候,也会将其一并释放。实际上,在其他Objective-C对象所能响应的选择子中,有很多是块也可以响应的。而重要的在于块和其他对象一样,也有引用计数,当最后一个指向块的引用移走之后,块就会回收了。回收时,也会释放块所捕获的变量。

如果将块定义在Objective-C类的实例方法中,除了可以访问类的所有实例变量之外,还可以使用self变量。块总可以修改实例变量,所以在声明时无须加__block。不过如果是通过settergetter方法捕获了实例变量,那么也会把self变量一并捕获,因为实例变量是与self所指代的实例关联在一起的。(此时要注意循环引用)

块的内部结构

块本身也是对象,在存放块对象的内存区域中,首个变量是只想Class对象的指针,该指针叫做isa指针。在内存布局中,最重要的就是invoke变量,这是个函数指针,指向块的实现代码。函数原型至少要接受一个void*类型的参数,此参数代表块。descriptor是指向结构体的指针,每个块里都包含此结构体,其中声明了块对象的总体大小,还声明了copydispose两个辅助函数所对应的函数指针。块还会把它所捕获的所有变量都拷贝一份,这些靠背放在了descriptor之后,捕获多少变量就要占据多少内存空间,这里拷贝的不是对象本身,而是指向这些对象的指针变量。

全局块、栈块、堆块

定义块的时候,其所占用的区域是分配在栈中的。就是说,块只在定义它的那个范围内有效。

编译器会给每个快分配好栈内存,然而等离开了相应的范围之后,编译器有可能把分配给块的内存复写掉。运行起来时而正确时而错误。为解决此问题,给每块对象发送copy消息。这样就可以把块从栈复制到堆中。拷贝之后的块可在定义它的范围之外使用,而且一旦复制到堆上,块就成了带引用计数的对象了。后续的复制不会真的执行复制,只是递增独享的引用计数。

除了栈块、堆块外,还有全局块。这种快不会捕捉任何状态(比如外围变量),运行时也无须有状态来参与。块所使用的整个内存区域,在编译期就完全确定了。因此,全局块可声明在全局内存里。

要点:

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