探索底层原理,积累从点滴做起。大家好,我是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++
代码做对比:
![](https://img.haomeiwen.com/i1760191/a73e4bd997db86c2.png)
通过对比我们发现,block
的声明和调用都在c++
文件一一对应显示。在c++
代码中,(void (*)()
表示强制转换,那么我们分析源码的时候为了便捷查看,我们将强制转换的代码暂时删掉:
![](https://img.haomeiwen.com/i1760191/d7f0addb4dfb2846.png)
下面我们根据上图简化后的c++
代码分别查看一下block
的定义和调用的内部源码实现。
1.定义block
在定义block
时调用了__main_block_impl_0
函数,并将__main_block_impl_0
函数的地址赋值给block
。我们来看__main_block_impl_0
函数内部结构:
![](https://img.haomeiwen.com/i1760191/def4b7ad22aa9c43.png)
我们看到,绿框标注的__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
源码:
![](https://img.haomeiwen.com/i1760191/4f7c0c40aebf9564.png)
从源码中可以看到
NSLog
,那么我们可以分析出这个参数中封装的就是我们定义的block
内部执行的逻辑。
也就是说把我们写在block
中的代码封装成__main_block_func_0
函数,并将__main_block_func_0
函数的地址作为参数传到__main_block_impl_0
的构造函数中保存在结构体内。
第二个参数:&__main_block_desc_0_DATA
![](https://img.haomeiwen.com/i1760191/879c036c222659ee.png)
从源码中看到
__main_block_desc_0
中存储着两个参数,reserved
和Block_size
,并且将0
赋值给reserved
,Block_size
则存储着__main_block_impl_0
的占用空间大小。
最终再把__main_block_desc_0
结构体的地址作为第二个参数传入__main_block_func_0
中。
我们在整体分析一下这段代码:
![](https://img.haomeiwen.com/i1760191/bf8145ad1758942a.png)
图中,红框标注的为第一个参数及代表的内容,黄框标注的为第二个参数及代表的内容。我们看到,在构造函数
__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;
内部查看源码:
![](https://img.haomeiwen.com/i1760191/62b48bb86149a622.png)
我们发现
__block_impl
结构体内部有一个isa
指针。这一点说明
block
本质上是一个oc对象
。
用一张图来总结以上分析:
![](https://img.haomeiwen.com/i1760191/71e4eceb107f6940.png)
2.执行block
我们先看一下简化后的执行block
的代码:
![](https://img.haomeiwen.com/i1760191/3e9219fe0b473ad5.png)
调用
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
的变量捕获机制。
![](https://img.haomeiwen.com/i1760191/56f0c6cf5295316e.png)
代码中,声明了局部变量
a = 10
、b = 20
,声明带两个int
类型参数的block
,block
内执行打印传入的参数和局部变量a
、b
的值,然后调用block
。
我们发现在c++
代码中,定义block
时,后面多了a
、b
两个参数。
然后我们进入__main_block_impl_0
结构体查看具体内容:
![](https://img.haomeiwen.com/i1760191/ed425b5aab6ab57c.png)
我们看到,
__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
函数中查看:
![](https://img.haomeiwen.com/i1760191/bd00423339a84a01.png)
通过分析代码我们可以得知,
__main_block_func_0
函数会从__main_block_impl_0
结构体取出a
、b
的值,完成NSLog
打印输出。
通过以上分析,我们可以总结出:在block
的底层结构体中,会将捕获的变量存放起来,保存变量的值。当block
内部封装的执行函数需要调用变量时,会从结构体中取出变量的值,完成调用。
我们再用一张图片简单展示一下block
的底层结构:
![](https://img.haomeiwen.com/i1760191/021a5bf523fa92d4.png)
下面我们继续研究如果block
捕获不同变量会产生什么效果
![](https://img.haomeiwen.com/i1760191/929ec70e40af952c.png)
代码中我们分别声明了局部变量
a
和b
以及全局变量c
和d
,其中a
为自动局部变量(auto
修饰,int a
就是声明了自动局部变量,auto
可不写),b
为静态局部变量。在定义完block后分别修改四个变量的值,然后调用block
。通过打印发现,只有自动局部变量a
的值没有变,其他三个变量的值均发生了改变,我们进入底层查看block
对这四个变量都做了什么:首先是
main
函数的代码:![](https://img.haomeiwen.com/i1760191/6a96b07d92b66e92.png)
我们发现,在
c++
的代码中,定义block
时自动局部变量a
依旧是捕获其值作为参数,静态局部变量b
则是捕获其地址作为参数传入,在看__main_block_impl_0
结构体中:![](https://img.haomeiwen.com/i1760191/c400867fdaa7f6d0.png)
自动局部变量
a
依旧是值存储,而静态局部变量b
则是以指针形式存储。在看一下调用执行封装函数的代码:
![](https://img.haomeiwen.com/i1760191/02dff35b3c14caf2.png)
自动局部变量
a
依旧是传递其值进行调用,而静态局部变量b
则是传递其指针进行调用。
我们可以得出结论:
block
对自动局部变量会进行捕获,访问形式是值传递
block
对静态局部变量会进行捕获,访问形式是指针传递
原因很简单,因为自动变量随时可能会被销毁,block
在执行的时候有可能自动变量已经被销毁了,那么此时如果再去访问被销毁的地址肯定会发生坏内存访问,因此对于自动变量一定是值传递而不可能是指针传递了。而静态局部变量不会被销毁,所以完全可以传递地址。
在block
调用之前静态局部变量的值发生改变,但静态局部变量的地址没有发生改变,对应block
中的捕获的地址不变,所以静态局部变量会随之改变。对应b
打印出的值就发生了改变。
同时我们从代码中发现,每段代码中都没有全局变量c
和d
,也就是说block
不会捕获全局变量,因为全局变量无论在哪里都可以访问。
我们可以得出结论:
block
对全局变量不会进行捕获,访问形式是直接访问
![](https://img.haomeiwen.com/i1760191/8e4b0d6c65b83d28.png)
三、block的类型
1.block的三种类型
block
有三种类型,都继承自NSBlock
类型。我们可以通过class
方法或者isa
指针查看其具体类型。
__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )
__NSMallocBlock__ ( _NSConcreteMallocBlock )
下面通过代码来展示三种类型的block
的具体使用场景以及block
的继承关系:
![](https://img.haomeiwen.com/i1760191/a1a9a55fa04f8299.png)
block
的父类继承自NSBlock
,而NSBlock
继承自NSObject
。这恰好也证明了我们之前说block
是一个OC对象
的结论。
block在内存中的存放区域
不同类型的block
在内存中存放的区域也不同。
__NSGlobalBlock__
类型的block
存放在数据段中,程序结束就会被回收。不过我们很少使用到__NSGlobalBlock__
类型的block
,因为这样使用block
并没有什么意义。
__NSStackBlock__
类型的block
存放在栈区中,栈区由系统自动分配和释放,作用域执行完毕之后就会被立即释放。
__NSMallocBlock__
类型的block
存放在堆区中,堆区需要我们自己进行内存管理。__NSMallocBlock__
类型的block
是我们平时经常使用的。
下面通过示意图总结一下:
![](https://img.haomeiwen.com/i1760191/f6e35d3a5a8bf17b.png)
那么系统是如何定义这三种类型呢?我们继续分析:
![](https://img.haomeiwen.com/i1760191/11d062e9dcfe7c6a.png)
我们看到,存放在栈区的
__NSStackBlock__
类型的block
调用copy
就会成为__NSMallocBlock__
类型,并且被复制存放在堆中。但是我们上文在测试代码中block2
并没有调用copy
,那它为什么会是__NSMallocBlock__
类型呢?
原因就是在ARC环境下,编译器会根据情况自动将栈上的block
进行一次copy
操作,将block
复制到堆上。
我们从上面的图片中可以看到,访问了外部变量的block
是__NSStackBlock__
类型,存储在栈中。在实际编码过程中,存在这一种情况就是我们声明一个全局的block
,在某个函数中全局的block
访问了局部变量,此时block
存储在栈中。当函数执行完毕后,栈内存中block
所占用的内存已经被系统回收,这时我们在函数外调用block
时就会出现问题。
下面我们用代码来验证一下,首先要关闭ARC
,回到MRC
环境中:
![](https://img.haomeiwen.com/i1760191/d390a835b6d84b22.png)
然后是代码验证:
![](https://img.haomeiwen.com/i1760191/50d67b7a21f5fb67.png)
我们发现,打印的值是-272632616,说明就已经出现了问题。
那么其他类型的block
调用copy
操作会有什么效果呢:
![](https://img.haomeiwen.com/i1760191/4bf381e5f5d25e38.png)
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
方法调用时机。
![](https://img.haomeiwen.com/i1760191/6f53d7ff17ce00e0.png)
通过打印我们看到,大括号内创建对象和
block
内调用对象的代码执行完毕之后,person
没有被释放。而是在block
被销毁时,peroson
才被释放。这是因为
person
为aotu变量
,传入block
后,ARC自动对block
调用copy
操作,将block
放入堆内存中,block
内部会有一个强引用引用person
对象,所以block
不被销毁的话,peroson
对象也不会销毁。我们可以通过MRC环境来验证一下这一点。我们关掉ARC环境测试一下:
![](https://img.haomeiwen.com/i1760191/cc0b399cd298e6d3.png)
可以看到,在MRC环境下,即使
block
没有被释放,当Person
对象调用release
操作后,就会被释放。因为MRC环境下block
在栈空间,栈空间对外面的person
不会进行强引用。如果我们对
block
进行copy
操作后,Person
对象依然是不会被释放的。
也就是堆空间的block
对person
对象进行了强引用,以保证person
对象不会被销毁。当block
自己释放之后也会对持有的person
对象进行release
释放操作。
![](https://img.haomeiwen.com/i1760191/679e6460de5059eb.png)
通过源码确实可以证明堆空间的
block
对对象类型的变量进行强引用。同时我们还发现
block
结构体中__main_block_impl_0
的描述结构体__main_block_desc_0
中多了两个参数:copy
函数和dispose
函数:![](https://img.haomeiwen.com/i1760191/66648e4956f015a9.png)
经过分析,
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进阶
iOS进阶.jpg
网友评论