理解 ARC 下的循环引用
来自伯乐在线团队之内-nathanw的翻译成果, 予以dopcn进行校稿, 形成了此篇译文, 要是未经得到许可, 不准许进行转载这个禁止令!
数字树叶网站的英文出处是, digitalleaves.com。诚挚欢迎各位加入翻译组。
刚成为苹果开发者时, 或许不会关心 ARC 下类似于日本 B 级恐怖片存在的循环引用。直到某天一个 app 因内存泄露闪退, 才突然意识到其存在, 且发现循环引用如幽灵般在代码各角落存在。年复一年, 开始学会处理循环引用、检测并避免它们, 可这部片子恐怖结局仍在, 随时可能出现。
ARC存在令诸多开发者(比方说我)失望之处, 其一为苹果留存了借助ARC实施内存管理之举, ARC颇为不幸地未涵盖循环引用检测器, 于是极易生成循环引用, 进而迫使开发者于编写代码之际采取某些特别的防范举措。
不少 iOS 开发者一直对循环引用疑惑费解, 就有些蒙圈。网上不少误导信息, 那些文章给出的建议不对, 修复方法也错, 那错误搞不好还能引发问题, 致使 app 闪退。在这篇文章里, 我要针对这些问题讲明白、说清楚。
理论简介
手动内存管理在内存管理中是可追溯的, 其简称为 MSR。在 MRR 里, 开发者所创建的每一个对象, 都要声明其拥有权, 以此来维持对象于内存中的存在, 当对象不再被需要时, 便撤销拥有权将其释放。MRR 通过引用计数系统达成这套拥有权体系, 这意味着每个对象都设有一个计数器, 凭借计数增加 1 来表明被一个对象所拥有, 而计数减 1 则表明不再持有。当计数变为零的时候, 对象即为得以释放。因手动管理内存着实烦扰人, 所以苹果推出自动引用计数即ARC以此解放开发者, 使之不用再劳力费神于亲自施加retain以及由添加release构成的相关具体操纵, 进而致力于App得以投身开展。于ARC情形下, 开发者会把一个变量界定为具备两种属性的其中之一, 要么是“strong”, 要么是“weak”。一种称作’weak”的具备脆弱特点引用是没有能力去留存对象的, 而被标记为“strong”的具备强劲特点引用不但能够保管留存对象, 还会使得对象它自身所携带的引用计数发生数量递进变为加起来大于一的精确数值。
我为什么要关心这些?
ARC的问题在于, 循环引用极易出现。具体而言, 当两个各异的对象, 彼此都有一个强引用指向对方时, 循环引用就这么产生了。来设想一下, 有一个book对象, 它持有多个page对象, 而每个page对象又分别有一个属性, 用以指向其所属的book对象。当你将持有book对象与page对象的变量释放之后, 它们依然存在着强引用, 分别指向各自, 所以即便已经不存在变量持有它们, 你也无法释放它们所占用的内存。
很不幸, 在实际当中, 循环引用可不是那么容易就能够被发现的。多个对象之间, 也就是 A 持有 B 的情况下, 偏偏 B 又持有 C, 可巧, C 居然也抱住了 A 不撒手, 循环引用随之产生。更令人发愁的是, Objective - C代码片段 以及 Swift 闭包, 这些都是独立的内存对象, 会对它们所引用的对象死死抱住、绝不放松, 如此一来, 潜在的循环引用问题就冒出来了。
循环引用存在对 app 的潜在危害, 它会致使内存消耗过高, 性能变差以致使 app 闪退等情况。但, 苹果文档却对于会可能发生循环引用的各种场景以及怎样去切实避免并没有给出詳细描述, 然而这就常常容易造成某些误解以及不良的编程习惯。
一些用例模拟
不多说废话, 我们一同来剖析某些场景里会不会出现循环引用 , 以及怎样去规避它。
父子对象关系
父子对象关系是典型案例, 此案例是循环引用类型, 不幸在于它是苹果文档里现存唯一案例, 事实上就是前文提及 Book 与 Page 的那一案例。惯常解决办法为, 在子类范畴定义指向父类的变量, 将该变量声明成 weak 弱引用类型, 以此来规避循环引用情况。
在swift里, 子类指向父对象变量是弱引用, 这种情况迫使我们把该弱引用定义成optional类型。要是不使用optional, 能有另外做法, 把指向父对象变量声明为“无主引用(unowned)”(意味着我们不持有该对象, 也不对其做内存管理)。然而处在这种情形下, 咱们必须极为小心, 确保只要还有子对象指向它, 父对象就别变成nil, 不然会直接闪退。
有效的常规做法是, 父对象得持有(强引用)子对象, 子对象只需维持向其的一个弱引用。这对集合对象同样适用, 集合对象得持有其所包含的对象。
当 Block 和闭包包含在类的成员变量中
除这样一个典型示例外, 或许并不那样直观。如同我们先前阐释的那般, 闭包以及block属于独立的内存对象, 会对它们所引用的对象进行retain操作, 所以要是我们存在一个类, 类里有个闭包变量, 并且此闭包恰巧引用了自身所属对象的某一属性或者方法, 那么便极有可能产生循环引用, 原因是闭包会创建强引用捕获“self”。
解决这个案例的办法是去定义一个具有较弱特质的 self, 而后在闭包亦或是 block 之中进行运用, 在 objective-C 里, 我们会去定义一个全新的变量:

