块的基础知识
块与函数类似,只不过是直接定义在另一个函数里的,和定义它的那个函数共享同一个范围内的东西。用“^”
符号表示。简单的块:
^{
//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); // 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
。不过如果是通过setter
、getter
方法捕获了实例变量,那么也会把self
变量一并捕获,因为实例变量是与self
所指代的实例关联在一起的。(此时要注意循环引用)
块的内部结构
块本身也是对象,在存放块对象的内存区域中,首个变量是只想Class
对象的指针,该指针叫做isa
指针。在内存布局中,最重要的就是invoke
变量,这是个函数指针,指向块的实现代码。函数原型至少要接受一个void*
类型的参数,此参数代表块。descriptor
是指向结构体的指针,每个块里都包含此结构体,其中声明了块对象的总体大小,还声明了copy
和dispose
两个辅助函数所对应的函数指针。块还会把它所捕获的所有变量都拷贝一份,这些靠背放在了descriptor
之后,捕获多少变量就要占据多少内存空间,这里拷贝的不是对象本身,而是指向这些对象的指针变量。
全局块、栈块、堆块
定义块的时候,其所占用的区域是分配在栈中的。就是说,块只在定义它的那个范围内有效。
编译器会给每个快分配好栈内存,然而等离开了相应的范围之后,编译器有可能把分配给块的内存复写掉。运行起来时而正确时而错误。为解决此问题,给每块对象发送copy
消息。这样就可以把块从栈复制到堆中。拷贝之后的块可在定义它的范围之外使用,而且一旦复制到堆上,块就成了带引用计数的对象了。后续的复制不会真的执行复制,只是递增独享的引用计数。
除了栈块、堆块外,还有全局块。这种快不会捕捉任何状态(比如外围变量),运行时也无须有状态来参与。块所使用的整个内存区域,在编译期就完全确定了。因此,全局块可声明在全局内存里。
要点:
- 块是C、C++、Objective-C中的词法闭包。
- 块接受参数,也可返回值。
- 块可以飞陪在栈或内存上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样和标准的Objective-C对象一样,具备引用计数。