iOS Block存储域及循环引用
系列文章:
iOS Block概念、语法及基本使用
iOS Block实现原理
iOS Block __block说明符
本文将讲解以下几点:
- Block种类
- Block变量存储域
- __block变量存储域
- 截获对象
- __block变量和对象
- Block循环引用
根据上几篇文章Block语法编译后的源代码我们看到,__block_impl结构体内部有一个成员变量:isa指针,__main_block_impl_0结构体初始化的时候,isa指针初始化为 impl.isa = &_NSConcreteStackBlock,因为Block也是OC对象,我们说该isa指针指向该Block实例所属的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内部持有self,设置布局的view持有block,但是block不持有view,当block执行完后就释放了,self的引用计数-1,所以block也不会持有self,所以不会导致循环引用。
3、reactiveCocoa如果不使用@weakify @strongify,会循环引用,两个宏就等于下边代码:
__weak typeof(self) weakSelf = self;
__strong typeof(weakSelf) strongSelf = weakSelf;
六、总结
以上几篇文章基本就把Block(以及__block变量)的定义、语法、应用、原理介绍完了,主要的目的还是能更灵活的应用于项目。
欢迎提出宝贵意见,喜欢赞一下吧。
图有点low,莫见怪,哈哈哈...