深入研究Block用weakSelf、strongSelf、@weakify、@strongify解决循环引用

111194012-d3d8244be4e6059f

前言

在上篇中,仔细分析了一下Block的实现原理以及__block捕获外部变量的原理。然而实际使用Block过程中,还是会遇到一些问题,比如Retain Circle的问题。

目录

  • 1.Retain Circle的由来
  • 2.weak、strong的实现原理
  • 3.weakSelf、strongSelf的用途
  • 4.@weakify、@strongify实现原理

一. Retain Circle的由来

121194012-2eb65c853e014ed3

循环引用的问题相信大家都很理解了,这里还是简单的提一下。

当A对象里面强引用了B对象,B对象又强引用了A对象,这样两者的retainCount值一直都无法为0,于是内存始终无法释放,导致内存泄露。所谓的内存泄露就是本应该释放的对象,在其生命周期结束之后依旧存在。

131194012-aeffe9c77bf9ef5c

这是2个对象之间的,相应的,这种循环还能存在于3,4……个对象之间,只要相互形成环,就会导致Retain Cicle的问题。

当然也存在自身引用自身的,当一个对象内部的一个obj,强引用的自身,也会导致循环引用的问题出现。常见的就是block里面引用的问题。

141194012-e6f47d76a23b40c6

二.weak、strong的实现原理

在ARC环境下,id类型和对象类型和C语言其他类型不同,类型前必须加上所有权的修饰符。

所有权修饰符总共有4种:

1.strong修饰符 2.weak修饰符 3.unsafe_unretained修饰符 4.autoreleasing修饰符

一般我们如果不写,默认的修饰符是__strong。

要想弄清楚strong,weak的实现原理,我们就需要研究研究clang(LLVM编译器)和objc4 Objective-C runtime库了。

关于clang有一份关于ARC详细的文档,有兴趣的可以仔细研究一下文档里面的说明和例子,很有帮助。

以下的讲解,也会来自于上述文档中的函数说明。

151194012-8ba2276f15bbfd49
1.__strong的实现原理
(1)对象持有自己

首先我们先来看看生成的对象持有自己的情况,利用alloc/new/copy/mutableCopy生成对象。

当我们声明了一个__strong对象

LLVM编译器会把上述代码转换成下面的样子

相应的会调用

上述这些方法都好理解。在ARC有效的时候就会自动插入release代码,在作用域结束的时候自动释放。

(2)对象不持有自己

生成对象的时候不用alloc/new/copy/mutableCopy等方法。

LLVM编译器会把上述代码转换成下面的样子

查看LLVM文档,其实是下述的过程

相应的会调用

与之前对象会持有自己的情况不同,这里多了一个objc_retainAutoreleasedReturnValue函数。

这里有3个函数需要说明:

1.id objc_retainAutoreleaseReturnValue(id value)

id objc_retainAutoreleaseReturnValue(id value); Precondition: value is null or a pointer to a valid object.

If value is null, this call has no effect. Otherwise, it performs a retain operation followed by the operation described in objc_autoreleaseReturnValue.

Equivalent to the following code: id objc_retainAutoreleaseReturnValue(id value) { return objc_autoreleaseReturnValue(objc_retain(value)); }

Always returns value

2.id objc_retainAutoreleasedReturnValue(id value)

id objc_retainAutoreleasedReturnValue(id value); Precondition: value is null or a pointer to a valid object.

If value is null, this call has no effect. Otherwise, it attempts to accept a hand off of a retain count from a call to objc_autoreleaseReturnValue on value in a recently-called function or something it calls. If that fails, it performs a retain operation exactly like objc_retain.

Always returns value

3.id objc_autoreleaseReturnValue(id value)

id objc_autoreleaseReturnValue(id value); Precondition: value is null or a pointer to a valid object.

If value is null, this call has no effect. Otherwise, it makes a best effort to hand off ownership of a retain count on the object to a call toobjc_retainAutoreleasedReturnValue for the same object in an enclosing call frame. If this is not possible, the object is autoreleased as above.

Always returns value

这3个函数其实都是在描述一件事情。 it makes a best effort to hand off ownership of a retain count on the object to a call to objc_retainAutoreleasedReturnValue for the same object in an enclosing call frame。

这属于LLVM编译器的一个优化。objc_retainAutoreleasedReturnValue函数是用于自己持有(retain)对象的函数,它持有的对象应为返回注册在autoreleasepool中对象的方法或者是函数的返回值。