然而在 Swift 我们只需要在闭包的头部声明 “
weak self in
”:
采用这样的方式, 于闭包完结之际, 内部的self变量不会遭受强引用, 因而它会被予以释放, 进而破除了循环引用。留意一下当self被声明做weak时, 闭包内部的self是一个可选值。
GCD: dispatch_async
与我们平常所觉得的不一样, dispatch_async自身不会导致循环引用。
在这个地方, 闭包对于self会构成强引用,不过实例化的self并不会对闭包展开强引用情形, 那么一旦闭包结束, 它就是会得到释放态势,如此一来循环引用也是不会出现的。然而, 一直都存在着一些开发者, 他们觉得它有可能产生循环引用状况。有部分开发者可能甚至这样来认为, 所有处于block以及闭包内部的self都是需要进行弱引用处理的:
于我而言, 针对每种情形都运用这种方式并非是一种良好的实践行为。咱们设想一下, 要是我们存在一个对象, 其用途是发送一项后台任务(举例来说就是下载数据), 而且调用了 self 的一个方法。此时倘若我们对 self 进行弱引用, 该对象的生命周期在闭包结束之前早些就结束从而被释放, 于是当我们的闭苞调用 doSomething()方法时, 这个对象有可能已然不存在了, 方法也就无法得到执行。恰当的解决办法是(苹果所推荐的)在闭包内部, 声明一个强引用去指向弱引用。
我感觉这种语法不但令人作呕、枯燥无味、毫不直观, 还违背了闭包作为一项独立处置实体的准则。要懂得领会对象的生存时长, 并清楚何时该声明弱引用, 以及知晓对象生存时期的重要含义。这是相当关键的。然而, 这却致使我分散注意力, 进而无法全神贯注于 app 开发的问题自身。倘若 Cocoa 并不运用 ARC, 那就没必要编写此种代码了。
本地闭包和 block
函数的闭包以及block, 要是没有去引用任何的实例或者类变量, 那么其自身也不会导致循环引用的情况出现。极为常见的其中一个例子便是UIView的animateWithDuration。
正如 dispatch_async 以及其他与其关联的 GCD 相关方法那样, 我们无需对局部变量闭包与 block 造成的循环引用予以担忧。
代理协议
代理协议同样是个典型场景, 这要求你运用弱引用来防止循环引用, 把代理声明为weak乃是一种既好且安全的做法:
设置一个属性, 该属性无原子性, 是弱引用类型, 其类型为id类型, 名称为delegate。
在 swift:
弱的变量, 委托, 类型为, 我的自定义委托, 问号表示可为空。
大多数情形下, 一个对象的代理持有一个实例化对象, 或者其生命周期应长于该对象, 以便响应代理方法, 所以一个设计优良的类不应需要我们去思考任何与此相关的生命周期问题。
使用 Instruments 调试循环引用
每当我竭尽全力地认真细致去做时, 有时依旧会 forget 声明一个弱引用, 进而不经意间创建出一个全新的对象, (多亏ARC屁事无所作为呐!)。然而幸运降临此际XCode自身便存在具有一大强大无比的工具Instruments, 它专门用, 于查找以及确定处于循环状态的某种特别指引情况。就像一旦你的app开发工作宣告完成, 紧接着就要提交至Apple Store, 这个地方, 预先去剖析你的app, 这无疑算是一种良好的习惯。Instruments具备着诸多组件, 这些组件能够用以对app的不同方面展开分析, 然而当前受到我们关注的却是Leak选项。
一旦 Instruments 启动起来, 你的应用就理应随之启动运转起来, 跟着去执行一些交互行为动作, 尤其像是你所想专注测试的区域或者是相应的视图控制器这些方面。凡是被探查到检测到的泄露之处, 统统都会以一条呈现为红色的线条形状显示在 Leaks 这个区域范围里面。Assistant 视图那儿就会显示出针对泄露情况而去展开的栈追踪相关呈现内容, 甚至于能够直接精准定位到出现问题状况对应的代码程序那里。