block 中的 weakSelf 和 strongSelf

2018-02-27 11:27:28来源:https://juejin.im/post/5a8e426f5188257a7450c437作者:稀土掘金人点击

分享
{
FNSObject *obj = [[FNSObject alloc] init];
// ...
FNSObject * __weak weakObj = obj;
weakObj.completionHandler = ^(NSInteger result) {
FNSObject *strongWeakObj = weakObj;
if (strongWeakObj) {
[strongWeakObj ...];
} else {
// Probably nothing...
}
};
weakObj.completionHandler();
}


上面的代码,来自于Apple 的官方文档(文档主要阐述使用 ARC 内存管理机制,所需要注意的一些问题)。文档中,使用 ViewController 作为对象类型,这里则是使用了继承于 NSObject 的 FNSObject,更具普适性。


明显上面的代码经过处理。而没有处理过的,可能出现内存泄漏的代码是这样的:


FNSObject *obj = [[FNSObject alloc] init];
// ...
obj.completionHandler = ^(NSInteger result) {
[obj ...];
};
obj.completionHandler()

这里提出第一个问题:


为什么会出现内存泄漏?


因为出现了循环引用
。ARC
的内存回收机制,建立在retainCount
(内存计数)的概念上。指针对于对象的内存地址的指向,会使得RetainCount
的数值发生变化。


FNSObject *obj = [[FNSObject alloc] init];


这句代码,左边生成一个obj
的指针,右边生成一个FNSObject
类的对象,=
字符将obj
指针指向右边对象对应的内存地址,使得这个对象的retainCount
实现了从 0 到 1 的增长,有了暂时留存下来的资格。


obj.completionHandler = ^(NSInteger result) {
[obj ...];
};


相同的,这行代码,同样让obj.completionHandler
和^(NSInteger result) { [obj ...]; }
有了指向关系。^(NSInteger result) { [obj ...]; }
对象的RetainCount
+ 1。



当然,其实运行到这里,[[FNSObject alloc] init]
对象的RetainCount
并不为 1,而是为 3。{ [obj ...]; }
中的代码,对这个对象的RetainCount
同样产生了影响。



这种影响,和block
的结构以及内部实现有关。



根据StackOverError 的一个答案,下面几个公布出来的Clang
源码里面,描述了ARC
下block
的具体实现。


CGBlocks.cpp
CGDecl.cpp
CGObjC.cpp


在block
从栈复制到堆的过程中,所执行的EmitARCStoreStrongCall
和EmitBlockLiteral
两个方法分别对原栈上的变量各产生一次RetainCount
+ 1 的作用。所以[[FNSObject alloc] init]
对象的RetainCount
值增长为 3。



而当上面代码所处的函数运行完毕之后,系统会对栈区内的指针进行清理,obj
这个指针的内存将会被回收,此时[[FNSObject alloc] init]
这个对象的RetainCount
会执行 -1 操作,综合上面的数值,此时这个对象的RetainCount
值为 2。



RetainCount
不为 0,[[FNSObject alloc] init]
没有被释放。



这于是导致了[[FNSObject alloc] init]
对象所持有的^(NSInteger result) { [obj ...]; }
也没有释放,而^(NSInteger result) { [obj ...]; }
这个对象的留存同样导致了其内部的__(类名称)__(函数名称)_block_impl_0
结构体中所指向[[FNSObject alloc] init]
对象的指针没有进行释放。



所以到这里,[[FNSObject alloc] init]
和^(NSInteger result) { [obj ...]; }
两个对象都无法释放。内存被长期占据。这是问题出现的原因。



为什么__weak
修饰符可以打破循环引用?


FNSObject * __weak weakObj = obj;


运行时本身维护了一个名为refcnts
的hash
表和一个名为weak_entries
的hash
表,用来监听被__weak
指针所指对象的RetainCount
的变化和建立对象和指针之间的索引关系。主要作用为指针的置 nil 操作,毕竟野指针调用方法会引起崩溃。这是__weak
修饰符的其中一个功能,但并不是在这里可以避免循环引用的主要原因。



