毫不夸张地说,block让objc这门语言变得更有魅力,它就是在其它语言中常见的闭包的概念。
在block之前,objc重度依赖delegate来完成一些用户行为,是block让我们开发者多了一个
更简单的选择,本文就《objc高级编程》来总结一下block的实现原理。
什么是block
用书中的话来概括block,就是:带自动变量的匿名函数。block可以有很多理解,它可以被
看作一个代码块,这个代码块即是匿名函数的函数体,它和普通的函数一样可以有参数,可
以有返回值,由此可见,block在使用上除了没有函数名之外和普通的函数没有区别,但是在
这里要注意区分函数和方法的区别。其带有自动变量,即是它可以保持block声明作用域内变
量的值,无论把这个block拿到哪里去执行,这些变量的值都为你保存着。这些都是显然的闭
包的概念,通过block来深入理解objc(甚至是其它语言)的闭包未尝不是一个好的方法。
虽然使用和理解block给接触objc不久的开发者带来了困扰,但是block的语法却很简单,通
过下面三个实例概括一下block的使用:
- 定义:
typedef void (^RFAudioBasicBlock) (void);
typedef void (^RFAudioSuccessBlock) (BOOL flag);
typedef void (^RFAudioSuccessDetailBlock) (BOOL flag, NSURL *url, NSTimeInterval duration);
typedef void (^RFAudioSuccessURLBlock) (BOOL flag, NSURL *url);
- 作为参数:
- (void)playWithURL:(NSURL *)url finishedBlock:(RFAudioSuccessDetailBlock)block;
- 使用:
[[RFAudioManager defaultManager] playWithURL:url finishedBlock:^(BOOL flag, NSURL *url) {
NSLog(@"播放结束:%@", url);
}];
block的本质与实现
直接通过代码来分析block的实现比较有说服力:
- objc源码
int main {
void (^blk)(void) = ^{printf("Block/n");};
blk();
return 0;
}
- 编译之后的代码
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
__main_block_impl_0 (void *fp, struct __main_block_desc_0 *desc, int flags=0){
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself){
printf("Block/n");
}
static struct __main_block_desc_0{
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
int main() {
void (^blk)(void) = (void (*)(void))&__main_block_impl_0(
(void *)__main_block_func_0, &__main_block_desc_0_DATA
);
( (void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr )(
(struct __block_impl *)blk
);
}
这段代码是我照着PDF文档手打出来的,等我打出来之后也就明白了这段代码的意思,不信你
可以试试。由源代码我们可以清晰地看到:
- Block就是__main_block_impl_0结构体指针类型的变量blk,即栈上生成的__main_block_impl_0
结构体实例(对象)。 - __block_impl即为Block的类信息,理解这里的类信息需要借助与objc对象与类之间的关系。
里面的isa变量我们很熟悉,标识了__block_impl的元类型NSConcreteStackBlock(其实标识了
该对象所处的位置在stack中)。 - Block如何截获自动变量,再简单不过了,将自动变量保存在__main_block_impl_0对象中即可。
由此,我们可以下结论:Block就是一个Objective-c对象。
__block说明符
Block能够轻松地截获自动变量的值,并在其调用内使用它,然而却不能更改它,如果需要在
Block内部更改外部的变量值,需要加上__block修饰符。咋一看,这个要求很简单,在Block
对象中保存变量的指针不就完事了吗?然而实际情况却不是,在Block调用的时候,自动变量
可能早已超出其作用域被销毁了,也就不能通过指针来访问自动变量了。看代码:
- objc代码
__block int val = 10;
*编译之后的代码
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int flags;
int __size;
int val;
};
int main() {
__Block_byref_val_0 val = {
0,
&val,
0,
sizeof(__Block_byref_val_0),
10
}
}
由上,我们不难看出所有被__block修饰的变量均会变成一个结构体(即对象),所以在block
中改变其值并不难,然而即便是对象也并没有解决超出作用域被收回的问题,下面我们来探讨
Block及__block变量超出作用域而存在的道理。
Block存储域
分解上述的isa指针,Block分为三种类型:
- _NSConcreteGlobalBlock
通过名称就可以判断出此Block为全局的,处于程序的数据区,生成此种类型的Block有
两种情况:
- 记述全局变量的地方有Block语法
- Block语法的表达式中不使用应该截获的自动变量时
此种类型的Block就如同全局变量一样,在全局环境下通过指针安全地使用。
_NSConcreteStackBlock
当Block截获自动变量的时候,其所处的区域在于Stack中。设置在栈上的Block,如果其
所属的作用域结束,该Block即会被废弃;同理__block类型的变量也配置在栈上,如果
其所属的变量作用域结束,该__block变量也会被废弃。_NSConcreteMallocBlock
大部分Block的结构体实例均是在栈上,如果在实例作用域结束的时候,要保留这个Block
就需要将其复制到堆上,如果堆上的Block引用也过期,就会被收回。下面探讨将Block
从栈复制到堆的情形。
typedef int (^blk_t)(int);
blk_t func(int rate) {
return ^(int count){return rate * count;};
}
该Block即是属于Stack上的实例变量,当函数返回的时候,Block也相继被废弃,但是此Block
作为函数的返回值,编译器会自动生成将Block复制到堆上的代码。一般情况下,编译器会自行
判断需要将Block复制到堆上的情形,但是也存在编译器不能判断的场景:
- 向方法或者函数的参数中传递Block时需要手动复制Block。这就是很好地解释了@property
后面的类型为Block时需要申明copy,当然申明strong/retain也不会出现问题,因为其对于
Block的默认行为也是copy。
还有两个我们不需要copy的场景:
- Cocoa框架的方法且方法名中含有usingBlock等时;
- 使用GCD的API时
至此我们可以稍微总结一下Block被复制的情形:
- 调用Block的copy实例方法时
- Block作为函数返回值返回时
- 将Block赋值给附有__strong修饰符的id类型的类或者Block类型的成员变量时
- 方法名中含有usingBlock的cocoa框架方法或者使用GCD时
__block变量作用域
由上文可知,__block变量被转化为结构体实例,其实就是对象,其随着持有它的Block一起被
复制到堆中或者被废弃。当其被复制之后,不管是栈中还是堆中,均可以访问到该对象,并对其
值进行的更改均有效,这是如何做到的呢?
在上文__Block_byref_val_0结构体中有个_forwarding字段,其即为__block变量对象的
指针,栈中对象的_forwarding实际上指向了堆中对象的地址,所以不管是在栈中还是
在堆中访问这个变量均指向了同一个地方即堆中的对象。所以在Block内外均能够对其
进行赋值和访问。
截获对象
有一种情况,如果非__block变量指向的是对象,那么我们依然得克服变量作用域失效而导致
对象被释放的困境,以达到Block截获自动变量的目的。这个问题也似乎很好解决,只需要在
Block中带入该自动变量的修饰符(strong/weak),于是Block对象中保持了对自动变量指向
的对象的强引用,那么目标对象即不会被释放,这样就达到了Block持有这个对象的目的。
实际上苹果也是这么做的,只是这个修饰符在Block被copy的时候才会生效,也即要求Block
对象处于堆中,如果该Block并没有被copy,那么自动变量的修饰符会丢失,自动变量所指
向的对象也会因为超出作用域而被释放。
如果__block变量指向的是对象,情况与非block变量类似。
最后
提醒:使用block是循环引用的高发区,所以要小心杜绝循环引用引起的内存泄露。