入门分析
在iOS开发的过程中免不了会用到block,今天来探究一下其底层结构和原理,接下来先上一段简单代码
使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m来将main.m文件转化为C++文件来一探究竟,由于该文件很大,故只会挑选关键内容来分析
首先找到main函数
可以看到最后一句就是我们调用block,由于其中包含很多强制转换,故简化后可看作
block->FuncPtr(block),是这样来调用的,而我们整体的block的结构是__main_block_impl_0的结构体,其中有三个结构体成员分别为:
__block_impl类型的:impl__main_block_desc_0类型的:Descint类型的:age
__block_impl结构体如下:
可以看到其中有个成员变量FuncPtr是一个指针类型的成员变量
__main_block_desc_0结构体如下:
这里最主要的是记录block所需要的内存大小
重新看回到main函数中的第一句代码
开始调用了__main_block_impl_0结构体的构造函数来创建block,同时看到传入的第一个参数__main_block_func_0是一个函数指针
在构造函数中将这个函数指针赋予到FuncPtr中,所以之后的调用会是block->FuncPtr(block)如此调用。
结构
将上面的解释总结一下,大概能得到的一副这样的示意图
block的本质
block的本质也是一个OC对象,因为它内部也有一个isa指针block是封装了函数调用及函数调用环境的OC对象
block底层结构如下图所示:
block的变量捕获
auto 局部变量的捕获
先来看这样一段代码,大家应该都知道答案是20,同样为了探索为什么是这样,使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m来查看
可以看到在__main_block_impl_0的构造函数中,将外部传入的age变量赋值给了结构体中的age成员变量
Tip
C++语法中构造函数会使用默认赋值,例如:__main_block_impl_0(void *fp, struct _main_block_desc_0 *desc, int _age, int flags=0) : age(_age),就是将外部传入的参数 _age 赋值给成员变量 age
static 局部变量的捕获
如果是这样,答案又该是多少呢?
同样使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m来查看
对比之前发现了略有不同,之前是直接把值传入了,这里传入了地址,接着往下看
可以看到这时候都是传递的指针,这个指针指向了age的地址,那么答案就应该是25了
全局变量的捕获
使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m来查看
可以看到这时候并未进行捕获
总结
| 变量类型 | 捕获到block内部 | 访问方式 |
|---|---|---|
| auto 局部变量 | ✅ | 值传递 |
| static 局部变量 | ✅ | 指针传递 |
| 全局变量 | ❌ | 直接访问 |
block的三种类型
block的copy
在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,如以下情况:
block作为函数返回值时- 将
block赋值给__strong指针时 block作为Cocoa API中方法名含有usingBlock的方法参数时(ex:NSArray的sort方法)block作为GCDAPI的方法参数时
对象类型的auto变量
从开始研究的变量一直是基本数据类型,现在如果外部对象是对象类型的话会有什么变化呢
可以看到__main_block_desc_0的内部多了copy和dispose,这里就是方便在block对象被从栈拷贝到堆上时把外部变量需要使用_Block_object_assign对外部对象进行retain操作
那么如果是弱引用会是怎样呢?
同样使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m,但是这时候会发现爆出一个错误__weak是要使用runtime才可以的,所以这里要加新的参数进去来指定使用的runtime版本
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 main.m命令变成这样即可
总结
当block内部访问了对象类型的auto变量时:
-
若
block在栈上,不会对auto变量产生强引用 -
若
block被拷贝到堆上- 会调用
block内部的copy函数 - copy函数内部会调用
_Block_object_assign函数 _Block_object_assign函数会根据变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,类似于retain
- 会调用
-
若
block从堆上移除- 会调用
block内部的dispose函数 dispose函数会调用_Block_object_dispose_Block_object_dispose会自动释放引用的auto变量,类似于release
- 会调用
__block修饰符
我们都知道block中若想修改捕获的外部变量并不能直接修改,直接修改会产生编译报错,例如:
那么到底为什么不能修改呢?还是看到之前分析的部分
可以看到age变量的作用域在main函数中,而改变age的值发生在__main_block_func_0函数中,并且在此函数中使用的age也并非变量age而是block记录的变量,所以我们想到用static修饰的局部变量是可以的,当然使用全局变量也可以,但是有个弊端这个变量会一直存放于内存中,所以引入了__block修饰符,我们来看一下,__block究竟做了什么
可以看到这里将age包装成了一个__Block_byref_age_0类型的对象,这个结构基本如下
__block的内存管理
- 当
block在栈上时,不会对__block变量产生强引用 - 当
block被拷贝到堆上时-
会调用
block内部的copy函数 -
copy函数内部会调用_Block_object_assign函数会对__block变量形成强引用
-
- 当
block从堆中移除时- 会调用
block内部dispose函数 dispose函数内部调用_Block_object_dispose函数_Block_object_dispose函数会自动释放引用的__block变量
- 会调用
对象类型的局部变量&__block变量
- 当
block在栈上时,对它们都不会产生强引用 - 当
block拷贝到堆上时,都会通过copy函数来处理 _Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用或者弱引用(仅有ARC时会retain,MRC时不会)- 当
block从堆上移除时,都会通过dispose函数来释放
__block中的__forwarding指针
从之前的分析中能看到使用__block修饰的变量会被包装成一个新的对象,其中有一个__forwarding指针,当时比较奇怪为什么要访问外部变量时要通过这个指针来取得,主要原因是当block拷贝到堆上后确保__forwarding指针都访问的对象的确在堆上。
解决循环引用
循环引用的问题经常出现于使用block的场景,其本质就是互相持有导致被持有的对象都无法正常释放,接下来会分为ARC环境下和MRC环境下的解决
ARC
ARC环境下有两种解决方案:
__weak、__unsafe_unretained
- 使用
__block解决(缺点:必须要调用block)
这个原理也很简单,因为__block修饰的变量生成的结构体会被block对象强引用,结构体对象同时会强引用self,self也同时强引用了block,形成三角关系的引用,最后的置空会打破这个三角关系,从而打破循环引用。
MRC
在MRC环境下时,__block修饰的对象类型不会在block拷贝到堆上时对外部变量进行强引用,那么根据这个规则,在MRC时打破循环引用使用__unsafe_unretained和__block。