iOS 开发—探秘 Block 原理

移动开发 iOS
很多block原理性的文章都比较老,里面讲的一些知识已经过时,这里用新版的iOS SDK再梳理一遍block原理,也是和大家一起对已有知识做一次复习。

1.概述

在iOS开发中,block大家用的都很熟悉了,是iOS开发中闭包的一种实现方式,可以对一段代码逻辑进行封装,使其可以像数据一样被传递、存储、调用,并且可以保存相关的上下文状态。

很多block原理性的文章都比较老,里面讲的一些知识已经过时,这里用新版的iOS SDK再梳理一遍block原理,也是和大家一起对已有知识做一次复习。

2.内存布局

block本质上可以理解为结构体,对于结构体的内存布局,先用一张图来表示一下,图中字段顺序按照布局的先后顺序:

  • isa:block也有isa,从内存结构上也属于对象,isa指向的是block的类对象,类对象例如__NSMallocBlock__,后续文章会讲到;
  • flags:用于存储一些标志位信息,例如是否捕获外部变量;
  • reserved:系统保留字段,后续可能会用于一些编译优化标志位,或者存储一些临时变量的处理;
  • invoke:函数指针,指向了block要执行的函数地址,也就是block代码块对应的函数地址;
  • descriptor(现在叫desc):指向block_desc_0,包含block大小、捕获的外部变量布局信息、增加引用计数和销毁的相关函数指针;
  • variables:block捕获的外部变量。

图片图片

3.类型

由于block也是对象,可以通过class方法获取到其类型,也就是类对象。block有下面三种类型:

  • __NSGlobalBlock__,没有访问auto变量的block,访问static变量是没问题的。这种类型的变量并没有什么意义,如果不需要用到auto变量,写成方法就可以满足需求;
  • __NSStackBlock__,在MRC环境下,访问了auto变量,会默认被放在栈区。需要手动copy到堆区,ARC环境下会在访问auto变量后,会自动拷贝到堆区;
  • __NSMallocBlock__,由开发者自己管理内存,不会由系统来释放。

block的分配主要是在三个区域,堆区、栈区、全局区,全局区的数据存储在数据段。

block在不同的场景会存在不同的内存区域中,在MRC中创建一个block首先是在__NSStackBlock__内存中的,然后我们使用copy方法将block拷贝到__NSMallocBlock__内存中进行内存管理。后来在ARC中系统已经帮我们做好了copy的操作,创建的block会自动copy到__NSMallocBlock__内存中,堆区的block也有引用计数的概念。如果这个block中没有用到任何外部参数,系统会将这个block存放在__NSGlobalBlock__内存中。

图片图片

并且block也有继承关系,以下面TestBlock的实例来说,其父类是__NSGlobalBlock__,所有block的父类是NSBlock,并且NSBlock继承自NSObject类。在更早一些的iOS系统中,__NSGlobalBlock__和NSBlock之间,还会有一层__NSGlobalBlock的关系(后面没有下划线)。

图片图片

4.转换C++

下面,我们通过clang命令将block转为结构体,来分析下其具体实现。虽然这并不是最终运行在iOS系统上的代码,其等于一种中间表现形式,后续编译链接优化才会形成运行在手机上的ipa包,但对于我们了解block的实现原理有很大帮助。

4.1转换命令

xcrun是Xcode用于查找和执行相关命令行的工具集,可以更好的执行clang命令,减少报错。

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc [源文件路径] -o [目标文件路径]

clang命令有下面这些关键参数:

  • -fobjc-arc:如果项目是ARC或者ARC和MRC混编的环境,需要通过此参数修饰,表示按ARC的方式进行转换,如果不需要ARC环境可以忽略;
  • -x objective-c++:此参数上面没用,如果包含Objective++源文件的时候,需要用到此参数,以确保clang可以区分OC和C++代码;
  • -rewrite-objc:告诉clang以C++的方式重写出来,包含的上层代码,clang会以底层代码的方式进行展现;
  • [目标文件路径]:非必传参数,不传的话默认在当前目录生成一个同名的cpp文件,例如main.m对应main.cpp。

4.2转换示例

下面在main.m中实现了一个很简单的block,并且没有捕获任何外部变量,通过clang命令查看C++代码,观察block的具体实现原理。

图片图片

转换后将C++源文件拉到最下面,可以看到main函数以及TestBlock的实现,main函数中有很多转义代码,删掉后梳理逻辑会更清晰。

图片图片

5.结构体

5.1基础结构

转换后的代码看着比较复杂,但我们只看关键信息,__main_block_impl_0构造函数也可以去掉,整理后就是下面三个结构体。在不包含外部变量和__block的前提下,block结构体各个字段就这么简单,关键就是isa、Block_size、FuncPtr这三个。

图片图片

我们也可以打印block结构体相关字段,但由于block的结构体并没有声明在某个.h文件中,所以需要我们讲clang转换后的结构体粘到对应的文件中,做显示声明。随后用__bridge的方式,将block对象桥接为自己声明的结构体,即可打印对应字段。

图片图片

结构体中impl.FuncPtr存储的就是回调函数地址,从地址可以看出是一个虚拟地址,block结构体都存储在堆区。

图片图片

5.2调用部分

看完block结构体的定义,我们来到main函数中,看block的实现和调用转换后是什么样的。将main函数中block相关的转换都去掉,结果如红圈部分。本质上就是两步,第一步是调用__main_block_impl_0的结构体构造函数,第二步是调用结构体的函数指针。

