学习MJ的视频课程,整理总结知识点–block详解
block是OC中实用频率很高的一个功能,同时在其它语言中也有相似的特性,比如swift中的闭包,Python中的闭包等。
本文先通过简单介绍block的使用,然后通过clang
重新编译导出block的一些源码来了解block的底层结构,随后会介绍block中的重要知识点1. block中的变量捕获,2. block的类型
block的使用
在OC中使用block,大致如下
1 | // 实现一个最简单的block |
我们要明白,哪些是block声明、哪些是实现、哪些是调用,使用上就不会弄混了。
block源码解析
下面我们通过一个Demo分析block的底层源码
思路是把包含block代码的文件通过clang命令重写导出,就可以看到block的源码了。
1 | int main(int argc, const char * argv[]) { |
通过clang
命令重写导出为cpp文件
1 | clang -rewrite-objc main.m -o main0.cpp |
我们观察生成的main0.cpp
文件中main函数部分,去掉里面不重要的类型转转换代码后,如下
1 | // 定义block变量 |
__main_block_impl_0
我们从定义block变量入手分析,我们查看__main_block_impl_0
,发现这一步是结构体有返回值,查看它的定义如下:
1 | struct __main_block_impl_0 { |
__main_block_impl_0
的内部有__block_impl
、__main_block_desc_0
、以及一个构造函数__main_block_impl_0
,类似OC的init方法,通过构造函数给结构体的变量赋值,最后返回结构体。
__block_impl
接下来分析__block_impl
,它的定义如下:
1 | struct __block_impl { |
__block_impl
是一个结构体,它的内部两个地方需要注意,void *isa
和void *FuncPtr
。isa很像之前学习的OC对象,我们知道OC对象中的isa是指向类的,我们可以提出一个疑问:block是OC对象吗,它的isa指向哪里。
另外,void *FuncPtr
是个指针,它指向哪里?我们在下面的文章中再讲解这两个问题。
__main_block_func_0
回到主题,已经明白了__main_block_func_0
的结构,那么接下来可以查看__main_block_func_0
的的接口,如下:
1 | // 封装了block执行逻辑的函数 |
这个__main_block_func_0
内部其实就是我们在OC中写的下面这段代码
1 | ^{ |
所以我们可以知道__main_block_func_0
实际上就是block内执行的函数代码。
还有一个地方就是__main_block_desc_0
,它的定义如下:
1 | static struct __main_block_desc_0 { |
从这里我们可以了解到,__main_block_desc_0_DATA
是获取block结构体的大小,其中reserved
是扩展保留字。
我们回到__main_block_impl_0
的构造函数,我们发现FuncPtr
是指向__main_block_func_0
,也是就block的执行逻辑的函数,故可以理解为FuncPtr
指向block的执行函数地址。
通过block的源码学习我们可以得知:
block本质是结构体,且有isa指针(实际上block本质也是一个OC对象)__main_block_impl_0
是block的结构体的主要信息
根据block的底层源码学习,可以总结更直观的的图示,如下
block变量捕获
通过上面的雨那么分析,我们了解了block的底层结构,下面我们分析一下block中经常用到的变量捕获,同样使用上面的Demo进行分析
当在block内访问外部变量时,可能会触发变量捕获。
1 | int age = 10; |
根据经验我们知道,当调用block时,输出的结果为age is 10, height is 20
,下面我们分析为什么会是这样,并总结规律。
首先,我们对这部分代码用clang重写导出为cpp
文件,观察源码中block相关的部分,我们发现
1 | struct __main_block_impl_0 { |
我们发现,结构体__main_block_impl_0
里面多了两个字段int age
、int *height
,其中age
是数值型,height
是指针型。
我们分析调用顺序
1 | int age = 10; |
可以发现age
的值,直接被传递到__main_block_impl_0
结构体中,这个结构体的构造函数会保存这个age的值,而height
是指针传递,结构体中也有对应的height
,但是是一个指针类型的。所以,结构体内部仅持有static类型变量的指针,当外接height
的值发生改变时,block内获取到的height也会相应改变。
在block结构体内部创建age
、height
保存访问的外部变量的过程,我们称之为变量捕获
。变量捕获是为了保证block内部能正常访问外部变量。
什么情况不需要变量捕获呢,其实我们可以推断一下,变量捕获的目的是为了保证block内部能正常访问外部的变量。假如访问的外部变量是一个全局变量,那么block随时都可以访问到这个全局变量,此时block就不需要捕获,而是直接访问这个全局变量。
总结:
局部变量,block要访问的话就会捕获
全局变量,不会捕获,直接访问
还有一种比较常见的场景,比如在一个方法中,block访问self的属性
1 | @implementation MJPerson |
此时block是够会捕获self变量或者self.name变量呢,答案是会,我们可以查看clang导出的对应源码,如下
1 | struct __MJPerson__test_block_impl_0 { |
看到确实是捕获了,而且捕获的是self这个变量。我们可以分析一下原因:
以为self是test
函数的默认参数(self和cmd),而self又不是全局变量,根据上面的总结,block访问局部变量都会进行捕获,而要访问的name
属于self的对象的一个属性,所以block就不捕获self,通过self访问其中的name
。
block类型
我们知道block有isa,那么这个isa是否来自NSObject
,block的isa是否代表它是对象类型的结构
可以先假设block是对象类型,那么调用[block class]
时,必然会返回对应的类型,通过下面这段代码进行测试
1 | void (^block)(void) = ^{ |
通过上面代码可以确认block确实是有class的,isa指针是来自NSObject
。我们可以进一步总结:
- block本质上也是一个OC对象,它内部也有个isa指针
- block是封装了函数调用以及函数调用环境的OC对象
实际上,block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock
NSGlobalBlock ( NSConcreteGlobalBlock )
_NSStackBlock ( NSConcreteStackBlock )
_NSMallocBlock ( _NSConcreteMallocBlock )
1 | // 堆:动态分配内存,需要程序员申请申请,也需要程序员自己管理内存 |
我们现在知道了block有3种类型,那么它们的区别是什么?
其实从名字分析,我们就看会发现一些特点Global
,Stack
,Malloc
,实际上它们对应是在内存中的区域。如下
程序区域:一般存放的是函数
数据区域:一般存放的是全局变量
堆区:动态分配内存,比如我们alloc出来的对象,需要开发者申请,释放对应的内存
栈区:自动分配内存局,系统自动管理,存放局部变量,函数中变量等
block存储在不同的内存区域,是根据什么来划分的?我们先给出总结的结果
block类型 | 环境 |
---|---|
NSGlobalBlock | 没有访问auto变量 |
NSStackBlock | 访问了auto变量 |
NSMallocBlock | NSStackBlock调用了copy |
实际上我们现在在项目中很少遇到
__NSStackBlock__
类型的block,因为在ARC环境下,__NSStackBlock__
类型的block在使用的是否,系统会自动把它copy到堆上去,成为__NSMallocBlock__
类型,所以我们在测试的时候,需要把ARC环境关闭(Automatic Reference Counting改为NO)才能验证
copy到堆上是为了不让程序自动管理,交由开发者管理它的释放时机
每一种类型的block调用copy后的结果如下所示
关于判断存储的内存区域,我们有一个简单的方法,就是看内存地址和哪一个已知区域的地址接近
1 | int age = 10; |
总结
根据本节所学的block知识,我们可以总结一下
- block本质上也是一个OC对象,它内部也有个isa指针
- block是封装了函数调用以及函数调用环境的OC对象
- block的底层结构如右图所示
auto变量的捕获
block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型NSGlobalBlock ( NSConcreteGlobalBlock )_NSStackBlock ( NSConcreteStackBlock )_NSMallocBlock ( _NSConcreteMallocBlock )
block类型 | 环境 |
---|---|
NSGlobalBlock | 没有访问auto变量 |
NSStackBlock | 访问了auto变量 |
NSMallocBlock | NSStackBlock调用了copy |
每一种类型的block调用copy后的结果如下所示
参考和源码
Demo源码:
Block底层-Demo