学习MJ的视频课程,整理总结知识点–block的copy、访问对象类型变量的底层实现
本文结合block的特性,探讨ARC下block自动copy的时机;以及block内访问外部的对象类型变量时底层实现;
block的copy
在上一节的block学习中我们遇到,在ARC环境下,block有时会被自动拷贝到堆空间去,我们可以分析一下原因。
常见的场景就是block里面访问一个局部变量,根据上一节所学,MRC下访问auto变量的block是存储在栈上的,但是我们在ARC环境下演练测试发现它是存储在堆空间上。因为栈空间内存是系统自动做处理的,堆空间才是开发者手动控制。如果让block保留在栈空间,很可能当我们调用block的时候,它已经被系统自动回收了,肯定不是我们想要的结果,所以在ARC下block会被自动copy到堆空间上,有开发者控制它的释放时机(不过这些工作实际是由ARC机制处理了)
ARC下block进行自动copy的根本目的是为了防止block在栈空间上被不合时宜的自动释放,而此时调用被释放的block就会出错,而copy到堆空间就不会自动释放。
从这个根本原因出发,我们可以总结一下在ARC环境下,block在哪些场景会被自动copy到堆上。首先,我们知道需要copy操作的一般是NSStackBlock
类型的block,而NSStackBlock类型的block的产生是因为block访问了auto变量。
所以我们可以推出一个关键点:在ARC下,访问了auto变量的block,默认都会进行copy操作,防止它在栈上被自动释放。包括以下几种:
- block作为函数返回值时(
Masonry
中) - 将block赋值给__strong指针时
- block作为一些函数的参数时(cocoa API useringBlock参数)、(GCD API block参数)
对象类型的auto变量
在之前的测试中,我们更多的是用block访问基本类型的auto变量,现在我们研究block访问对象类型的auto变量的底层。
参见Demo,新建一个自定义类,研究它的实例对象释放时机
1 | int main(int argc, const char * argv[]) { |
默认情况下,person对象在出了大括号后,就会调用自己的dealloc方法进行释放。
当我们用一个block访问person的属性时
1 | MJBlock block; |
此时发现person出了大括号的作用域,没有调用dealloc,直到block销毁时,person才销毁。
我们简化一下代码如下
1 | MJBlock block; |
我们研究此时的block源码,分析为什么会是这样。clang编译后的block源码如下
1 | struct __main_block_impl_0 { |
block结构体里面有person的指针,而block在堆空间,在ARC下,block会强引用着person,所以当block销毁时person才会销毁。
我们还可以验证在MRC环境下person的释放时机,此时block是在栈空间的。会发现在MRC下,person出了自己的大括号作用域后,就销毁了。
我们可与得出一个结论:栈空间上的block是不会对对象类型的auto变量强引用,堆空间上的block会对对象类型的auto变量强引用。
我们再使用weak类型的person实验一下此时person的释放时机
1 | MJBlock block; |
会发现person在出了自己的大括号作用域后就销毁了,说明此时block是没有强引用着person对象,我们看下使用__weak
时的block源码
此时用clang命令会报错cannot create __weak reference because the current deployment target does
因为__weak是属于运行时的特性,clang默认是编译期,我们要指定clang的runtime环境才能导出源文件。
修改clang命令如下:
1 | clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-9.0.0 main.m -o main2.cpp |
我们看此时的block源码如下
1 | struct __main_block_impl_0 { |
会发现block里面的person对象是MJPerson *__weak weakPerson;
类型,从字面意思上理解就是block不强引用这个person对象了,这也就解释了为什么__weak的person对象在出了自己的大括号作用域后就销毁了,因为没有别的对象对它强引用了。
我们再看下此时block的desc结构体
1 | static struct __main_block_desc_0 { |
会发现此时的__main_block_desc_0结构体,相对于访问基本类型的auto变量时多了两个函数指针void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); void (*dispose)(struct __main_block_impl_0*);
而这两个函数的实现如下
1 | static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { |
说明当BLOCK_FIELD_IS_OBJECT
时,会多两个函数实现,分别是_Block_object_assign
、_Block_object_dispose
。
实际上_Block_object_assign
就是block决定是否引用person对象的关键,当block内部的person是MJPerson *__weak weakPerson;
,_Block_object_assign
就也是用__weak关联这个person
小结
关于block访问对象类型的auto变量,做一个总结
如果block是在栈上,将不会对auto变量产生强引用
如果block被拷贝到堆上
会调用block内部的copy函数
copy函数内部会调用_Block_object_assign函数
_Block_object_assign函数会根据auto变量的修饰符(strong、weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用如果block从堆上移除
会调用block内部的dispose函数
dispose函数内部会调用_Block_object_dispose函数
_Block_object_dispose函数会自动释放引用的auto变量(release)
参考和源码
Demo源码:
Block底层-Demo