Block深入理解
数据结构定义
1 | struct Block_descriptor { |
通过该图,我们可以知道,一个 block 实例实际上由 6 部分构成:
- isa 指针,所有对象都有该指针,block的本质是对象isa指向下面三种block类。
- flags,用于按 bit 位表示一些 block 的附加信息,本文后面介绍 block copy 的实现代码可以看到对该变量的使用。
- reserved,保留变量。
- invoke,函数指针,指向具体的 block 实现的函数调用地址。
- descriptor, 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针。
- variables,capture 过来的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。
问题
1、为什么block不能修改外部变量的值?即:写操作不对外部变量生效?加上__block
就可以生效了?
2、UIView的animation方法block中引用self会导致循环引用吗?self会在block执行完后释放吗?
3、GCD中持有self会导致循环引用吗?
解答问题
将问题分解为以下几部分一一作答
问题研究的是那种类型的block
block内为什么不能修改block外部的变量
最优解及原理
其他几种解法
修改外部变量的必要条件“将auto从栈copy到堆”
将auto变量封装为结构体(对象)
block类型
__NSGlobalBlock__
没有访问auto变量__NSStackBlock__
访问了auto变量__NSMallocBlock__
__NSStackBlock__
调用了copy
每种类型的block调用copy后结果如下
Block的类 | 原block存储区 | 调用copy |
---|---|---|
_NSConcreteGlobalBlock | 数据区.data区 | 什么也不做 |
_NSConcreteStackBlcok | 栈 | 从栈复制到堆 |
_NSConcreteMallocBlock | 堆 | 引用计数加一 |
在ARC环境下以下情况会自动将栈上的block复制到堆上:
block作为函数值返回
将block赋值给__strong指针时
block作为Cocoa API中方法名含有using block的方法参数时
block 作为 GCD API的方法参数时
block为什么不能修改block外部变量
block不允许修改外部变量的值,block本质是一个对象,花括号区域是对象内部的函数,变量进入花括号时,本质是进入了另一个函数区域,改变了作用域。如果不加上这样的限制,当在函数内想声明一个同名变量,是允许还是不允许呢?只有加上这样的限制,才能实现这种场景。
所以Apple在编译器层加了限制,当block内部试图修改自动变量时会直接报错。
block怎样保证能正常访问外部变量xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
查看对应的C++代码
为了保证block内部能访问外部变量,block有一个变量捕获机制,将外部变量复制到了block的结构体中,保存在variables中。
全局变量: 不捕获,可直接访问
局部变量:
- auto: 捕获到内部,如果是值类型,值传递,如果是对象,引用计数加1
- static: 基本数据类型也是指针传递
1 | typedef void(*CWBlock)(void); |
block实现修改外部变量的最优解
两种内存开销大的方法:
- 加static(放在静态存储区/全局初始化区):捕获变量的指针。
- 将变量设为全局变量。全局变量是无视作用域的,所以可以在block内修改
最优解:局部变量加__block关键字- __block 可以解决block内无法修改auto变量值的问题
- __block不能修饰全局变量、静态变量
- ARC中无论是否添加__block,block中的auto变量都会被从栈上copy到堆上
编译器会将__block修饰的变量包装成一个结构体(对象),在结构体中新建一个同名auto变量,block内部捕获改结构体指针,将结构体copy到堆上,在block中使用自动变量时,使用指针指向的结构体中的自动变量,于是就达到了修改外部变量的作用。
1 | typedef void(*CWBlock)(void); |
UIView Animation 会造成循环引用吗
1 | - (void)viewWillDisappear:(BOOL)animated { |
在 viewWillDisappear 的时候,执行一个延迟 3s 的动画,是否会像 dispatch_after 一样,等待动画执行完成,再释放 self?
实验结果是不会的,self 能正常释放。因为以上代码其实等同于:
1 | - (void)viewWillDisappear:(BOOL)animated { |
block 的作用就是简化动画提交的代码,而动画实际提交给了 CATransaction,当 delay 的时间到时,再进行执行。所以在这里,block 中的代码只是一个瞬时操作(如果在在 block 中 NSLog,可以发现 NSLog 是立马执行的),而实际 view 的状态,是被保存在了 self.animationView.layer.modelLayer 中。当 view removeFromSuperView 时,animation 就会被移除,[view release],所以不会出现动画执行完后,再释放 view 的问题。
参考文档:
谈Objective-C block的实现
Objective-C Block 分析
技术清谈
从对象持有到 UIView Animation