Block底层原理 - 捕获机制,循环引用梳理
前段时间通过学习小马哥视频,对block有了深刻的理解和学习,对此总结一下,加深印象。
1.1 捕获机制
在Object-C中,Block的本质是一个封装了函数调用和函数调用环境的OC对象(内部有isa指针):
函数调用:block内部将要执行的代码
函数调用环境:传进block中的参数和在内部调用时需要访问的外部变量(捕获机制原因)
但是:绝大部分情况Block创建后,并不会立即调用,而是在其它函数或者合适的时机调用,而block外部的变量有可能已经被销毁;
这时候例如局部变量a已经被销毁,a被销毁后,调用block时内部需要访问a的值,而打印出来a的值为5,证明:Block内部将外部变量捕获进内部,在调用block函数时使用
1.1.1 局部变量与全局变量
auto变量:自动变量,离开作用域就销毁。
对于auto和static类型的变量,全局变量的捕获,可以通过OC代码转化为C/C++代码查看,使用终端cd到当前文件夹下,输入:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件
注意:1.输出的文件必须以.cpp后缀命名 2. 必须cd到当前文件夹或者指定文件夹,不然生成的.cpp文件在用户目录下(或者通过在finder中全局搜索可以找到)
由生成的c++代码可知:
1、auto变量a会直接将值捕获进去,用一个新的a成员变量接收并保存,所以捕获以后,block外部的a与内部的a为两个不同的值,互不干扰,修改一个的值不会影响另一个的值;
2、static变量捕获的是*b,捕获的是指针,内部与外部是同一个指针,修改一个另一个也会改变;
3、全局变量不会被block捕获,而是在调用函数的时候直接访问。
由此很好的解释了一个面试经常会问到的问题和答案:
ps:通过加断点可以打印出外部的a和block内部的a地址值不一样,b、c内外的地址值一样。
1.1.2 Blcok的类型
block分为3中类型:__NSGlobalBlock__、__NSStackBlock__、__NSMallocBlock__,最终都继承自NSBlock ->NSObject。
1、__NSGlobalBlock__:没有访问外部auto变量;调用copy后什么也不做;
2、__NSStackBlock__:访问了外部的auto变量;调用copy后,block从栈区复制到堆区,类型变成__NSMallocBlock__;
3、__NSMallocBlock__:__NSStackBlock__调用copy后的类型;调用copy函数后引用计数+1
在ARC环境下,编译器会自动将block执行copy操作复制到堆上,例如:(block需要被保留后面调用,因为栈上的的block出作用域就会被销毁)(1)block作为函数的返回值时;(2)GCD中的block;(3)将block赋值给__strong强指针时;(4)block作为Cocoa API中方法名含有usingBlock的方法参数时。
1.1.3 block内部实现及调用c/c++分析
1.1.3.1 main函数底层c/c++代码实现:
对main函数简化后,总结:
1、创建person对象是对YYPerson类对象发送alloc和init两条消息,给age赋值是对person对象发送setAge消息;
2、block赋值实际上就是__main_block_impl_0结构体;
3、调用block,通过block找到FuncPtr(函数调用的地址),执行__main_block_func_0
1.1.3.2 block impl、func底层实现及简化
c++构造函数:对于block的内部实现,__main_block_impl_0,初始化使用到了c++结构体及构造函数,查阅后了解到其语法及实现:
1.1.3.3 blcok赋值以及调用
1、block赋值实际上就是__main_block_impl_0结构体,以及传入结构体的参数:__mian_block_func_0,person对象,__main_block_desc_0函数描述等;
2、调用block,通过block找到FuncPtr(函数调用的地址),执行流程:__main_block_impl_0 -> impl -> FuncPtr -> __main_block_func_0 ->调用。
3、FuncPtr:__main_block_impl_0赋值时传入的第一个参数是__main_block_func_0,而在构造函数赋值时第一个参数是void *fp,即说明__func_block_func_0就是fp,而后fp被赋值给了(__block_impl)impl(block对象)的FuncPtr;
1.1.3.4 对象类型的auto变量的捕获
1、当block存储在栈上:block随时会被销毁,所以不会对auto对象进行强引用;
2、如果block被拷贝到堆上:会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用;
3、当堆上的block被移除时:调用block内部的dispose函数,dispose函数会调用_block_object_dispose函数,_block_object_dispose函数会自动释放引用的auto对象
1.2 循环引用的产生和解决
1、oc方法在生成c++代码以后可以发现,oc方法都有两个隐式参数:self和_cmd,在调用self.a等方法的时候,使用的就是这个隐式的参数self;
2、所以在oc方法中,self是一个参数,也就是一个局部变量,由oc的捕获机制,局部变量self会被捕获到block内部;
3、self被捕获到block内部,会对self产生强引用,而self对block也是强引用,就会造成循环引用
如何解决循环引用问题,需要打破双方之间的强引用关系,改用弱引用:
4、使用__block修饰解决循环引用问题:必须要调用block。