iOS Block存储域及循环引用
系列文章:
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 ViewControllervoid (^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 = 1array count = 2array 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,莫见怪,哈哈哈…
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » iOS Block存储域及循环引用