图片图片

第一行main函数中调用的构造方法,是__main_block_impl_0结构体声明的C++构造函数,因为我们创建的是一个最简单block,可以看到block的存储区域是在stack栈区的。即main函数调用完,block生命周期就会结束。

图片图片

__main_block_impl_0构造函数有两个参数,第一个红圈部分就是传入函数指针地址,函数对应的就是block内部的实现代码。第二个参数是__main_block_desc_0_DATA结构体,其定义为__main_block_desc_0,并且默认实现第一个参数传0,第二个参数是block结构体的大小,结构体为__main_block_impl_0 block自身的结构体大小。第三个参数有默认值,可以不传。

图片图片

__main_block_desc_0结构体是一种紧凑型的写法,在声明__main_block_desc_0结构体后,紧接着声明了一个名为__main_block_desc_0_DATA的变量,变量类型为静态变量,并且实现了初始化相关代码。

图片图片

在执行block的代码位置,可以看到并不是block->impl.FuncPtr的方式调用,而是直接block->FuncPtr的方式调用,中间少了一步。

严谨些来说应该加上impl,但不加也不会出问题。这是因为,如果看未删除转换代码的原始clang代码,可以看到block是被转换为__block_impl的,也就是说被当做__block_impl看待的。如果再结合__main_block_impl_0的结构体定义来看,__block_impl在成员变量的第一位,所以访问FuncPtr是没有问题的,只要不访问Desc就是可以的。

6.外部变量

6.1值类型

如果在block的调用中加一个外部变量,那结构体将会是怎样的?

图片图片

通过clang命令可以可以看到,转换后的__main_block_impl_0中增加了一个同名字段,这很简单没必要过多解释。在__main_block_impl_0构造函数中传入,通过冒号后的初始化列表对value参数进行初始化。

图片图片

后面传参和使用,就都是结构体赋值和取值逻辑,很简单。

图片图片

6.2值传递

下面这种写法,在block的使用中很容易踩坑。在block中使用value参数,并且打印value参数,发现结果为1,而不是2。

图片图片

通过C++源码我们可以看到,这是因为如果block引用的外部变量是值类型,会采取直接复制值的方式,而不是指针引用。

图片图片

想解决这个问题也很简单,通过__block修饰一下值类型,即可实现block内value的值和外部value参数统一。

图片图片

6.3静态变量

我们看一下,如果捕获的是一个static修饰的静态变量,其结构体会是什么实现。

图片图片

转换为C++代码后,可以看到原来的值传递变成了地址传递,__main_block_impl_0中value的引用是指针引用,在main函数中将value的地址传入。如果被static修饰的本身就是一个对象,对象是通过指针引用的,在block的结构体中就是两个星号引用。也就是NSObject **obj。

图片图片

正是由于静态变量地址传递的实现,在block内可以对静态变量直接进行更改,而无需用__block进行修饰。

图片图片

6.4全局变量

如果把value改为全局变量,结构体会有什么变化呢?

图片图片

因为全局变量的作用域很大,所以并不需要block进行单独持有即可访问,结构体并不会新增字段。

图片图片

6.5对象类型变量

如果block中引用的是对象,而不是基础数据类型,结构体会是什么定义呢?

图片图片

执行clang命令,执行完成后结构体是下图的,下面代码去掉了转换,以及整理过代码。可以看到多了两个函数指针,__main_block_copy_0和__main_block_dispose_0。

以copy的实现__main_block_copy_0为例,执行后会调用Block_object_assign的实现,在实现中系统会根据person的引用方式,__strong、__weak、__unsafe_unretained,是强引用还是弱引用,调用对应的内存管理方法。

__main_block_dispose_0函数在block从堆区移除的时候被调用,调用dispose时会调用实现Block_object_dispose函数,函数中会根据person的引用方式,进行对应的减少引用计数或释放操作。

copy和dispose两个函数都有一个3的参数,这个参数是一个标志位,表示外部变量类型。这里是BLOCK_FIELD_IS_OBJECT表示一个对象类型,也有BLOCK_FIELD_IS_WEAK表示weak引用的变量,BLOCK_FIELD_IS_BLOCK表示block类型的变量等。

图片图片


责任编辑:武晓燕 来源: 搜狐技术产品
相关推荐

2013-06-04 15:41:31

iOS开发移动开发block

2017-03-07 09:45:43

iOSBlock开发

2009-06-15 15:57:21

Spring工作原理

2023-06-07 15:25:19

Kafka版本日志

2013-07-19 12:52:50

iOS中BlockiOS开发学习

2024-02-27 22:31:00

Feign动态代理核心

2023-02-22 07:04:05

自动机原理优化实践

2011-08-08 18:11:45

IOS 4Block UIActionShe

2010-08-09 08:48:46

File APIWeb

2009-11-04 15:54:20

Portlet入门企业门户

2014-03-07 13:23:23

百度面试iOS

2010-02-26 17:54:54

python

2009-11-06 16:10:54

ClosureJavaScript开Google

2010-08-27 10:41:41

iPhone核心应用程序

2009-08-25 13:48:01

Java EE架构企业级应用

2013-07-19 14:00:13

iOS中BlockiOS开发学习

2013-07-19 14:35:59

iOS中BlockiOS开发学习

2011-09-01 10:42:14

Objective-CCocoa内存管理

2013-04-17 10:06:55

Google GlasMirror API

2011-06-28 10:42:38

Windows 8开发部门DevX
点赞
收藏

51CTO技术栈公众号