探索底层原理,积累从点滴做起。大家好,我是Mars。
往期回顾
iOS底层原理探索—OC对象的本质
iOS底层原理探索—class的本质
iOS底层原理探索—KVO的本质
iOS底层原理探索— KVC的本质
iOS底层原理探索— Category的本质(一)
iOS底层原理探索— Category的本质(二)
iOS底层原理探索— 关联对象的本质
今天继续带领大家探索iOS之block
的本质。
一、block底层结构分析
本文我们研究block
的底层原理,如果你对block
的基础掌握的不是很透彻的话,建议大家先去自行学习一些block
的基础,然后再来阅读本文。
首先我们在main
函数中声明一个block
并完成调用:
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^{
NSLog(@"Hello, World!");
};
block();
}
return 0;
}
然后我们通过clang
命令将main.m
转为c++
文件,来帮助我们查看block
内部结构:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
我们将生成的.cpp
文件拖入工程中并取消编译,将OC
代码跟转化完的c++
代码做对比:
通过对比我们发现,block
的声明和调用都在c++
文件一一对应显示。在c++
代码中,(void (*)()
表示强制转换,那么我们分析源码的时候为了便捷查看,我们将强制转换的代码暂时删掉:
下面我们根据上图简化后的c++
代码分别查看一下block
的定义和调用的内部源码实现。
1.定义block
在定义block
时调用了__main_block_impl_0
函数,并将__main_block_impl_0
函数的地址赋值给block
。我们来看__main_block_impl_0
函数内部结构:
我们看到,绿框标注的__main_block_imp_0
结构体内有一个同名函数__main_block_imp_0
,已用红框标注。这个同名的函数__main_block_imp_0
为构造函数
,构造函数
中对一些变量进行了赋值,类似于OC
的init
函数,构造函数
最终会返回一个结构体(此处对构造函数不做过多阐述,有疑问者可自行百度)。此处返回的结构体就是__main_block_imp_0
。
也就是说定义block
时调用&__main_block_imp_0
,最终会将__main_block_imp_0
结构体的地址赋值给了block
。
我们继续分析红框标注的__main_block_imp_0
函数。发现这个函数传入了三个参数,但是从上面简化后的c++
代码中可以看到只传入了两个参数而已,这是为什么呢?
这就涉及到c++
的语法,我们在上图中用黄色框标注的int flags=0
,此处flags
是直接赋值了,在c++
语法中如果flage
参数在调用的时候可以省略不传,也就是可以忽略这个参数。
接下来我们分析一下剩下的两个参数分别代表什么。
第一个参数:__main_block_func_0
我们直接查看__main_block_func_0
源码:
从源码中可以看到
NSLog
,那么我们可以分析出这个参数中封装的就是我们定义的
block
内部执行的逻辑。
也就是说把我们写在block
中的代码封装成__main_block_func_0
函数,并将__main_block_func_0
函数的地址作为参数传到__main_block_impl_0
的构造函数中保存在结构体内。
第二个参数:&__main_block_desc_0_DATA
从源码中看到
__main_block_desc_0
中存储着两个参数,
reserved
和
Block_size
,并且将
0
赋值给
reserved
,
Block_size
则存储着结构体
__main_block_impl_0
的占用空间大小。
最终再把__main_block_desc_0
结构体的地址作为第二个参数传入__main_block_impl_0
中。
我们在整体分析一下这段代码:
图中,红框标注的为第一个参数及代表的内容,黄框标注的为第二个参数及代表的内容。我们看到,在构造函数
__main_block_imp_0
中,把第一个参数以
fp
赋值给
impl.FuncPtr
,把第二个参数以
desc
赋值给
Desc
。
在构造函数__main_block_imp_0
中还有两句赋值代码,其中impl.isa = &_NSConcreteStackBlock;
这段代码含义是我们定义的block
类型为_NSConcreteStackBlock
。另外一句impl.Flags = flags;
赋值代码,由于上文我们解释flags
可忽略,所以这句代码我们也暂时忽略。
构造函数__main_block_imp_0
赋值完毕之后,返回一个结构体,即__main_block_imp_0
结构体,里面包含struct __block_impl impl;
和struct __main_block_desc_0* Desc;
经过分析,impl
中包含block
的类型和封装的内部执行逻辑,Desc
则包含block
所占用内存空间大小。
我们在进入struct __block_impl impl;
内部查看源码:
我们发现
__block_impl
结构体内部有一个
isa
指针。这一点说明
block
本质上是一个oc对象
。
用一张图来总结以上分析:
2.执行block
我们先看一下简化后的执行block
的代码:
调用
block
就是通过
block
找到
FuncPtr
直接调用。上文我们知道,
FuncPtr
是封装在
__block_impl
结构体中,而
block
指向的是
__main_block_impl_0
类型结构体,那它是如何找到
FuncPtr
的呢?我们来看一下没有简化的调用
block
的代码:
//调用block
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
可以看出,(__block_impl *)block
将block
强制转换为__block_impl
类型的,然后拿到FuncPtr
。因为__block_impl
是__main_block_impl_0
结构体的第一个成员,相当于将__block_impl
结构体的成员直接拿出来放在__main_block_impl_0
中,那么也就说明__block_impl
的内存地址就是__main_block_impl_0
结构体的内存地址开头。所以可以强制转换成功,也可以找到FunPtr
。
ok,至此我们理清了block
的底层结构,也可以总结出:
block
本质上是一个OC对象
,其内部也有一个isa
指针。
block
就是封装了函数调用以及函数调用环境的OC对象
。
二、block的变量捕获机制
分析完block
的底层结构,我们继续探索block
的其他内容。
为了保证block
内部能够正常访问外部的变量,block
有一个变量捕获机制。下面我们定义block
,通过其源码来分析block
的变量捕获机制。
代码中,声明了局部变量
a = 10
、
b = 20
,声明带两个
int
类型参数的
block
,
block
内执行打印传入的参数和局部变量
a
、
b
的值,然后调用
block
。
我们发现在c++
代码中,定义block
时,后面多了a
、b
两个参数。
然后我们进入__main_block_impl_0
结构体查看具体内容:
我们看到,
__main_block_impl_0
结构体中多了两个变量,即我们之前声明的
a
和
b
两个变量,同时我们还看到在
__main_block_impl_0
构造函数中红色标注的代码
a(_a), b(_b)
。这也是
c++
的语法,代表了将传入的参数
_a
赋值给
a
,将传入的参数
_b
赋值给
b
。然后保存在
__main_block_impl_0
结构体中
然后我们进入__main_block_func_0
函数中查看:
通过分析代码我们可以得知,
__main_block_func_0
函数会从
__main_block_impl_0
结构体取出
a
、
b
的值,完成
NSLog
打印输出。
通过以上分析,我们可以总结出:在block
的底层结构体中,会将捕获的变量存放起来,保存变量的值。当block
内部封装的执行函数需要调用变量时,会从结构体中取出变量的值,完成调用。
我们再用一张图片简单展示一下block
的底层结构:
下面我们继续研究如果block
捕获不同变量会产生什么效果
代码中我们分别声明了局部变量
a
和
b
以及全局变量
c
和
d
,其中
a
为自动局部变量(
auto
修饰,
int a
就是声明了自动局部变量,
auto
可不写),
b
为静态局部变量。在定义完block后分别修改四个变量的值,然后调用
block
。通过打印发现,只有自动局部变量
a
的值没有变,其他三个变量的值均发生了改变,我们进入底层查看
block
对这四个变量都做了什么:
首先是
main
函数的代码:
我们发现,在
c++
的代码中,定义
block
时自动局部变量
a
依旧是捕获其值作为参数,静态局部变量
b
则是捕获其地址作为参数传入,在看
__main_block_impl_0
结构体中:
自动局部变量
a
依旧是值存储,而静态局部变量
b
则是以指针形式存储。
在看一下调用执行封装函数的代码:
自动局部变量
a
依旧是传递其值进行调用,而静态局部变量
b
则是传递其指针进行调用。
我们可以得出结论:
block
对自动局部变量会进行捕获,访问形式是值传递
block
对静态局部变量会进行捕获,访问形式是指针传递
原因很简单,因为自动变量随时可能会被销毁,block
在执行的时候有可能自动变量已经被销毁了,那么此时如果再去访问被销毁的地址肯定会发生坏内存访问,因此对于自动变量一定是值传递而不可能是指针传递了。而静态局部变量不会被销毁,所以完全可以传递地址。
在block
调用之前静态局部变量的值发生改变,但静态局部变量的地址没有发生改变,对应block
中的捕获的地址不变,所以静态局部变量会随之改变。对应b
打印出的值就发生了改变。
同时我们从代码中发现,每段代码中都没有全局变量c
和d
,也就是说block
不会捕获全局变量,因为全局变量无论在哪里都可以访问。
我们可以得出结论:
block
对全局变量不会进行捕获,访问形式是直接访问
三、block的类型
1.block的三种类型
block
有三种类型,都继承自NSBlock
类型。我们可以通过class
方法或者isa
指针查看其具体类型。
__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )
__NSMallocBlock__ ( _NSConcreteMallocBlock )
下面通过代码来展示三种类型的block
的具体使用场景以及block
的继承关系:
block
的父类继承自
NSBlock
,而
NSBlock
继承自
NSObject
。这恰好也证明了我们之前说
block
是一个
OC对象
的结论。
block在内存中的存放区域
不同类型的block
在内存中存放的区域也不同。
__NSGlobalBlock__
类型的block
存放在数据段中,程序结束就会被回收。不过我们很少使用到__NSGlobalBlock__
类型的block
,因为这样使用block
并没有什么意义。
__NSStackBlock__
类型的block
存放在栈区中,栈区由系统自动分配和释放,作用域执行完毕之后就会被立即释放。
__NSMallocBlock__
类型的block
存放在堆区中,堆区需要我们自己进行内存管理。__NSMallocBlock__
类型的block
是我们平时经常使用的。
下面通过示意图总结一下:
那么系统是如何定义这三种类型呢?我们继续分析:
我们看到,存放在栈区的
__NSStackBlock__
类型的
block
调用
copy
就会成为
__NSMallocBlock__
类型,并且被复制存放在堆中。但是我们上文在测试代码中
block2
并没有调用
copy
,那它为什么会是
__NSMallocBlock__
类型呢?
原因就是在ARC环境下,编译器会根据情况自动将栈上的block
进行一次copy
操作,将block
复制到堆上。
我们从上面的图片中可以看到,访问了外部变量的block
是__NSStackBlock__
类型,存储在栈中。在实际编码过程中,存在这一种情况就是我们声明一个全局的block
,在某个函数中全局的block
访问了局部变量,此时block
存储在栈中。当函数执行完毕后,栈内存中block
所占用的内存已经被系统回收,这时我们在函数外调用block
时就会出现问题。
下面我们用代码来验证一下,首先要关闭ARC
,回到MRC
环境中:
然后是代码验证:
我们发现,打印的值是-272632616,说明就已经出现了问题。
那么其他类型的block
调用copy
操作会有什么效果呢:
ARC在一下四种情况下会对block
进行copy
操作:
block
作为函数返回值时- 将
block
赋值给__strong
指针时Cocoa API
中方法名含有usingBlock
的方法参数时,例如遍历数组的enumerateObjectsUsingBlock
方法GCD API
中block
为方法参数时
至此我们掌握了block
的底层结构、block
的变量捕获机制以及block
的类型,那么block
中对象类型变量的捕获是什么样的呢?当在block
中访问的变量为对象类型时,该变量什么时候才会被销毁?我们继续分析。
四、block中对象类型变量的捕获
首先我们写一段测试代码,实际查看一下。声明一个Person
类,内部声明age
属性,并且实现dealloc
方法,方法内打印Person -- dealloc
。然后我们创建一个block
,并在block
内部捕获person
的age
属性,查看Person
对象的dealloc
方法调用时机。
通过打印我们看到,大括号内创建对象和
block
内调用对象的代码执行完毕之后,
person
没有被释放。而是在
block
被销毁时,
peroson
才被释放。
这是因为
person
为
aotu变量
,传入
block
后,ARC自动对
block
调用
copy
操作,将
block
放入堆内存中,
block
内部会有一个强引用引用
person
对象,所以
block
不被销毁的话,
peroson
对象也不会销毁。
我们可以通过MRC环境来验证一下这一点。我们关掉ARC环境测试一下:
可以看到,在MRC环境下,即使
block
没有被释放,当
Person
对象调用
release
操作后,就会被释放。因为MRC环境下
block
在栈空间,栈空间对外面的
person
不会进行强引用。
如果我们对
block
进行
copy
操作后,
Person
对象依然是不会被释放的。
也就是堆空间的block
对person
对象进行了强引用,以保证person
对象不会被销毁。当block
自己释放之后也会对持有的person
对象进行release
释放操作。
通过源码确实可以证明堆空间的
block
对对象类型的变量进行强引用。
同时我们还发现
block
结构体
中__main_block_impl_0
的描述结构体
__main_block_desc_0
中多了两个参数:
copy
函数和
dispose
函数:
经过分析,
copy
函数和
dispose
函数中传入的都是
__main_block_impl_0
结构体本身。其中:
copy函数
copy
函数就是__main_block_copy_0
函数,__main_block_copy_0
函数内部调用_Block_object_assign
函数,并传入是person
对象的地址、person
对象以及8
三个参数。
当block
进行copy
操作时,内部就会自动调用__main_block_desc_0
内部的__main_block_copy_0
函数,__main_block_copy_0
函数内部又会调用_Block_object_assign
函数。
_Block_object_assign
函数会自动根据__main_block_impl_0
结构体内部的person
是什么类型的指针,对person
对象产生强引用或者弱引用。可以理解为_Block_object_assign
函数内部会对person
进行引用计数器的操作,如果__main_block_impl_0
结构体内person
指针是__strong
类型,则为强引用,引用计数+1,如果__main_block_impl_0
结构体内person
指针是__weak
类型,则为弱引用,引用计数不变。
dispose函数
dispose
函数就是__main_block_dispose_0
函数,__main_block_dispose_0
函数内部调用_Block_object_dispose
函数,传入person
对象以及8
两个参数。
当block
从堆中移除时就会自动调用__main_block_desc_0
中的__main_block_dispose_0
函数,__main_block_dispose_0
函数内部会调用_Block_object_dispose
函数。
_Block_object_dispose
会对person
对象进行释放,相当于release
操作,也就是放弃对person
对象的引用,而person
究竟是否被释放还是取决于person
对象自己的引用计数。
当我们用__weak
修饰block
捕获的对象类型变量时,内部结构唯一的变化就是__main_block_impl_0
结构体中对Person
对象引用是__weak
修饰的,也就是弱引用。其他代码没有变化。
那么我们就可以得出总结:
1.当
block
中捕获的变量为对象类型时,block
底层结构体中的__main_block_desc_0
会出两个参数copy
函数和dispose
函数,从而对内部引用的对象进行内存管理。
2.当
block
进行copy
操作,拷贝到堆区后,copy
函数会调用_Block_object_assign
函数,根据变量的修饰符(__strong
、__weak
、unsafe_unretained
)做出相应的操作,形成强引用或者弱引用
3.当
block
从堆中移除,dispose
函数会调用_Block_object_dispose
函数,自动释放引用的变量。
block
的底层探索暂时告一段落,下篇文章会继续为大家分析block
在使用过程中需要注意的问题。
更多技术知识请关注公众号
iOS进阶