iOS – Block底层解析

BlockiOS开发中一种比较特殊的数据结构,它可以保存一段代码,在合适的地方再调用,具有语法简介、回调方便、编程思路清晰、执行效率高等优点,受到众多猿猿的喜爱。但是Block在使用过程中,如果对Block理解不深刻,容易出现Cycle Retain的问题。本文主要从ARC模式下解析一下Block的底层实现,以及Block的三种类型(栈、堆、全局)的区别。

一、Block定义

1. Block 定义及使用

2. 项目中使用格式

在项目中,通常会重新定义block的类型的别名,然后用别名来定义block的类型

二、Block底层实现

block的底层实现是结构体,和类的底层实现类似,都有isa指针,可以把block当成是一个对象。下面通过创建一个控制台程序,来窥探block的底层实现

1. block内存结构

block 的内存结构图
block内存结构图

Block_layout结构体成员含义如下:

  • isa: 指向所属类的指针,也就是block的类型
  • flags: 标志变量,在实现block的内部操作时会用到
  • Reserved: 保留变量
  • invoke: block执行时调用的函数指针,block内部的执行代码都在这个函数中
  • descriptor: block的详细描述,包含 copy/dispose 函数,处理block引用外部变量时使用
  • variables: block范围外的变量,如果block没有调用任何外部变量,该变量就不存在

Block_descriptor结构体成员含义如下:

  • reserved: 保留变量
  • size: block的内存大小
  • copy: 拷贝block中被 __block 修饰的外部变量
  • dispose: 和 copy 方法配置应用,用来释放资源

具体实现代码如下(代码来自Block_private.h):

2. 创建block

3. 转换结构

利用 clang*.m 的文件转换为 *.cpp 文件,就可以看到 block 的底层实现了

转换后的代码:

从代码中可以看出,__main_block_impl_0就是blockC++实现(最后面的_0代表是main中的第几个block),__main_block_func_0block的代码块,__main_block_desc_0block的描述,__block_implblock的定义。

__block_impl成员含义如下:

  • isa: 指向所属类的指针,也就是block的类型
  • flags,标志变量,在实现block的内部操作时会用到
  • Reserved,保留变量
  • FuncPtr,block执行时调用的函数指针

__main_block_impl_0解释如下:

  • impl: block对象
  • Desc: block对象的描述

其中,__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) 这是显式构造函数,flags的默认值为0,函数体如下:

可以看出:

  • __main_block_impl_0的isa指针指向了_NSConcreteStackBlock,所有的局部block都是在栈上门创建
  • 从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_size记录了block结构体大小等信息

__main_block_desc_0成员含义如下:

  • reserved: 保留变量
  • Block_size: block内存大小,sizeof(struct __main_block_impl_0)

三、Block类型

block有三种类型:

  • _NSConcreteGlobalBlock: 存储在全局数据区
  • _NSConcreteStackBlock: 存储在栈区
  • _NSConcreteMallocBlock: 存储在堆区

APUE的进程虚拟内存段分布图如下:

内存分布图

其中,_NSConcreteGlobalBlock_NSConcreteStackBlock 可以由程序创建,而 _NSConcreteMallocBlock 则无法由程序创建,只能由 _NSConcreteStackBlock 通过拷贝生成。

1. 全局block 和 栈block

测试代码如下:

clang转换后的代码如下:

可以看出, globalBlockisa 指向了 _NSConcreteGlobalBlock,即在全局区域创建,编译时其具体代码在代码段中,block变量则存储在全局数据区;而stackBlockisa 则指向了 _NSConcreteStackBlock,表明在栈区创建。

2. 捕获外部参数对栈区block结构的影响

由于堆区 block 是由栈区 block 转化而成, 所以下面主要分析栈区 block 如何转化为堆区 block

捕获局部非静态变量

代码:

转化后:

可以看到,block 对于栈区变量的引用只是值传递,由于 block 内部变量 a 和 外部变量 a 不在同一个作用域,所以在 block 内部不能把变量 a 作为左值(left-value),因为赋值没有意义。所以,如果出现如下代码,编译器会提示错误:

捕获局部静态变量

代码:

转换后:

由于局部静态变量也存储在静态数据区,和程序拥有一样的生命周期,但是其作用范围局限在定义它的函数中,所有在block里是通过地址来访问。

捕获全局变量

代码:

转换后:

可以看出,因为全局变量都在静态数据区,在程序结束前不会被销毁,所以block直接访问了对应的变量,而没有在Persontest_block_impl_0结构体中给变量预留位置。

捕获对象的变量

代码:

转换后:

可以看到,即使 block只是引用对象的变量,但是底层依然引用的是对象本身 self,这和直接使用 self.a产生的循环引用的问题是一样的。所以,要在 block 内使用对象的弱引用,即可解决循环引用的问题。并且,对变量a的访问也是通过 self的地址加 a的偏移量的形式。

