block详解

毫不夸张地说,block让objc这门语言变得更有魅力,它就是在其它语言中常见的闭包的概念。 在block之前,objc重度依赖delegate来完成一些用户行为,是block让我们开发者多了一个 更简单的选择,本文就《objc高级编程》来总结一下block的实现原理。

什么是block

用书中的话来概括block,就是:带自动变量的匿名函数。block可以有很多理解,它可以被 看作一个代码块,这个代码块即是匿名函数的函数体,它和普通的函数一样可以有参数,可 以有返回值,由此可见,block在使用上除了没有函数名之外和普通的函数没有区别,但是在 这里要注意区分函数和方法的区别。其带有自动变量,即是它可以保持block声明作用域内变 量的值,无论把这个block拿到哪里去执行,这些变量的值都为你保存着。这些都是显然的闭 包的概念,通过block来深入理解objc(甚至是其它语言)的闭包未尝不是一个好的方法。

虽然使用和理解block给接触objc不久的开发者带来了困扰,但是block的语法却很简单,通 过下面三个实例概括一下block的使用:

  1. 定义:
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);
  1. 作为参数:
- (void)playWithURL:(NSURL *)url finishedBlock:(RFAudioSuccessDetailBlock)block;
  1. 使用:
[[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文档手打出来的,等我打出来之后也就明白了这段代码的意思,不信你 可以试试。由源代码我们可以清晰地看到:

  1. Block就是main_block_impl_0结构体指针类型的变量blk,即栈上生成的main_block_impl_0 结构体实例(对象)。
  2. block_impl即为Block的类信息,理解这里的类信息需要借助与objc对象与类之间的关系。 里面的isa变量我们很熟悉,标识了block_impl的元类型NSConcreteStackBlock(其实标识了 该对象所处的位置在stack中)。
  3. 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是循环引用的高发区,所以要小心杜绝循环引用引起的内存泄露。



Previous     Next
zhing /
Published under (CC) BY-NC-SA in categories ios  tagged with ios