在ARC中原本对象生成之后是要注册到autoreleasepool中,但是调用了objc_autoreleasedReturnValue 之后,紧接着调用了 objc_retainAutoreleasedReturnValue,objc_autoreleasedReturnValue函数会去检查该函数方法或者函数调用方的执行命令列表,如果里面有objc_retainAutoreleasedReturnValue()方法,那么该对象就直接返回给方法或者函数的调用方。达到了即使对象不注册到autoreleasepool中,也可以返回拿到相应的对象。

2.__weak的实现原理

声明一个__weak对象

假设这里的strongObj是一个已经声明好了的对象。

LLVM转换成对应的代码

相应的会调用

看看文档描述

id objc_initWeak(id *object, id value); Precondition: object is a valid pointer which has not been registered as a __weak object.

value is null or a pointer to a valid object. If value is a null pointer or the object to which it points has begun deallocation, object is zero-initialized. Otherwise, object is registered as a __weak object pointing to value

Equivalent to the following code: id objc_initWeak(id _object, id value) { _object = nil; return objc_storeWeak(object, value); }

Returns the value of object after the call. Does not need to be atomic with respect to calls to objc_storeWeak on object

objc_initWeak的实现其实是这样的

会把传入的object变成0或者nil,然后执行objc_storeWeak函数。

那么objc_destoryWeak函数是干什么的呢?

void objc_destroyWeak(id *object); Precondition: object is a valid pointer which either contains a null pointer or has been registered as a __weak object.

object is unregistered as a weak object, if it ever was. The current value of object is left unspecified; otherwise, equivalent to the following code:

void objc_destroyWeak(id *object) { objc_storeWeak(object, nil); }

Does not need to be atomic with respect to calls to objc_storeWeak on object

objc_destoryWeak函数的实现

也是会去调用objc_storeWeak函数。objc_initWeak和objc_destroyWeak函数都会去调用objc_storeWeak函数,唯一不同的是调用的入参不同,一个是value,一个是nil。

那么重点就都落在objc_storeWeak函数上了。

id objc_storeWeak(id *object, id value); Precondition: object is a valid pointer which either contains a null pointer or has been registered as a __weak object. value is null or a pointer to a valid object.

If value is a null pointer or the object to which it points has begun deallocation, object is assigned null and unregistered as a weak object. Otherwise, object is registered as a weak object or has its registration updated to point to value

Returns the value of object after the call.

objc_storeWeak函数的用途就很明显了。由于weak表也是用Hash table实现的,所以objc_storeWeak函数就把第一个入参的变量地址注册到weak表中,然后根据第二个入参来决定是否移除。如果第二个参数为0,那么就把__weak变量从weak表中删除记录,并从引用计数表中删除对应的键值记录。

所以如果weak引用的原对象如果被释放了,那么对应的weak对象就会被指为nil。原来就是通过objc_storeWeak函数这些函数来实现的。

以上就是ARC中strong和weak的简单的实现原理,更加详细的还请大家去看看这一章开头提到的那个LLVM文档,里面说明的很详细。

161194012-3c3eea3397b741f6

三.weakSelf、strongSelf的用途

在提weakSelf、strongSelf之前,我们先引入一个Retain Cicle的例子。

假设自定义的一个student类

例子1:

到这里,大家应该看出来了,这里肯定出现了循环引用了。student的study的Block里面强引用了student自身。根据上篇文章的分析,可以知道,_NSConcreteMallocBlock捕获了外部的对象,会在内部持有它。retainCount值会加一。

我们用Instruments来观察一下。添加Leak观察器。

当程序运行起来之后,在Leak Checks观察器里面应该可以看到红色的❌,点击它就会看到内存leak了。有2个泄露的对象。Block和Student相互循环引用了。

171194012-8c51133ef5b64e01

打开Cycles & Roots 观察一下循环的环。

181194012-b32b093f6ddf412c

这里形成环的原因block里面持有student本身,student本身又持有block。

那再看一个例子2:

我把block新传入一个参数,传入的是student.name。这个时候会引起循环引用么?

答案肯定是不会。

191194012-345a97d7a6eb607c

如上图,并不会出现内存泄露。原因是因为,student是作为形参传递进block的,block并不会捕获形参到block内部进行持有。所以肯定不会造成循环引用。

再改一下。看例子3:

这样会形成循环引用么?

201194012-4ec0577cf7a01f6f

答案也是否定的。