捕获__block修饰的基本变量

代码:

转换后:

可以看到,被__block修饰的变量被封装成了一个对象,类型为__Block_byref_a_0,然后把&a作为参数传给了block

__Block_byref_a_0 成员含义如下:

  • __isa: 指向所属类的指针,被初始化为 (void*)0
  • __forwarding: 指向对象在堆中的拷贝
  • __flags: 标志变量,在实现block的内部操作时会用到
  • __size: 对象的内存大小
  • a: 原始类型的变量

其中,isa__flags__size 的含义和之前类似,而 __forwarding 是用来指向对象在堆中的拷贝,runtime.c 里有源码说明:

这样做是为了保证在 block内 或 block 变量后面对变量a的访问,都是直接访问堆内的对象,而不上栈上的变量。同时,在 block 拷贝到堆内时,它所捕获的由 __block 修饰的局部基本类型也会被拷贝到堆内(拷贝的是封装后的对象),从而会有 copydispose处理函数。

Block_byref的结构定义在 Block_private.h 文件里有介绍:

可以看到,Block_byref__Block_byref_a_0 的前4个成员类型相同,可以互相转化。

** copy 函数

copy 的实现函数是 _Block_object_assign,它根据对象的 flags 来判断是否需要拷贝,或者只是赋值,函数实现在 runtime.c 里:

主要操作都在代码注释中了,总体来说,__block修饰的基本类型会被包装为对象,并且只在最初block拷贝时复制一次,后面的拷贝只会增加这个捕获变量的引用计数。

** dispose 函数

dispose 的实现函数是 _Block_object_dispose,代码依然可以在 runtime.c 里:

可以看到,被__block修改的变量,释放时要 latching_decr_int减引用计数,直到计数为0,就释放改对象;而普通的对象、block,就直接释放销毁。

捕获没有__block修饰的对象

代码:

转换后部分结果如下:

对象在没有__block修饰时,并没有产生__Block_byref_a_0结构体,只是将标志位修改为BLOCK_FIELD_IS_OBJECT。而在_Block_object_assign中对应的判断分支代码如下:

可以看到,block复制时,会retain捕捉对象,以增加其引用计数。

捕获有__block修饰的对象

代码:

转化后部分结果如下:

可以看到,对于对象,处理增加了__Block_byref_a_0外,还另外增加了两个辅助函数__Block_byref_id_object_copy__Block_byref_id_object_dispose,以实现对对象内存的管理。其中两者的最后一个参数131表示BLOCK_BYREF_CALLER|BLOCK_FIELD_IS_OBJECTBLOCK_BYREF_CALLER表示在内部实现中不对a对象进行retaincopy;以下为_Block_object_assign函数的部分代码:

_Block_byref_assign_copy函数的以下代码会对上面的辅助函数__Block_byref_id_object_copy_131进行调用;570425344表示BLOCK_HAS_COPY_DISPOSE | BLOCK_HAS_DESCRIPTOR,即(1<<25 | 1<<29),_Block_byref_assign_copy函数的部分代码:

四、ARC中block的工作特点

ARC模式下,在栈间传递block时,不需要手动copy栈中的block,即可让block正常工作。主要原因是ARC对栈中的block自动执行了copy,将_NSConcreteStackBlock类型的block转换成了_NSConcreteMallocBlock类型的block

1. block,非函数参数

代码:

从打印结果可以看出,ARC模式下,block 只有引用了外部的变量,并且被强引用,才会被拷贝到堆上;只引用了外部的变量,或者被弱引用都只在栈上创建;如果没有引用外部变量,无论是否被强引用,都会被转换为全局 block,也就是说,在编译时,这个block的所有内容已经在代码段中生成了。

2. block,作为参数传递

代码

可以看出,ARC模式下,栈区的block 被拷贝到了堆区,在 testBlock 函数结束后依然可以访问;而 MRC模式下,由于我们没有手动执行[block copy]来将block拷贝到堆区,随着函数生命周期结束,block被销毁,访问时出现野指针错误,但是如果把testBlock函数中的block打印语句删掉:

那么,block就变为全局的,在MRC模式下,再次访问不会出错。


参考文章

http://www.jianshu.com/p/51d04b7639f1

http://www.jianshu.com/p/aff2cad778c0

http://www.galloway.me.uk/2013/05/a-look-inside-blocks-episode-3-block-copy/

runtime.c

Block.private.h

感觉不错,打个赏?
微信                                 支付宝
pay_weixin            pay_zhifubao
金额随意 快来“”我呀~
联系方式:kelvin@fishbay.cn

发表评论

电子邮件地址不会被公开。 必填项已用*标注