iOS Block 概念, 语法及基本使用
iOS Block 实现原理
iOS Block __block 说明符
根据上几篇文章 Block 语法编译后的源代码我们看到,__block_impl 结构体内部有一个成员变量: isa 指针,__main_block_impl_0 结构体初始化的时候, isa 指针初始化为 impl.isa = &_NSConcreteStackBlock, 因为 Block 也是 OC 对象, 我们说该 isa 指针指向该 Block 实例所属的 Block 类.
Block 有以下几种:
Block 类 | Block 存储域 |
---|---|
_NSConcreteStackBlock | 栈 |
_NSConcreteGlobalBlock | 程序的数据区域(.data 区) |
_NSConcreteMallocBlock | 堆 |
顺便说一下程序的内存分配情况:
区域 | 存放的东东 |
---|---|
栈区(stack) | 由编译器自动分配释放 ,存放函数的参数值,局部变量的值 |
堆区(heap) | 程序员分配(alloc/new/copy/mutableCopy) |
全局区(静态区)static | 全局变量和静态变量 |
常量区 | 常量字符串等 |
数据区(代码区) | 存放函数体的二进制代码 |
到目前位置看到的 Block 全都是_NSConcreteStackBlock, 其实不是这样的, 在记述全局变量的地方使用 Block 语法时, 生成的 Block 为 _NSConcreteGlobalBlock, 举个例子看下:
- @implementation ViewController
- void (^block)(void) = ^{
- NSLog(@"haha");
- };
- @end
编译后__block_block_impl_0 结构体:
- struct __block_block_impl_0 {
- struct __block_impl impl;
- struct __block_block_desc_0* Desc;
- __block_block_impl_0(void *fp, struct __block_block_desc_0 *desc, int flags=0) {
- impl.isa = &_NSConcreteGlobalBlock;//global
- impl.Flags = flags;
- impl.FuncPtr = fp;
- Desc = desc;
- }
- };
该 Block 的类为_NSConcreteGlobalBlock 类, 即存放在数据区也就是代码区, 因为使用全局变量的地方不能使用自动变量, 所以不存在对自动变量的截获. 由此 Block 结构体实例的内容不依赖于执行的状态, 所以整个程序中只需一个实例, 因此把该结构体实例放在数据区.
在以下情况下生成的 Block 结构体实例属于 _NSConcreteGlobalBlock 类:
记述全局变量的地方有 Block 语法时;
Block 语法的表达式中不使用应截获的自动变量时;
除以上两种情况外, 都会生成 _NSConcreteStackBlock 类, 且保存在栈区域.
一, Block 变量存储域
配置在全局变量上的 Block, 从变量作用域外也可以通过指针安全地使用, 但是设置在栈的 Block, 如果其所属的变量作用域结束, 该 block 也就被废弃. 由于__block 变量也配置在栈上, 同样其所属的变量作用域结束, 则该__block 变量也同样被废弃.
Block 提供了将 Block 从栈区 copy 到堆区的方法. 如下图:
复制到堆上. jpg
复制到堆上的 Block 将_NSConcreteMallocBlock 类对象写入 Block 结构体实例的成员变量 isa:
impl.isa = &_NSConcreteMallocBlock;
还记得上一节说到的__block 变量结构体实例的 __forwarding 指针指向__block 变量结构体自己吧, 也就是说无论 Block 结构体实例配置在栈上还是堆上, 都能够访问__block 变量.
那么什么时候 Block 从栈上复制到堆上呢, 其实大多数情况下, 编译器会恰当的进行判断, 自动生成将 Block 从栈上复制到堆上.
以下情况需要程序员自己通过 copy 方法将 Block 从栈区复制到堆区:
向方法或函数的参数中传递 Block 时;
不需要手动复制的情况:
Cocoa 框架的方法且方法名中含有 usingBlock 等时;
GCD 的 API
下图是按 Block 的存储域, 使用 copy 后, Block 有什么变化
Block 的类 | Block 原区域 | 复制后 |
---|---|---|
_NSConcreteStackBlock | 栈 | 从栈复制到堆 |
_NSConcreteGlobalBlock | 数据区 | 什么也不做 |
_NSConcreteMallocBlock | 堆 | 引用计数增加 |
从从上边可以看出, 不管 Block 配置在何处, 用 copy 方法复制都不会出现任何问题. 在不确定时调用 copy 方法即可.
此处有一个例子:
blk = [[[[blk copy] copy] copy] copy];
该代码解释如下:
- {
- // 将配置在堆上的 Block 复制给变量 tmp, 变量 tmp 持有强引用的 Block;
- blk_t tmp = [blk copy];
- // 将 Block 变量 tmp 赋值给 blk 变量, 大括号走完后, tmp 释放, blk 继续持有 Block;
- blk = tmp;
- }
- // 以此类推...
- {
- blk_t tmp = [blk copy];
- blk = tmp;
- }
- {
- blk_t tmp = [blk copy];
- blk = tmp;
- }
- {
- blk_t tmp = [blk copy];
- blk = tmp;
- }
由此可见, ARC 下使用 copy 完全没问题.
二,__block 变量存储域
Block 从栈上复制到堆上, 那么在 Block 中使用的__block 变量是怎么处理的呢, 看下表:
__block 变量配置区域 | Block 从栈复制到堆时的影响 |
---|---|
栈 | 从栈复制到堆并被 Block 持有 |
堆 | 被 Block 持有 |
说明: 若一个 Block 中使用了__block 变量, 当 Block 变量从栈复制到堆上时, 那么__block 变量也会被复制到堆上.
__block 变量复制到堆上. jpg
多个 Block 变量使用__block 变量时, 因为最先会将所有的 Block 配置在栈上, 所以__block 变量也会配置在栈上. 在任何一个 Block 变量被赋值到堆上时,__block 变量一并被赋值到堆上, 当其他的 Block 变量复制到堆上时, 其使用的__block 变量引用计数增加:
__block 变量被复制到堆区. jpg
配置在堆上的 Block 被废弃时,__block 变量也被废弃:
__block 变量废弃. jpg
到这里我们看到, Block 变量和 OC 对象的内存管理机制是一样的, 都是使用引用计数, 所以也验证了那句话: Block 是 OC 对象.
三, 截获对象
先来看一个例子:
- typedef void (^block)(id obj);
- block blk;// 全局变量 Block
- - (void)viewDidLoad {
- [super viewDidLoad];
- id array = [NSMutableArray array];
- blk = [^(id obj){
- [array addObject:obj];
- NSLog(@"array count = %ld",[array count]);
- } copy];
- blk([[NSObject alloc] init]);
- blk([[NSObject alloc] init]);
- blk([[NSObject alloc] init]);
- }
打印:
- array count = 1
- array count = 2
- array count = 3
从源代码可以看出, array 变量是临时变量, viewDidLoad 方法走完就被废弃, 但依然有打印, 说明变量没有释放, 从前几篇文章可以想象, 打印的 array 变量被 Block 结构体实例持有了, 下面来验证下, 编译后的代码如下:
- struct __block_impl {
- void *isa;
- int Flags;
- int Reserved;
- void *FuncPtr;
- };
- static struct __ViewController__viewDidLoad_block_desc_0 {
- size_t reserved;
- size_t Block_size;
- void (*copy)(struct __ViewController__viewDidLoad_block_impl_0*, struct __ViewController__viewDidLoad_block_impl_0*);
- void (*dispose)(struct __ViewController__viewDidLoad_block_impl_0*);
- } __ViewController__viewDidLoad_block_desc_0_DATA = {
- 0,
- sizeof(struct __ViewController__viewDidLoad_block_impl_0),
- __ViewController__viewDidLoad_block_copy_0,
- __ViewController__viewDidLoad_block_dispose_0
- };
- // 函数指针调用的函数
- static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself, id obj) {
- id array = __cself->array; // bound by copy
- ((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)array, sel_registerName("addObject:"), (id)obj);
- NSLog((NSString *)&__NSConstantStringImpl__var_folders_9k_z85dfkt91zd1j387gcxn8xkh0000gn_T_ViewController_503b9f_mi_0,((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
- }
- //copy 和 dispose 函数
- static void __ViewController__viewDidLoad_block_copy_0(struct __ViewController__viewDidLoad_block_impl_0*dst, struct __ViewController__viewDidLoad_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
- static void __ViewController__viewDidLoad_block_dispose_0(struct __ViewController__viewDidLoad_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
- //Block 结构体
- struct __ViewController__viewDidLoad_block_impl_0 {
- struct __block_impl impl;
- struct __ViewController__viewDidLoad_block_desc_0* Desc;
- id array;// 持有 array 变量
- __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
- impl.isa = &_NSConcreteStackBlock;
- impl.Flags = flags;
- impl.FuncPtr = fp;
- Desc = desc;
- }
- };
- //viewDidLoad 方法
- static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
- ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
- id array = ((NSMutableArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"));
- blk = (block)((id (*)(id, SEL))(void *)objc_msgSend)((id)((void (*)(id))&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, array, 570425344)), sel_registerName("copy"));
- ((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
- ((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
- ((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
- }
请注意, 可以看到, id 类型的 array 变量被 Block 结构体持有了.
在这里说明一点, 其实我们创建的对象, 默认会带上__strong 所有权修饰符, 比如:
id array = [NSMutableArray array];
上边代码等同于下边代码:
id __strong array = [NSMutableArray array];
在 OC 语言中, C 语言的结构体不能含有附有__strong 修饰符的变量, 因为编译器不知道应何时进行 C 语言结构体的初始化和废弃操作, 不能很好的管理内存.
前两节我们看到了 copy dispose 函数, 没有做详细解释, 只是猜想了一下, 接下来说说这两个函数.
OC 运行时可以准确的把握 block 从栈上复制到堆上和 Block 废弃的时机, 因此 Block 结构体内部使用带有__strong 或__weak 修饰符的变量, 也可以在恰当的时刻初始化和废弃, 为此需要在 __ViewController__viewDidLoad_block_desc_0 结构体内部加上 copy 和 dispose 成员变量, 以及作为函数指针赋值给这两个变量的 __ViewController__viewDidLoad_block_copy_0 和 __ViewController__viewDidLoad_block_dispose_0 函数
copy 函数内部使用了_Block_object_assign 函数将对象类型对象赋值给 Block 结构体内的成员变量并持有该对象._Block_object_assign 函数调用相当于 retain 实例方法的函数.
dispose 函数内部使用_Block_object_dispose 函数释放 Block 结构体内部的对象类型的成员变量._Block_object_dispose 函数调用相当于 release 实例方法的函数.
我们只看到了生成的 copy 和 dispose 函数, 但是没看到调用啊, 那到底啥时候调用这两个函数呢, 这是系统自动发生的动作:
函数 | 调用时机 |
---|---|
copy | 栈上 Block 被复制到堆上时 |
dispose | 堆上 Block 被废弃时 |
当 Block 从栈上复制到堆上时, 会调用 copy 函数; 当堆上的 Block 被废弃时, 会调用 dispose 函数.
上一节提到了两点, 什么时候 block 会从栈上复制到堆上, 现在总结如下:
Block 调用 copy 方法时
Block 作为函数返回值返回时
将 Block 赋值给赋有__strong 修饰符 id 类型的类或 Block 类型成员变量时
在方法名中含有 usingBlock 的 Cocoa 框架方法或 GCD 的 API 中传递 Block 时
有了这种构造, 通过使用__strong 修饰符的变量, Block 中截获的对象就能超出其变量作用域存在.
上一节我们研究__block 变量的时候, 看到过 copy 和 dispose 函数, 现在 Block 截获对象的也出现了, 而且转换后的代码基本相同, 后边的注释不同:
类型 | _Block_object_assign/dispose 函数 |
---|---|
Block 截获对象 | BLOCK_FIELD_IS_OBJECT |
__block 变量 | BLOCK_FIELD_IS_BYREF |
通过这两个 OBJECT,BYREF 来区分 copy/dispose 函数的对象类型是对象还是__block 变量. 与 copy 函数持有截获的对象, dispose 释放持有的对象相同, copy 函数持有 Block 所使用的__block 变量, dispose 函数释放__block 变量.
有一点需要说明, 这本书上的截获对象的例子, Block 不调用 copy 方法, 我本地测试的不会强制结束. 可以解释为: blk 变量为全局变量, 生成的 Block 结构体实例也是全局变量, 全局变量持有 array 变量, 所以程序不会强制结束. 如果这个解释有误的话, 还请读者指针, 谢!
四,__block 变量和对象
__block 说明符可指定任意类型的变量. 下面看下__block 修饰 OC 对象.
__block id obj = [[NSObject alloc] init];
clang 转换如下:
__block 结构体
- struct __Block_byref_obj_0 {
- void *__isa;
- __Block_byref_obj_0 *__forwarding;
- int __flags;
- int __size;
- void (*__Block_byref_id_object_copy)(void*, void*);
- void (*__Block_byref_id_object_dispose)(void*);
- id obj;
- };
- // 声明部分
- __attribute__((__blocks__(byref))) __Block_byref_obj_0 obj = {
- (void*)0,
- (__Block_byref_obj_0 *)&obj,
- 33554432,
- sizeof(__Block_byref_obj_0),
- __Block_byref_id_object_copy_131,
- __Block_byref_id_object_dispose_131,
- [[NSObject alloc] init]
- }
Block 截获对象这一小节中, 当 Block 从栈复制到堆上时, 使用 copy 函数持有截获的对象, 当 Block 被废弃时, 使用 dispose 释放截获的对象.
在__block 说明符修饰对象时, 在__block 变量结构体中看到了 copy 和 dispose 函数, 那说明当__block 变量从栈上复制到堆上时, 使用 copy 函数持有赋值给__block 变量的对象, 当堆上的__block 变量被废弃时, 使用 dispose 函数释放赋值给__block 变量的对象.
由此可知, 只要堆上的__block 结构体实例变量没有被释放, 那么__block 变量就不会被释放.
五, Block 循环引用
原因: 在 Block 内部使用对象类型的变量, 该变量持有 Block, 当 Block 从栈上复制到堆上时, Block 同时持有了对象类型变量, 那么当对象类型释放时, 由于变量和 Block 互相引用导致内存泄漏, 举个例子:
- typedef void (^block)(id obj);
- @property (nonatomic, copy) block blk;
- - (void)viewDidLoad {
- [super viewDidLoad];
- self.array = [NSMutableArray array];
- self.blk = ^(id obj){
- [self.array addObject:obj];
- NSLog(@"array count = %ld",[self.array count]);
- };
- }
这样写如果这个 VC 被 pop, 那么这个 VC 是释放不了的, VC 持有 Block,Block 内部持有 VC.
循环引用. jpg
修改一下:
- - (void)viewDidLoad {
- [super viewDidLoad];
- self.array = [NSMutableArray array];
- ViewController * __weak temp = self;
- self.blk = ^(id obj){
- [temp.array addObject:obj];
- NSLog(@"array count = %ld",[temp.array count]);
- };
- }
循环引用消失:
循环引用消失. jpg
在此根据自己的项目中使用到的 Block 场景, 来总结下 Block 使用时的注意事项, 说不定项目中真的有内存泄漏呢
1,UIView 的 animation 动画块使用了 Block, 内部使用 self 不会循环引用, 为什么呢
答: UIView 动画块是类方法, 不被 self 持有, 所以不会循环引用.
2,Monsary 也使用了 Block 来设置控件的布局, Block 内部使用 self, 为什么不会循环引用呢
答: Monsary 使用的 Block 是当做参数传递进去的, 前边说过, 当 Block 当做参数传递进去, 编译器不会把 Block 截获的自动变量复制到堆中, self 持有控件, 即 self 持有 Block, 但是 Block 不持有 self, 所以不会循环引用.
3,ReactiveCocoa 如果不使用 @weakify @strongify, 会循环引用, 两个宏就等于下边代码:
- __weak typeof(self) weakSelf = self;
- __strong typeof(weakSelf) strongSelf = weakSelf;
六, 总结
以上几篇文章基本就把 Block(以及__block 变量) 的定义, 语法, 应用, 原理介绍完了, 主要的目的还是能更灵活的应用于项目.
欢迎提出宝贵意见, 喜欢赞一下吧.
图有点 low, 莫见怪, 哈哈哈...
来源: http://www.jianshu.com/p/cc39cccad4b5