iOS开发Block底层原理 - 捕获机制,循环引用梳理

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局部变量、全局变量

对于auto和static类型的变量,全局变量的捕获,可以通过OC代码转化为C/C++代码查看,使用终端cd到当前文件夹下,输入:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件

注意:1.输出的文件必须以.cpp后缀命名 2. 必须cd到当前文件夹或者指定文件夹,不然生成的.cpp文件在用户目录下(或者通过在finder中全局搜索可以找到)

由上图代码生成的C++代码

由生成的c++代码可知:

摘自小码哥课件

1、auto变量a会直接将值捕获进去,用一个新的a成员变量接收并保存,所以捕获以后,block外部的a与内部的a为两个不同的值,互不干扰,修改一个的值不会影响另一个的值;

2、static变量捕获的是*b,捕获的是指针,内部与外部是同一个指针,修改一个另一个也会改变;

3、全局变量不会被block捕获,而是在调用函数的时候直接访问。

由此很好的解释了一个面试经常会问到的问题和答案:

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

NSMallocBlock和NSStackBlock

在ARC环境下,编译器会自动将block执行copy操作复制到堆上,例如:(block需要被保留后面调用,因为栈上的的block出作用域就会被销毁)(1)block作为函数的返回值时;(2)GCD中的block;(3)将block赋值给__strong强指针时;(4)block作为Cocoa API中方法名含有usingBlock的方法参数时。

arc下栈上的block会被copy到堆上(摘自小码哥课件)

1.1.3 block内部实现及调用c/c++分析

block对person对象捕获代码

1.1.3.1  main函数底层c/c++代码实现:

main函数底层c/c++代码
main函数简化后

对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底层实现及简化

block底层c/c++实现
blcok简化及内部结构体(构造函数)

c++构造函数:对于block的内部实现,__main_block_impl_0,初始化使用到了c++结构体及构造函数,查阅后了解到其语法及实现:

c++构造函数

1.1.3.3 blcok赋值以及调用

main函数以及block impl、func联系

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变量的捕获

peson对象的捕获流程

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 循环引用的产生和解决

block捕获self示例

1、oc方法在生成c++代码以后可以发现,oc方法都有两个隐式参数:self和_cmd,在调用self.a等方法的时候,使用的就是这个隐式的参数self;

2、所以在oc方法中,self是一个参数,也就是一个局部变量,由oc的捕获机制,局部变量self会被捕获到block内部;

3、self被捕获到block内部,会对self产生强引用,而self对block也是强引用,就会造成循环引用

如何解决循环引用问题,需要打破双方之间的强引用关系,改用弱引用:

如何解决循环引用(摘自小码哥课件)

4、使用__block修饰解决循环引用问题:必须要调用block。

你可能感兴趣的:(iOS开发Block底层原理 - 捕获机制,循环引用梳理)