声明__weak
修饰的指针时,编译器并不会同时插入retain
的语句。所以__weak
修饰下的指针指向对象,不会使得对象的内存计数产生变化。这个特性确实使得引用循环被打破,而且起作用的并不是外部指针的修饰符,而是block
内部__(类名称)__(函数名称)_block_impl_0
结构体内对应外部变量指针的修饰符。



在block
的复杂结构中,有一个struct
为__(类名称)__(函数名称)_block_impl_0
,以上面的代码为例子,如果block
内部使用到了obj
,结构体中会生成一个FNSObject
类型的指针,且修饰符为__strong
,并指向外部的[[FNSObject alloc] init]
变量,此时新的指针对于[[FNSObject alloc] init]
是一种强引用,而如果block
内部使用到了weakObj
,新的指针则会被修饰为__weak
,对于[[FNSObject alloc] init]
则是一种弱引用,就如上面所说,并不增加内存计数。



所以此时__weak
修饰符 让block
拥有一个可用的指针对象,但是又不会成为循环引用的元凶。



为什么不用__unsafe_unretained
代替__weak
?



__unsafe_unreatined
同样不会引起内存计数的变化,也提供一个目标对象的引用指针。在这里__unsafe_unreatined
和__weak
有类似的作用。有问题的是,__unsafe_unreatined
不具备自动指针置 nil 的功能,所生成的weakObj
指针在栈上管理,所处函数执行完毕的时候,会被系统回收,但block
堆上的指针却不会,崩溃在所难免。



block
一定要__strong
一下吗?为什么?



不一定需要。但就像买保险一样,__strong
就是一份保单。未雨绸缪为的就是以防万一。


{
FNSObject *obj = [[FNSObject alloc] init];
// ...
FNSObject * __weak weakObj = obj;
weakObj.completionHandler = ^(NSInteger result)
{
[weakObj ...1];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(100000... * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakObj ...2];
});
};
weakObj.completionHandler();
weakObj = nil;
}


在上面的代码中,问题很明显。延时block
内部对于[FNSObject alloc] init]
也是弱引用。[weakObj ...2];
执行的时候,weakObj
本身已经为 nil。



block
内部指针的修饰符受到外界指针的影响,所以为了防止[[FNSObject alloc] init]
过早释放。需要重新生成一个__strong
指针用来增长[[FNSObject alloc] init]
的内存计数,且仅在weakObj.completionHandler
代码块范围内生效。代码如下:


FNSObject *strongWeakObj = weakObj;


当然,这里之所以没有添加__strong
修饰符,是因为这种实例化方式,生成的指针默认添加__strong
修饰符。



这里引出另一个问题。我们平时用的__strong __typeof__ (ttttt) s##ttttt = ttttt
这种写法,它的__strong
修饰符可否省略?



答案是不行的,typeof
并不具有任何默认修饰符,它的修饰符是由(ttttt)
这个括号内的指针变量决定的。一旦使用__typeof__ (weakObj) strongWeakObj= weakObj
这样的写法,无非创建出另一个弱引用指针而已。


还有什么其他避免循环引用的方法?
FNSObject *obj = [[FNSObject alloc] init];
// ...
obj.completionHandler = ^(NSInteger result) {
[obj ...];
};
obj.completionHandler()
obj.completionHandler = nil;


依然以上面的代码为例,在确保不影响block
内代码运行的前提下,手动释放block
可以直接打破循环引用。


__block FNSObject *obj = [[FNSObject alloc] init];
// ...
obj.completionHandler = ^(NSInteger result) {
[obj ...];
obj = nil;
};
obj.completionHandler()


这样的代码也是可以的,重点在于减少[[FNSObject alloc] init]
的内存计数。



另可以直接将持有block
的对象的指针作为block
的参数直接传入,此时是以对应__main_block_func_0
函数参数传递的方式进入,不会造成循环引用。


如:


{
FNSObject *obj = [[FNSObject alloc] init];
// ...
obj.completionHandler = ^(id objjjjj) {
[objjjjj ...];
};
obj.completionHandler(obj);
}
总结


以往如果偷懒,总是喜欢直接同时使用__weak
和__strong
来解决这类问题。但如果能每次使用都有的放矢,是一件好事。


最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台