ViewController虽然强引用着student,但是student里面的blcok强引用的是viewController的name属性,并没有形成环。如果把上述的self.name改成self,也依旧不会产生循环引用。因为他们都没有强引用这个block。

那遇到循环引用我们改如何处理呢??类比平时我们经常写的delegate,可以知道,只要有一边是__weak就可以打破循环。

先说一种做法,利用__block解决循环的做法。例子4:

这样写会循环么?看上去应该不会。但是实际上却是会的。

211194012-544bc137d452c71b

221194012-92ae745538414431

由于没有执行study这个block,现在student持有该block,block持有block变量,block变量又持有student对象。3者形成了环,导致了循环引用了。 想打破环就需要破坏掉其中一个引用。__block不持有student即可。

只需要执行一下block即可。例子5:

这样就不会循环引用了。

231194012-8d5dc8c77796e5b2

使用block解决循环引用虽然可以控制对象持有时间,在block中还能动态的控制是block变量的值,可以赋值nil,也可以赋值其他的值,但是有一个唯一的缺点就是需要执行一次block才行。否则还是会造成循环引用。

值得注意的是,在ARC下__block会导致对象被retain,有可能导致循环引用。而在MRC下,则不会retain这个对象,也不会导致循环引用。

接下来可以正式开始讲讲weakSelf 和 strongSelf的用法了。

1.weakSelf

说道weakSelf,需要先来区分几种写法。 weak typeof(self)weakSelf = self; 这是AFN里面的写法。。

define WEAKSELF typeof(self) __weak weakSelf = self; 这是我们平时的写法。。

先区分typeof() 和 typeof() 由于笔者一直很崇拜AFNetWorking的作者,这个库里面的代码都很整洁,里面各方面的代码都可以当做代码范本来阅读。遇到不懂疑惑的,都要深究,肯定会有收获。这里就是一处,平时我们的写法是不带的,AFN里面用这种写法有什么特殊的用途么?

在SOF上能找到相关的答案

typeof() and __typeof() are compiler-specific extensions to the C language, because standard C does not include such an operator. Standard C requires compilers to prefix language extensions with a double-underscore (which is also why you should never do so for your own functions, variables, etc.) typeof() is exactly the same, but throws the underscores out the window with the understanding that every modern compiler supports it. (Actually, now that I think about it, Visual C++ might not. It does support decltype() though, which generally provides the same behaviour as typeof().) All three mean the same thing, but none are standard C so a conforming compiler may choose to make any mean something different.

其实两者都是一样的东西,只不过是C里面不同的标准,兼容性不同罢了。

更加详细的官方说明

那么抽象出来就是这2种写法。

define WEAKSELF __weak typeof(self)weakSelf = self;

define WEAKSELF typeof(self) __weak weakSelf = self;

这样子看就清楚了,两种写法就是完全一样的。

我们可以用WEAKSELF来解决循环引用的问题。例子6:

这样就解决了循环引用的问题了。

解决循环应用的问题一定要分析清楚哪里出现了循环引用,只需要把其中一环加上weakSelf这类似的宏,就可以解决循环引用。如果分析不清楚,就只能无脑添加weakSelf、strongSelf,这样的做法不可取。

在上面的例子3中,就完全不存在循环引用,要是无脑加weakSelf、strongSelf是不对的。在例子6中,也只需要加一个weakSelf就可以了,也不需要加strongSelf。

曾经在segmentfault也看到过这样一个问题,问:为什么iOS的Masonry中的self不会循环引用?

> 如果我用blocksKit的bk_addEventHandler > 方法, 其中使用strong self, 该viewController就无法dealloc, 我理解是因为,self retain self.view, retain testButton, retain self. 但是如果只用Mansonry的mas_makeConstraints > 方法, 同样使用strong self, 该viewController却能正常dealloc, 请问这是为什么, 为什么Masonry没有导致循环引用? 看到这里,读者应该就应该能回答这个问题了。

在Masonry这个block中,block仅仅捕获了self的translatesAutoresizingMaskIntoConstraints变量,但是并没有持有self。

上述描述有误,感谢@酷酷的哀殿 耐心指点

更正如下:

关于 Masonry ,它捕获了变量 self,然后对其执行了setTranslatesAutoresizingMaskIntoConstraints:方法。但是,因为执行完毕后,block会被销毁,没有形成环。所以,没有引起循环依赖。

2.strongSelf

上面介绍完了weakSelf,既然weakSelf能完美解决Retain Circle的问题了,那为何还需要strongSelf呢?

