Objective-C高级编程读书笔记三部曲已经写完, 另外两篇如下 :
Objective-C高级编程读书笔记之内存管理
Objective-C高级编程读书笔记之GCD
Blocks
这里有五道关于block的测试题, 大家可以去做做测试看看自己对block了解多少.
目录
- Block的定义
- Block有哪几种类型
- Block特性
- __block修饰符
- block调用copy方法的内部实现
- block的循环引用问题
- 总结
1. block的定义
block是Objective-C对于闭包的实现(闭包是一个函数<或者指向函数的指针>加上函数有关的自由变量).
block也是对象, 以下对block结构体的成员作简单解释
- isa : 指向Class对象的指针, 所有对象都有该指针
- flags : 用于按 bit 位表示一些 block 的附加信息
- reserved : 保留变量
- invoke : 指向函数的指针, 指向block的实现代码
- descriptor : 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针。
- 捕捉到的变量 : 捕捉过来的变量,block 之所以能够访问它外部的局部变量,就是因为将这些变量(或对象的地址)拷贝到了结构体中。
- copy : 用于保留捕获的对象
- dispose : 用于释放捕获的对象
2. block的类型
- 全局块(_NSConcreteGlobalBlock)
- 栈块(_NSConcreteStackBlock)
- 堆块(_NSConcreteMallocBlock)
这三种block各自的存储域如下表
类 | 设置对象的存储域 |
---|---|
_NSConcreteStackBlock | 栈 |
_NSConcreteGlobalBlock | 程序的数据区域(.data区) |
_NSConcreteMallocBlock | 堆 |
说明 :
- 全局块存在于全局内存中, 相当于单例.
- 栈块存在于栈内存中, 超出其作用域则马上被销毁
- 堆块存在于堆内存中, 是一个带引用计数的对象, 需要自行管理其内存
全局块(_NSConcreteGlobalBlock)
- block定义在全局变量的地方
- block没有截获任何自动变量
以上两个情况满足任意一个则该block为全局块, 全局块的生命周期贯穿整个程序, 相当于单例.
栈块(_NSConcreteStackBlock)
只要不是全局块, 且block没有被copy, 就是栈块.栈块的生命周期很短, 当前作用域结束, 该block就被废弃. 要想在当前作用域以外的地方使用该block, 应该把该block从栈copy到堆上
堆块(_NSConcreteMallocBlock)
简单来说, 栈块copy之后就变成堆块, 这简单吧~
ARC下的block类型
因为ARC下默认变量修饰符为__strong, 所以我们接触到的block几乎全是堆block和全局block.
ARC下, blk = block; 相当于 blk = [block copy];
3. block特性
1. 截获自动变量值
1> 对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的. 也就是说block的自动变量截获只针对block内部使用的自动变量, 不使用则不截获, 因为截获的自动变量会存储于block的结构体内部, 会导致block体积变大.
int age = 10;
myBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
输出为
age = 10
2> 对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的.
__block int age = 10;
myBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
输出为
age = 18
意味着对于第一种情况, 在block外部修改变量的值并不会应该block内部变量的值.而第二种情况则反之.
并且第一种情况block内部不允许修改变量的值, 第二种情况下可以. (有例外, 静态变量, 静态全局变量, 全局变量即使不使用__block修饰符也可以在block内部修改其值)
2. 截获对象
对象不同于自动变量, 就算对象不加上__block修饰符, 在block内部能够修改对象的属性.
block截获对象与截获自动变量有所不同.
堆块会持有对象, 而不会持有__block修饰的对象, 而栈块永远不会持有对象, 为什么呢?
- 堆块作用域不同于栈块, 堆块可以超出其作用域地方使用, 所以堆块结构体内部会保留对象的强指针, 保证堆块在生命周期结束之前都能访问对象. 而对于__block对象为什么不会持有呢? 原因很简单, 因为block对象会跟随block被复制到堆中, block再去引用堆中的对象(后面会讲这个过程)..
- 栈块只能在当前作用域下使用, 所以其内部不会持有对象. 因为不存在在作用域之外访问对象的可能(栈离开当前作用域立马被销毁)
4. __block修饰符
为什么__block修饰符修饰的变量就能够在block内部修改呢?? 原因在此
利用clang -rewrite-objc 源代码文件名
便可揭开其神秘的面纱.
__block int val = 10;
转换成
__Block_byref_val_0 val = {
0,
&val,
0,
sizeof(__Block_byref_val_0),
10
};
天哪! 一个局部变量加上__block修饰符后竟然跟block一样变成了一个__Block_byref_val_0结构体类型的自动变量实例.
此时我们在block内部访问val变量则需要通过一个叫__forwarding的成员变量来间接访问val变量(下面会对__forwarding进行详解)
5. copy
block的copy操作究竟做了什么呢?
这里不得不提及__block变量的存储域
__block变量的配置存储域 | block从栈复制到堆时的影响 |
---|---|
栈 | 从栈复制到堆并被Block持有 |
堆 | 被Block持有 |
由上图可知, 对一个栈块进行copy操作会连同block与__block变量(不管有没有使用)在内一同copy到堆上, 并且block会持有__block变量(使用).
ps : 堆上的block及__block变量均为对象, 都有各自的引用计数
当然, 当block被销毁时, block持有的__block也会被释放
到这里我们能知道, 此思考方式与Objective-C的引用计数内存管理完全相同.
那么有人就会问了, 既然__block变量也被复制到堆上去了, 那么访问该变量是访问栈上的还是堆上的呢?? __forwarding 终于要闪亮登场了
通过__forwarding, 无论实在block中, block外访问__block变量, 也不管该变量在栈上或堆上, 都能顺利地访问同一个__block变量.
什么时候我们需要手动对block调用copy方法
前面我们说到 : 要想在当前作用域以外的地方使用该block, 应该把该block从栈copy到堆上. 实际上, 在ARC下, 以下几种情况下, 编译器会帮我们把栈上的block复制到堆中
- block作为函数返回值返回时
- 将block赋值给__strong修饰符id类型或block类型成员变量时
- 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递block时
理论上我们只有把block作为函数/方法的参数传入时才需要对block进行copy操作.
我们对不同地方的block调用copy会产生什么效果呢?
Block的类 | 副本源的配置存储域 | 拷贝效果 |
---|---|---|
_NSConcreteStackBlock | 栈 | 从栈拷贝到堆 |
_NSConcreteGlobalBlock | 程序的数据区域 | 什么也不做 |
_NSConcreteMallocBlock | 堆 | 引用计数增加 |
所以, 不管block是什么类型, 在什么地方, 用copy方法都不会引起任何问题.如下表格所示. 就算是反复多次调用copy方法, 如
blk = [[[[blk copy] copy] copy] copy];
该源码可解释如下 :
{
block tmp = [blk copy]; // block被tmp持有
blk = tmp; // block被tmp和blk持有
}
// tmp超出作用域, 其指向的block也被释放, block被blk持有
{
block tmp = [blk copy]; // block被tmp和blk持有
blk = tmp; // blk指向的旧block释放, 并强引用新block, 最终block被tmp和blk持有
}
// tmp超出作用域, 其指向的block也被释放, block被blk持有
...下面不断重复该过程
我们知道, 这只是一个循环的过程, block被tmp持有 -> block被tmp和blk持有 -> block被blk持有 -> block被tmp和blk持有 -> ......
由此可得知, 在ARC下该代码也没有任何问题.
总结 : 如果block需要给作用域外的地方使用, 但是你不知道需不需要copy, 那就copy吧. 反正不会错
6. block的循环引用
这部分相信大家都清楚怎样做能破环, 所以我在这就只简单说两句
- MRC下用__block可以避免循环引用(原因见上面block特性之截获自动变量值)
- ARC下用__weak来避免循环引用
这里需要提醒大家的是, 只有堆块(_NSConcreteMallocBlock)才可能会造成循环引用, 其他两种block不会
7. Block总结 :
- block相比函数更加方便, 高效, 苹果强烈推荐使用
- ARC下编译器会帮助我们更好地管理block的生命周期
- ARC下block属性声明为strong或copy其实都一样, 因为编译器内部会帮我们实现copy方法
- 善用__weak(ARC)或__block(MRC)来避免循环引用
推荐几篇有关block的文章
谈Objective-C block的实现
让我们来深入简出block吧
欢迎大家关注@Jerry4me, 关注菜鸟成长_. 我会不定时更新一些学习心得与文章.