要了解Block需要先了解什么是“闭包性”
2.3 闭包性
上文说过,block实际是Objc对闭包的实现。
我们来看看下面代码:
#import void logBlock( int ( ^ theBlock )( void ) )
{
NSLog( @"Closure var X: %i", theBlock() );
}
int main( void )
{
NSAutoreleasePool * pool;
int ( ^ myBlock )( void );
int x;
pool = [ [ NSAutoreleasePool alloc ] init ];
x = 42;
myBlock = ^( void )
{
returnx;
};
logBlock( myBlock );
[ pool release ];
returnEXIT_SUCCESS;
}
上面的代码在main函数中声明了一个整型,并赋值42,另外还声明了一个block,该block会将42返回。然后将block传递给 logBlock函数,该函数会显示出返回的值42。即使是在函数logBlock中执行block,而block又声明在main函数中,但是 block仍然可以访问到x变量,并将这个值返回。
注意:block同样可以访问全局变量,即使是static。
一,明确两点
1,Block可以访问Block函数以及语法作用域以内的外部变量。也就是说:一个函数里定义了个block,这个block可以访问该函数的内部变量(当然还包括静态,全局变量)-即block可以使用和本身定义范围相同的变量。
2,Block其实是特殊的Objective-C对象,可以使用copy,release等来管理内存,但和一般的NSObject的管理方式有些不同,稍后会说明。
二,Block语法
Block很像函数指针,这从Block的语法上就可以看出。Block的原型:
返回值 (^名称)(参数列表)
Block的定义
^ 返回值类型 (参数列表) { 表达式 }
其中返回值类型和参数列表都可以省略,最简单的Block就是:
^{ ; };
一般的定义就是:
返回值 (^名称)(参数列表) = ^(参数列表){代码段};
为了方便通常使用typedef定义:
typedef void (^blk) (void);
三,Block存储域
Block能够截获自动变量,自动变量的当前值会被拷贝到栈上作为常量,此时不能在Block内对自动变量进行赋值操作,如果有这种需求,则需要该变量是:
1,静态变量
2,全局变量
3,或者使用__block修饰符
根据Block中是否引用了自动变量,可以将Block存储区域分类:
1,_NSConcreteStackBlock-存储在栈上
2,_NSConcreteGlobalBlock-存储在全局数据区域(和全局变量一样)
3,_NSConcreteMallocBlock-存储在堆上
没 有引用自动变量或者在全局作用域的Block为_NSConcreteGlobalBlock,其他的基本上都是 _NSConcreteStackBlock。对_NSConcreteStackBlock执行copy操作会生成 _NSConcreteMallocBlock。
一般来说出问题的Block大部分都是_NSConcreteStackBlock,超过了_NSConcreteStackBlock的作用域_NSConcreteStackBlock就会销毁。
四,对Block执行retain,copy方法的效果
Block是C语言的扩展,C语法也可以使用Block的语法,对应的C语言使用Block_copy,Block_release.
无论是_NSConcreteStackBlock,还是_NSConcreteGlobalBlock,执行retain都不起作用。而_NSConcreteMallocBlock执行retain引用计数+1。
对 于copy操作,_NSConcreteStackBlock会被复制到堆上得到新的_NSConcreteMallocBlock,而 _NSConcreteGlobalBlock执行copy操作不起作用。而对_NSConcreteMallocBlock执行copy操作会引起引用 计数加1。
那么就引出一个问题,对_NSConcreteMallocBlock多次copy会不会引起问题呢?参考Objective-C高级管理115页。
五,什么时候要对NSConcreteStackBlock执行copy操作?
配 置在栈上的Block也就是NSConcreteStackBlock类型的Block,如果其所属的变量作用域结束该Block就会废弃。这个时候如果 继续使用该Block,就应该使用copy方法,将NSConcreteStackBlock拷贝为_NSConcreteMallocBlock。当 _NSConcreteMallocBlock的引用计数变为0,该_NSConcreteMallocBlock就会被释放。
如果是非ARC环境,需要显式的执行copy或者antorelease方法。
而当ARC有效的时候,实际上大部分情况下编译器已经为我们做好了,自动的将Block从栈上复制到堆上。包括以下几个情况:
1,Block作为返回值时
类似在非ARC的时候,对返回值Block执行[[returnedBlock copy] autorelease];
2,方法的参数中传递Block时
3,Cocoa框架中方法名中还有useringBlock等时
4,GCD相关的一系列API传递Block时。
比如:[mutableAarry addObject:stackBlock];这段代码在非ARC环境下肯定有问题,而在ARC环境下方法参数中传递NSConcreteStackBlock会自动执行copy。
六,Block的循环引用
对于非ARC下, 为了防止循环引用, 我们使用__block来修饰在Block中使用的对象:
对于ARC下, 为了防止循环引用, 我们使用__weak来修饰在Block中使用的对象。原理就是:ARC中,Block中如果引用了__strong修饰符的自动变量,则相当于Block对该变量的引用计数+1。
2.4 block中变量的复制与修改
对于block外的变量引用,block默认是将其复制到其数据结构中来实现访问的,如下图:
通过block进行闭包的变量是const的。也就是说不能在block中直接修改这些变量。来看看当block试着增加x的值时,会发生什么:
myBlock = ^( void )
{
x++;
returnx;
};
编译器会报错,表明在block中变量x是只读的。
有时候确实需要在block中处理变量,怎么办?别着急,我们可以用__block关键字来声明变量,这样就可以在block中修改变量了。
基于之前的代码,给x变量添加__block关键字,如下:
__block int x;
对于用__block修饰的外部变量引用,block是复制其引用地址来实现访问的,如下图:
3.编译器中的block
3.1 block的数据结构定义
我们通过大师文章中的一张图来说明:
上图这个结构是在栈中的结构,我们来看看对应的结构体定义:
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
从上面代码看出,Block_layout就是对block结构体的定义:
isa指针:指向表明该block类型的类。
flags:按bit位表示一些block的附加信息,比如判断block类型、判断block引用计数、判断block是否需要执行辅助函数等。
reserved:保留变量,我的理解是表示block内部的变量数。
invoke:函数指针,指向具体的block实现的函数调用地址。
descriptor:block的附加描述信息,比如保留变量数、block的大小、进行copy或dispose的辅助函数指针。
variables:因为block有闭包性,所以可以访问block外部的局部变量。这些variables就是复制到结构体中的外部局部变量或变量的地址。
3.2 block的类型
block有几种不同的类型,每种类型都有对应的类,上述中isa指针就是指向这个类。这里列出常见的三种类型:
_NSConcreteGlobalBlock:全局的静态block,不会访问任何外部变量,不会涉及到任何拷贝,比如一个空的block。例如:
#include int main()
{
^{ printf("Hello, World!\n"); } ();
return0;
}
_NSConcreteStackBlock:保存在栈中的block,当函数返回时被销毁。例如:
#include int main()
{
char a ='A';
^{ printf("%c\n",a); } ();
return0;
_NSConcreteMallocBlock:保存在堆中的block,当引用计数为0时被销毁。该类型的block都是由 _NSConcreteStackBlock类型的block从栈中复制到堆中形成的。例如下面代码中,在 exampleB_addBlockToArray方法中的block还是_NSConcreteStackBlock类型的,在exampleB方法中 就被复制到了堆中,成为_NSConcreteMallocBlock类型的block:
void exampleB_addBlockToArray(NSMutableArray *array) {
char b ='B';
[array addObject:^{
printf("%c\n", b);
}];
}
void exampleB() {
NSMutableArray *array = [NSMutableArray array];
exampleB_addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block();
}
总结一下:
_NSConcreteGlobalBlock类型的block要么是空block,要么是不访问任何外部变量的block。它既不在栈中,也不在堆中,我理解为它可能在内存的全局区。
_NSConcreteStackBlock类型的block有闭包行为,也就是有访问外部变量,并且该block只且只有有一次执行,因为栈中的空间是可重复使用的,所以当栈中的block执行一次之后就被清除出栈了,所以无法多次使用。
_NSConcreteMallocBlock类型的block有闭包行为,并且该block需要被多次执行。当需要多次执行时,就会把该block从栈中复制到堆中,供以多次执行。
3.3 编译器如何编译
我们通过一个简单的示例来说明:
#import typedef void(^BlockA)(void);
__attribute__((noinline))
void runBlockA(BlockA block) {
block();
}
void doBlockA() {
BlockA block = ^{
// Empty block
};
runBlockA(block);
}
上面的代码定义了一个名为BlockA的block类型,该block在函数doBlockA中实现,并将其作为函数runBlockA的参数,最后在函数doBlockA中调用函数runBloackA。
注 意:如果block的创建和调用都在一个函数里面,那么优化器(optimiser)可能会对代码做优化处理,从而导致我们看不到编译器中的一些操作,所 以用__attribute__((noinline))给函数runBlockA添加noinline,这样优化器就不会在doBlockA函数中对 runBlockA的调用做内联优化处理。
我们来看看编译器做的工作内容:
#import __attribute__((noinline))
void runBlockA(struct Block_layout *block) {
block->invoke();
}
void block_invoke(struct Block_layout *block) {
// Empty block function
}
void doBlockA() {
struct Block_descriptor descriptor;
descriptor->reserved = 0;
descriptor->size = 20;
descriptor->copy = NULL;
descriptor->dispose = NULL;
struct Block_layout block;
block->isa = _NSConcreteGlobalBlock;
block->flags = 1342177280;
block->reserved = 0;
block->invoke = block_invoke;
block->descriptor = descriptor;
runBlockA(&block);
}
上面的代码结合block的数据结构定义,我们能很容易得理解编译器内部对block的工作内容。
3.4 copy()和dispose()
上 文中提到,如果我们想要在以后继续使用某个block,就必须要对该block进行拷贝操作,即从栈空间复制到堆空间。所以拷贝操作就需要调用 Block_copy()函数,block的descriptor中有一个copy()辅助函数,该函数在Block_copy()中执行,用于当 block需要拷贝对象的时候,拷贝辅助函数会retain住已经拷贝的对象。
既然有有copy那么就应该有release,与 Block_copy()对应的函数是Block_release(),它的作用不言而喻,就是释放我们不需要再使用的block,block的 descriptor中有一个dispose()辅助函数,该函数在Block_release()中执行,负责做和copy()辅助函数相反的操作,例 如释放掉所有在block中拷贝的变量等。
4.总结
以上内容是我学习各大师的文章后对自己学习情况的一个记录,其中有部分文字和代码示例是来自大师的文章,还有一些自己的理解,如有错误还请大家勘误。