Block的使用
是什么?
块,封装了函数调用以及调用环境的OC对象
Block底层数据结构
Block本质上也是一个OC对象,它内部也有个isa
指针
Block是封装了函数调用以及调用环境的OC对象
Block底层数据结构如下图所示:
Block变量捕获机制
为了保证block内部能够正常访问外部变量,block有个变量捕获机制。
- 对于全局变量,不会捕获到block内部,访问方式为直接访问
- 对于auto类型的局部变量,会捕获到block内部,block内部会自动生成一个成员变量,用来存储这个变量的值,访问方式为值传递。
- 对于static类型的局部变量,会捕获到block内部,block内部会自动生成一个成员变量,用来存储这个变量的地址,访问方式为指针传递
- 对于对象类型的局部变量,block会连同它的所有权修饰符一起捕获
为什么局部变量需要捕获,全局变量不用捕获呢?
- 作用域的原因,全局变量哪里都可以直接访问,所有不用捕获
- 局部变量,外部不能直接访问,所以需要捕获
- auto类型的局部变量可能会被销毁,其内存会消失,block将来执行代码的时候不可能再去访问那块内存,所以捕获其值
- static变量会一直保存在内存中,所以捕获其地址即可
对象类型的auto变量
当block内部访问了对象类型的auto变量时
如果block是在堆上,将不会对auto变量产生强引用
如果block被拷贝到堆上
① block 内部的 desc 结构体会新增两个函数:
copy
(__main_block_copy_0
,函数名命名规范同__main_block_impl_0
)
dispose
(__main_block_dispose_0)
② 会调用 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)
__block
__block修饰符
__block
可以用于解决 block 内部无法修改 auto 变量值的问题;__block
不能修饰全局变量、静态变量;- 编译器会将
__block
变量包装成一个对象(struct __Block_byref_age_0
(byref:按地址传递)); - 加
__block
修饰不会修改变量的性质,它还是 auto 变量; - 一般情况下,对被捕获变量进行赋值(赋值!=使用)操作需要添加
__block
修饰符。比如给数组添加或者删除对象,就不用加__block
修饰; - 在 MRC 下使用
__block
修饰对象类型,在 block 内部不会对该对象进行 retain 操作,所以在 MRC 环境下可以通过__block
解决循环引用的问题。
__block的__forwarding指针
__block
的__forwarding
指针存在的意义?
为什么要通过 age 结构体里的__forwarding
指针拿到 age 变量的值,而不直接 age 结构体拿到 age 变量的值呢?
__block
的__forwarding
是指向自己本身的指针, 为了不论在任何内存位置,都可以顺利的访问同一个 __block
变量。
- block 对象 copy 到堆上时,内部的
__block
变量也会 copy 到堆上去。为了防止 age 的值赋值给栈上的__block
变量,就使用了__forwarding
; - 当
__block
变量在栈上的时候,__block
变量的结构体中的__forwarding
指针指向自己,这样通过__forwarding
取到结构体中的 age 给它赋值没有问题; - 当
__block
变量 copy 到堆上后,栈上的__forwarding
指针会指向 copy 到堆上的 _block 变量结构体,而堆上的__forwarding
指向自己;
这样不管我们访问的是栈上还是堆上的 __block
变量结构体,只要是通过__forwarding
指针访问,都是访问到堆上的 __block
变量结构体;给 age 赋值,就肯定会赋值给堆上的那个 __block
变量中的 age
对象类型的auto变量、__block变量内存管理区别
当 block 在栈上时,对它们都不会产生强引用
当 block 拷贝到堆上时,都会通过 copy 函数来处理它们
当 block 从堆上移除时,都会通过 dispose 函数来释放它们
被__block修饰的对象类型
- 当
__block
变量在栈上时,不会对指向的对象产生强引用 - 当
__block
变量被 copy 到堆时
①__Block_byref_object_0
即__block
变量内部会新增两个函数:
copy
(__Block_byref_id_object_copy)
dispose
(__Block_byref_id_object_dispose)
② 会调用__block
变量内部的copy
函数
③copy
函数内部会调用_Block_object_assign函数
④_Block_object_assign
函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于 ARC 时会 retain,MRC 时不会 retain) - 如果
__block
变量从堆上移除
① 会调用__block
变量内部的 dispose 函数
② dispose 函数内部会调用_Block_object_dispose函数
③_Block_object_dispose
函数会自动释放指向的对象(release)
Block类型
block 有 3 种类型,可以通过调用 class 方法或者 isa 指针 查看具体类型,最终都是继承自 NSBlock 类型。
每一种类型的 block 调用 copy 后的结果如下所示:
几个问题
Q:block在给 NSMutableArray 添加或移除对象,需不需要添加 __block?
不需要。
Q:self 会不会捕获到 block 内部?
会捕获。 OC 方法都有两个隐式参数,方法调用者self和方法名_cmd。 参数也是一种局部变量。
Q:_name 会不会捕获到 block 内部?
会捕获。不是将_name变量进行捕获,而是直接将self捕获到 block 内部,因为_name是 Person 类的成员变量,_name来自当前的对象/方法调用者self(self->_name)。
如果使用self.name即调用self的getter方法,即给self对象发送一条消息,那还是要访问到self。self是局部变量,不是全局变量,所以self会捕获到 block 内部。
Q:__block 修饰符使用注意点:
在 MRC 下使用 __block
修饰对象类型,在 block 内部不会对该对象进行 retain 操作,所以在 MRC 环境下可以通过 __block
解决循环引用的问题。
Q:解决在 block 内部通过弱指针访问对象成员时编译器报错的问题:
1 | __weak typeof(self) weakSelf = self; |
Q:以下代码有问题吗?
1 | __block id weakSelf = self; |
在 MRC 下,不会产生循环引用;
在 ARC 下,会产生循环引用,导致内存泄漏,解决方案如下。
1 | __block id weakSelf = self; |
缺点:必须要调用 block,而且 block 里要将指针置为 nil。如果一直不调用 block,对象就会一直保存在内存中,造成内存泄漏。