关于block网上也是一大把的文章,但总觉得有的说的太深有的说的太浅。最近看了一些block的资料,并动手做了一些实践,摘录并添加了一些结论。
什么是block?
首先,看一个极简的block:
1 | int main(int argc, const char * argv[]) { |
block编译转换结构
对其执行clang -rewrite-objc编译转换成C++实现,得到以下代码:
1 | struct __block_impl { |
不难看出其中的main_block_impl_0就是block的一个C++的实现(最后面的_0代表是main中的第几个block),也就是说也是一个结构体。
其中block_impl的定义如下:
1 | struct __block_impl { |
其结构体成员如下:
- isa,指向所属类的指针,也就是block的类型
- flags,标志变量,在实现block的内部操作时会用到
- Reserved,保留变量
- FuncPtr,block执行时调用的函数指针
可以看出,它包含了isa指针(包含isa指针的皆为对象),也就是说block也是一个对象* (runtime里面,对象和类都是用结构体表示)。
__main_block_desc_0的定义如下:
1 | static struct __main_block_desc_0 { |
其结构成员含义如下:
- reserved:保留字段
- Block_size:block大小(sizeof(struct __main_block_impl_0))
以上代码在定义main_block_desc_0结构体时,同时创建了main_block_desc_0_DATA,并给它赋值,以供在main函数中对__main_block_impl_0进行初始化。
__main_block_impl_0定义了显式的构造函数,其函数体如下:
1 | __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { |
可以看出,
- __main_block_impl_0的isa指针指向了_NSConcreteStackBlock,
- 从main函数中看, main_block_impl_0的FuncPtr指向了函数main_block_func_0
- main_block_impl_0的Desc也指向了定义main_block_desc_0时就创建的__main_block_desc_0_DATA,其中纪录了block结构体大小等信息。
- 以上就是根据编译转换的结果,对一个简单block的解析,后面会将block操作不同类型的外部变量,对block结构的影响进行相应的说明。
block实际结构
接下来观察下Block_private.h文件中对block的相关结构体的真实定义:
1 | /* Revised new layout. */ |
有了上文对编译转换的分析,这里只针对略微不同的成员进行分析:
invoke,同上文的FuncPtr,block执行时调用的函数指针,block定义时内部的执行代码都在这个函数中
Block_descriptor,block的详细描述
- copy/dispose,辅助拷贝/销毁函数,处理block范围外的变量时使用
- 总体来说,block就是一个里面存储了指向函数体中包含定义block时的代码块的函数指针,以及block外部上下文变量等信息的结构体。
block的类型
block的常见类型有3种:
_NSConcreteGlobalBlock(全局)
_NSConcreteStackBlock(栈)
_NSConcreteMallocBlock(堆)
- 其中前2种在Block.h种声明,后1种在Block_private.h中声明,所以最后1种基本不会在源码中出现。
- 由于无法直接创建_NSConcreteMallocBlock类型的block,所以这里只对前面2种进行手动创建分析,最后1种通过源代码分析。
NSConcreteGlobalBlock和NSConcreteStackBlock
首先,根据前面两种类型,编写以下代码:
1 | void (^globalBlock)() = ^{ |
对其进行编译转换后得到以下缩略代码:
1 | // globalBlock |
可以看出globalBlock的isa指向了_NSConcreteGlobalBlock,即在全局区域创建,编译时具体的代码就已经确定在上图中的代码段中了,block变量存储在全局数据存储区;stackBlock的isa指向了_NSConcreteStackBlock,即在栈区创建。
NSConcreteMallocBlock
- 接下来是在堆中的block,堆中的block无法直接创建,其需要由_NSConcreteStackBlock类型的block拷贝而来(也就是说block需要执行copy之后才能存放到堆中)。由于block的拷贝最终都会调用_Block_copy_internal函数,所以观察这个函数就可以知道堆中block是如何被创建的了:
1 |
|
从以上代码以及注释可以很清楚的看出,函数通过memmove将栈中的block的内容拷贝到了堆中,并使isa指向了_NSConcreteMallocBlock。
block主要的一些学问就出在栈中block向堆中block的转移过程中了。
捕捉变量对block结构的影响
接下来会编译转换捕捉不同变量类型的block,以对比它们的区别。
局部变量
前:
1 | - (void)test |
后:
1 | struct __Person__test_block_impl_0 { |
可以看到,block相对于文章开头增加了一个int类型的成员变量,他就是用来存储外部变量a的。可以看出,这次拷贝只是一次值传递。并且当我们想在block中进行以下操作时,将会发生错误, 因为_I_Person_test函数中的a和Persontest_block_func_0函数中的a并没有在同一个作用域,所以在block对a进行赋值是没有意义的,所以编译器给出了错误。我们可以通过地址传递来消除以上错误:
1 | - (void)test |
但是变量a的生命周期是和方法test的栈相关联的,当test运行结束,栈随之销毁,那么变量a就会被销毁,p也就成为了野指针。如果block是作为参数或者返回值,这些类型都是跨栈的,也就是说再次调用会造成野指针错误。
全局变量
前:
1 | // 全局静态 |
后:
1 | static int a; |
可以看出,因为全局变量都是在静态数据存储区,在程序结束前不会被销毁,所以block直接访问了对应的变量,而没有在Persontest_block_impl_0结构体中给变量预留位置。
局部静态变量
前
1 | - (void)test |
后:
1 | struct __Person__test_block_impl_0 { |
需要注意一点的是静态局部变量是存储在静态数据存储区域的,也就是和程序拥有一样的生命周期,也就是说在程序运行时,都能够保证block访问到一个有效的变量。但是其作用范围还是局限于定义它的函数中,所以只能在block通过静态局部变量的地址来进行访问。
关于变量的存储我原来的这篇博客有提及:c语言臆想–全局—局部变量
__block修饰的变量
前:
1 | - (void)test |
后:
1 | struct __Block_byref_a_0 { |
可以看到,对比上面的结果,明显多了Block_byref_a_0结构体,这个结构体中含有isa指针,所以也是一个对象,它是用来包装局部变量a的。当block被copy到堆中时,Persontest_block_impl_0的拷贝辅助函数Persontest_block_copy_0会将Block_byref_a_0拷贝至堆中,所以即使局部变量所在堆被销毁,block依然能对堆中的局部变量进行操作。其中Block_byref_a_0成员指针forwarding用来指向它在堆中的拷贝,其依据源码如下:
1 | static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) { |
这样做是为了保证操作的值始终是堆中的拷贝,而不是栈中的值。(处理在局部变量所在栈还没销毁,就调用block来改变局部变量值的情况,如果没有__forwarding指针,则修改无效)
至于block如何实现对局部变量的拷贝,下面会详细说明。
self隐式循环引用
前:
1 | @implementation Person |
后:
1 | struct __Person__test_block_impl_0 { |
如果在编译转换前,将_a改成self.a,能很明显地看出是产生了循环引用(self强引用block,block强引用self)。那么使用_a呢?经过编译转换后,依然可以在Persontest_block_impl_0看见self的身影。且在函数_I_Person_test中,传入的参数也是self。通过以下语句,可以看出,不管是用什么形式访问实例变量,最终都会转换成self+变量内存偏移的形式。所以在上面例子中使用_a也会造成循环引用。
1 | static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) { |
不同类型block的复制
block的复制代码在_Block_copy_internal函数中。
栈block
从以下代码可以看出,栈block的复制不仅仅复制了其内容,还添加了一些额外的东西
1 | * 1、往flags中并入了BLOCK_NEEDS_FREE(这个标志表明block需要释放,在release以及再次拷贝时会用到) |
堆block
从以下代码看出,如果block的flags中有BLOCK_NEEDS_FREE标志(block从栈中拷贝到堆时添加的标志),就执行latching_incr_int操作,其功能就是让block的引用计数加1。所以堆中block的拷贝只是单纯地改变了引用计数
1 | ... |
全局block
从以下代码看出,对于全局block,函数没有做任何操作,直接返回了传入的block
1 | else if (aBlock->flags & BLOCK_IS_GLOBAL) { |
block辅助函数
- 上文提及到了block辅助copy与dispose处理函数,这里分析下这两个函数的内部实现。在捕* 获变量为__block修饰的基本类型,或者为对象时,block才会有这两个辅助函数。
- block捕捉变量拷贝函数为_Block_object_assign。在调用复制block的函数_Block_copy_internal时,会根据block有无辅助函数来对捕捉变量拷贝函数_Block_object_assign进行调用。而在_Block_object_assign函数中,也会判断捕捉变量包装而成的对象(Block_byref结构体)是否有辅助函数,来进行调用。
__block修饰的基本类型的辅助函数
编写以下代码:
1 | typedef void(^Block)(); |
转换成C++代码后:
1 | typedef void(*Block)(); |
从上面代码中,被block修饰的a变量变为了Block_byref_a_0类型,根据这个格式,从源码中查看得到相似的定义:
1 | struct Block_byref { |
可以看出,__block将原来的基本类型包装成了对象。因为以上两个结构体的前4个成员的类型都是一样的,内存空间排列一致,所以可以进行以下操作:
1 | // 转换成C++代码 |
主要操作都在代码注释中了,总体来说,__block修饰的基本类型会被包装为对象,并且只在最初block拷贝时复制一次,后面的拷贝只会增加这个捕获变量的引用计数。
对象的辅助函数
没有__block修饰
1 | typedef void(^Block)(); |
首先,在没有__block修饰时,对象编译转换的结果如下,删除了一些变化不大的代码:
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { |
对象在没有block修饰时,并没有产生Block_byref_a_0结构体,只是将标志位修改为BLOCK_FIELD_IS_OBJECT。而在_Block_object_assign中对应的判断分支代码如下:
1 | else if ((flags & BLOCK_FIELD_IS_OBJECT) == BLOCK_FIELD_IS_OBJECT) { |
可以看到,block复制时,会retain捕捉对象,以增加其引用计数。
有__block修饰
1 | typedef void(^Block)(); |
在这种情况下,编译转换的部分结果如下:
1 | struct __Block_byref_a_0 { |
- 可以看到,对于对象,Block_byref_a_0另外增加了两个辅助函数Block_byref_id_object_copy、__Block_byref_id_object_dispose,以实现对对象
- 内存的管理。其中两者的最后一个参数131表示BLOCK_BYREF_CALLER|BLOCK_FIELD_IS_OBJECT,BLOCK_BYREF_CALLER表示在内部实现中不对a对象进行retain或copy;以下为相关源码
1 | if ((flags & BLOCK_BYREF_CALLER) == BLOCK_BYREF_CALLER) { |
_Block_byref_assign_copy函数的以下代码会对上面的辅助函数(__Block_byref_id_object_copy_131)进行调用;570425344表示BLOCK_HAS_COPY_DISPOSE|BLOCK_HAS_DESCRIPTOR,所以会执行以下相关源码:
1 | if (src->flags & BLOCK_HAS_COPY_DISPOSE) { |
ARC中block的工作
苹果文档提及,在ARC模式下,在栈间传递block时,不需要手动copy栈中的block,即可让block正常工作。主要原因是ARC对栈中的block自动执行了copy,将_NSConcreteStackBlock类型的block转换成了_NSConcreteMallocBlock的block。
block试验
下面对block做点实验:
1 | int main(int argc, const char * argv[]) { |
可以看出,ARC对类型为strong且捕获了外部变量的block进行了copy。并且当block类型为strong,但是创建时没有捕获外部变量,block最终会变成NSGlobalBlock类型(这里可能因为block中的代码没有捕获外部变量,所以不需要在栈中开辟变量,也就是说,在编译时,这个block的所有内容已经在代码段中生成了,所以就把block的类型转换为全局类型)
block作为参数传递
再来看下使用在栈中的block需要注意的情况:
1 | NSMutableArray *arrayM; |
可以看到,ARC情况下因为自动执行了copy,所以返回类型为NSMallocBlock,在函数结束后依然可以访问;而非ARC情况下,需要我们手动调用[block copy]来将block拷贝到堆中,否则因为栈中的block生命周期和函数中的栈生命周期关联,当函数退出后,相应的堆被销毁,block也就不存在了。
如果把block的以下代码删除:
1 | NSLog(@"%d", a); |
那么block就会变成全局类型,在main中访问也不会出崩溃。
block作为返回值
在非ARC情况下,如果返回值是block,则一般这样操作:
1 | return [[block copy] autorelease]; |
对于外部要使用的block,更趋向于把它拷贝到堆中,使其脱离栈生命周期的约束。
block属性
这里还有一点关于block类型的ARC属性。上文也说明了,ARC会自动帮strong类型且捕获外部变量的block进行copy,所以在定义block类型的属性时也可以使用strong,不一定使用copy。也就是以下代码:
1 | /** 假如有栈block赋给以下两个属性 **/ |
参考博文
谈Objective-C Block的实现(http://blog.devtang.com/blog/2013/07/28/a-look-inside-blocks/)
iOS中block实现的探究(http://blog.csdn.net/jasonblog/article/details/7756763)
A look inside blocks: Episode 3
runtime.c
Block_private.h