还是先从AFN经典说起,以下是AFN其中的一段代码:

如果block里面不加strong typeof(weakSelf)strongSelf = weakSelf会如何呢?

输出:

为什么输出是这样的呢?

重点就在dispatch_after这个函数里面。在study()的block结束之后,student被自动释放了。又由于dispatch_after里面捕获的weak的student,根据第二章讲过的weak的实现原理,在原对象释放之后,__weak对象就会变成null,防止野指针。所以就输出了null了。

那么我们怎么才能在weakSelf之后,block里面还能继续使用weakSelf之后的对象呢?

究其根本原因就是weakSelf之后,无法控制什么时候会被释放,为了保证在block内不会被释放,需要添加__strong。

在block里面使用的__strong修饰的weakSelf是为了在函数生命周期中防止self提前释放。strongSelf是一个自动变量当block执行完毕就会释放自动变量strongSelf不会对self进行一直进行强引用。

输出

至此,我们就明白了weakSelf、strongSelf的用途了。

weakSelf 是为了block不持有self,避免Retain Circle循环引用。在 Block 内如果需要访问 self 的方法、变量,建议使用 weakSelf。

strongSelf的目的是因为一旦进入block执行,假设不允许self在这个执行过程中释放,就需要加入strongSelf。block执行完后这个strongSelf 会自动释放,没有不会存在循环引用问题。如果在 Block 内需要多次 访问 self,则需要使用 strongSelf。

关于Retain Circle最后总结一下,有3种方式可以解决循环引用。

结合《Effective Objective-C 2.0》(编写高质量iOS与OS X代码的52个有效方法)这本书的例子,来总结一下。

EOCNetworkFetcher.h

EOCNetworkFetcher.m

EOCClass.m

在这个例子中,存在3者之间形成环

1、completion handler的block因为要设置_fetchedData实例变量的值,所以它必须捕获self变量,也就是说handler块保留了EOCClass实例;

2、EOCClass实例通过strong实例变量保留了EOCNetworkFetcher,最后EOCNetworkFetcher实例对象也会保留了handler的block。

书上说的3种方法来打破循环。

方法一:手动释放EOCNetworkFetcher使用之后持有的_networkFetcher,这样可以打破循环引用

方法二:直接释放block。因为在使用完对象之后需要人为手动释放,如果忘记释放就会造成循环引用了。如果使用完completion handler之后直接释放block即可。打破循环引用

方法三:使用weakSelf、strongSelf

241194012-59b08429238b088d

四.@weakify、@strongify实现原理

上面讲完了weakSelf、strongSelf之后,接下来再讲讲@weakify、@strongify,这两个关键字是RAC中避免Block循环引用而开发的2个宏,这2个宏的实现过程很牛,值得我们学习。

@weakify、@strongify的作用和weakSelf、strongSelf对应的一样。这里我们具体看看大神是怎么实现这2个宏的。

直接从源码看起来。

看到这种宏定义,咋一看什么都不知道。那就只能一层层的往下看。

1. weakify

先从weakify(…)开始。

这里在debug模式下使用@autoreleasepool是为了维持编译器的分析能力,而使用@try/@catch 是为了防止插入一些不必要的autoreleasepool。rac_keywordify 实际上就是autoreleasepool {}
的宏替换。因为有了autoreleasepool {}的宏替换,所以weakify要加上@,形成@autoreleasepool {}。

__VA_ARGS__:总体来说就是将左边宏中 … 的内容原样抄写在右边 __VA_ARGS__ 所在的位置。它是一个可变参数的宏,是新的C99规范中新增的,目前似乎只有gcc支持(VC从VC2005开始支持)。

那么我们使用@weakify(self)传入进去。__VA_ARGS__相当于self。此时我们可以把最新开始的weakify套下来。于是就变成了这样:

rac_weakify_,, __weak, __VA_ARGS__整体替换MACRO, SEP, CONTEXT, …

这里需要注意的是,源码中就是给的两个”,”逗号是连着的,所以我们也要等效替换参数,相当于SEP是空值。

替换完成之后就是下面这个样子:

现在我们需要弄懂的就是metamacro_concat 和 metamacro_argcount是干什么用的。

继续看看metamacro_concat 的实现

## 是宏连接符。举个例子:

假设宏定义为#define XNAME(n) x##n,代码为:XNAME(4),则在预编译时,宏发现XNAME(4)与XNAME(n)匹配,则令 n 为 4,然后将右边的n的内容也变为4,然后将整个XNAME(4)替换为 x##n,亦即 x4,故 最终结果为 XNAME(4) 变为 x4。所以A##B就是AB。

metamacro_argcount 的实现

metamacro_concat是上面讲过的连接符,那么metamacro_at, N = metamacro_atN,由于N = 20,于是metamacro_atN = metamacro_at20。

metamacro_at20的作用就是截取前20个参数,剩下的参数传入metamacro_head。

metamacro_head的作用返回第一个参数。返回到上一级metamacro_at20,如果我们从最源头的@weakify(self),传递进来,那么metamacro_at20(self,20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1),截取前20个参数,最后一个留给metamacro_head_(1),那么就应该返回1。

metamacro_concat(metamacro_foreach_cxt, metamacro_argcount(self)) = metamacro_concat(metamacro_foreach_cxt, 1) 最终可以替换成metamacro_foreach_cxt1。

在源码中继续搜寻。

metamacro_foreach_cxt这个宏定义有点像递归,这里可以看到N 最大就是20,于是metamacro_foreach_cxt19就是最大,metamacro_foreach_cxt19会生成rac_weakify_(0,__weak,_18),然后再把前18个数传入metamacro_foreach_cxt18,并生成rac_weakify_(0,__weak,_17),依次类推,一直递推到metamacro_foreach_cxt0。

metamacro_foreach_cxt0就是终止条件,不做任何操作了。

于是最初的@weakify就被替换成

代入参数

最终需要解析的就是racweakify

把(0,weak,self)的参数替换进来(INDEX, CONTEXT, VAR)。 INDEX = 0, CONTEXT = weak,VAR = self,

于是

最终@weakify(self) = weak typeof_(self) self_weak = self;

这里的selfweak 就完全等价于我们之前写的weakSelf。

2. strongify

再继续分析strongify(…)

rac_keywordify还是和weakify一样,是autoreleasepool {},只为了前面能加上@

strongify比weakify多了这些_Pragma语句。

关键字_Pragma是C99里面引入的。_Pragma比#pragma(在设计上)更加合理,因而功能也有所增强。

上面的等效替换

这里的clang语句的作用:忽略当一个局部变量或类型声明遮盖另一个变量的警告。

最初的

strongify里面需要弄清楚的就是metamacroforeach 和 rac_strongify

我们先替换一次,SEP = 空 , MACRO = racstrongifyVA_ARGS , 于是替换成这样。

根据之前分析,metamacroforeach_cxt再次等效替换,metamacro_foreach_cxt##1(metamacro_foreach_iter,,rac_strongify,self)

根据

再次替换成metamacroforeach_iter(0, rac_strongify, self)

继续看看metamacro_foreach_iter的实现

最终替换成racstrongify(0,self)

INDEX = 0, VAR = self,于是@strongify(self)就等价于

注意@strongify(self)只能使用在block中,如果用在block外面,会报错,因为这里会提示你Redefinition of ‘self’。

总结一下

@weakify(self) = @autoreleasepool{} weak typeof_ (self) self_weak = self;

@strongify(self) = @autoreleasepool{} strong typeof_(self) self = self_weak;

经过分析以后,其实@weakify(self) 和 @strongify(self) 就是比我们日常写的weakSelf、strongSelf多了一个@autoreleasepool{}而已,至于为何要用这些复杂的宏定义来做,目前我还没有理解。如果有大神指导其中的原因,还请多多指点。

更新

针对文章中给的例子3,大家都提出了疑问,为何没有检测出循环引用?其实这个例子有点不好。因为这个ViewController的引用计数一出来就是6,因为它被其他很多对象引用着。当然它是强引用了student,因为student的retainCount值是2。ViewController释放的时候才会把student的值减一。针对这个例子3,我重新抽取出中间的模型,重新举一个例子。

既然ViewController特殊,那我们就新建一个类。

251194012-81da182083ef4625
261194012-3b483f1ccd94e77c

如图所示,还是出现了循环引用,student的block强引用了teacher,teacher又强引用了student,导致两者都无法释放。

打赏支持我写出更多好文章,谢谢!

打赏作者

打赏支持我写出更多好文章,谢谢!

任选一种支付方式

1 4 收藏 4 评论

关于作者:一缕殇流化隐半边冰霜

已退役ACMer,现役iOS工程师。博观而约取,厚积而薄发。 个人主页 · 我的文章 · 5 ·   

相关文章

可能感兴趣的话题



直接登录
最新评论
跳到底部
返回顶部