当前位置:首页 > > 充电吧
[导读]摘要:对许多开发者而言,ARC最令人失望之处莫过于苹果公司让ARC来管理内存。不幸的是ARC没有循环引用检测器,因此很容易出现Retain Cycle现象,从而迫使开发者在编码时要采取特殊的预防措施。

摘要:对许多开发者而言,ARC最令人失望之处莫过于苹果公司让ARC来管理内存。不幸的是ARC没有循环引用检测器,因此很容易出现Retain Cycle现象,从而迫使开发者在编码时要采取特殊的预防措施。

ARC中的Retain Cycle就像日本B级恐怖电影一样。开始使用Cocoa或Cocoa Touch做开发时,你甚至不会在意它的存在。直到有一天应用程序由于内存泄漏而出现了崩溃现象,你才意识到它们的存在,看到像幽灵一样的Retain Cycle无处不在。随着岁月流逝,你学会适应它们,发现它们,避免它们……但最终恐慌还在,无孔不入。

包括我在内,对于许多开发人员来说,ARC的最令人失望之处莫过于苹果公司让ARC来管理内存。不幸的是ARC没有循环引用检测器,因此很容易出现Retain Cycle现象,从而迫使开发人员在编码时要采取特殊的预防措施。

对于iOS开发人员来说,Retain Cycle是个难点。在网上有很多误导信息[1][2],人们所给出的这些错误信息和修复方法甚至会导致应用出现新的问题,甚至崩溃掉,基于这样的情况,本文中我会阐明主题,给读者一些启发。

相关理论一瞥

Cocoa框架内存管理可以追溯到MRR(Manual Retain Release),在MRR中,开发人员在创建对象的时候,要为每个内存中的对象声明所有权。并且,当不再需要该对象时,要放弃所有权。MRR通过引用计数系统来实现这种所有权机制。每个对象都被分配一个计数器指示被“拥有”了多少次,每次加一,释放对象的时候每次减一。当引用计数变成零的时候,该对象将不复存在。对于开发人员来说,不得不手动维护引用计数真的是很烦人的事情,于是苹果公司引入了自动引用计数(Automated Reference Counting, ARC)机制,免得开发人员手动添加保留(retain)和释放(release)指令,让他们专注于解决应用程序的问题。在ARC环境下,开发人员要将一个变量定义为“strong”或“weak”。使用weak的话应用程序中被声明的对象不会被retain,而使用strong声明的对象将会被retain,并且其引用计数加一。


为什么要在乎?

ARC的问题在于容易导致Retain Cycle,它发生在两个不同的对象间彼此包含强引用的时候。试想一个Book对象包含一系列的Page对象,每个Page对象有个属性指向该页所在的这本书。当你释放掉指向Book和Page的变量时,Book和Page之间还存在着强引用。因此,即使没有变量指向Book和Page了,Book和Page及其所占用的内存也不会被释放掉。

不幸之处在于并非所有Retain Cycle都很容易被发现。对象之间的传递关系(A引用B,B转而引用C,C引用A)会导致Retain Cycle。更糟糕的是Objective-C里的块(block)和Swift里的闭包(closure)都被认为是独立的内存对象。因此,任何在块或闭包内对像的引用都将会对其变量做retain操作。因此,如果对象仍然retain这个块的话,就会导致潜在的Retain Cycle发生。

Retain Cycle可以成为应用程序潜在的危害,导致内存消耗过高,性能低下和崩溃。但还没有来自于苹果公司的文档,针对Retain Cycle可能发生的不同场景,以及如何避免进行描述。这导致了一些误解并形成了不良的编程习惯。

用例场景

那么闲话少说,我们一起来分析一些场景,确定它们是否会导致Retain Cycle以及如何避免:

父子对象关系

这是Retain Cycle的典型例子。不幸的是这也是苹果公司唯一给出相关解决方案文档的例子。就是我上面描述的Book和Page对象的例子。这种情况的典型解决方案是把Child类里面的代表父类的变量定义成weak,这样就可以避免Retain Cycle。


[cpp] view plaincopyclass Parent {      var name: String      var child: Child?      init(name: String) {         self.name = name      }   }   class Child {      var name: String      weak var parent: Parent!      init(name: String, parent: Parent) {         self.name = name         self.parent = parent      }   }  


在Swift语言中,代表父类的变量是个弱变量的事实迫使我们将其定义为可选类型。不使用可选类型的另一种做法是将父类型对象声明为“unowned”(意味着我们不会对变量声明进行内存管理或声明所有权)。然而在这种情况下,我们必须非常仔细地确保只有一个Child实例指向Parent,Parent就不能是nil,否则程序就会崩溃:


[cpp] view plaincopyclass Parent {      var name: String      var child: Child?      init(name: String) {         self.name = name      }   }   class Child {      var name: String      unowned var parent: Parent      init(name: String, parent: Parent) {         self.name = name         self.parent = parent      }   }   var parent: Parent! = Parent(name: "John")   var child: Child! = Child(name: "Alan", parent: parent)   parent = nil   child.parent <== possible crash here!  


一般来说公认的做法是父对象必须拥有(强引用)其子对象,这些子对象对其父对象应该只保持一个弱引用。这同样适用于集合,集合必须拥有其所包含的对象。

包含在实例变量中的块和闭包

另一个经典的例子虽然不是那么直观,但正如我们之前所说的那样,闭包和块是独立的内存对象,并retain了它们所引用的对象。因此如果我们有一个包含闭包变量的类,这个变量又恰好引用了其所拥有对象的属性或方法,由于闭包通过创建一个强引用而“捕获”了自己,就会有Retain Cycle发生。


[cpp] view plaincopyclass MyClass {      lazy var myClosureVar = {         self.doSomething()      }   }  


这种情况下的解决方法是将自身定义成“weak”版本,并且将此弱引用赋给闭包或块。在Objective-C语言中,要定义个新的变量:


[cpp] view plaincopy- (id) init() {      __weak MyClass * weakSelf = self;      self.myClosureVar = ^{         [weakSelf doSomething];      }   }  


而在Swift语言中,我们只需要指定“[weak self] in”作为闭包的启动参数:


[cpp] view plaincopyvar myClosureVar = {      [weak self] in      self?.doSomething()   }  


这样一来,当闭包快执行完毕时,self变量不会被强制retain,因此会得到释放,打破循环。注意,当声明为weak时,self在闭包内是如何变成可选类型的。

GCD中的dispatch_async

与一贯的认识相反,dispatch_async本身并不会导致Retain Cycle。


[cpp] view plaincopydispatch_async(queue, { () -> Void in      self.doSomething();    });  


在这里,闭包对self强引用,但是类(self)的实例对闭包没有任何强引用,因此一旦闭包结束,它将被释放,也不会有环出现,然而,有的时候会被(错误地)认为这种情况会导致Retain Cycle。一些开发人员甚至一针见血地指出将块或闭包内所有对“self”的引用都声明为weak:


[cpp] view plaincopydispatch_async(queue, {      [weak self] in      self?.doSomething()   })  


在我看来,对每种情况都这样做并不是好的做法。我们假设某个对象启动了一个长时间的后台任务(比如从网络上下载一些东西),然后调用了一个“self”方法。如果对self传递一个弱引用的话,那么类会在闭包结束之前完成它的生命周期。因此,当调用 doSomething()的时候,类的实例已经不存在了,所以这个方法永远不会被执行。这种情况下,(苹果公司)建议的解决方案是对闭包内的弱引用(???)声明一个强引用:


[cpp] view plaincopydispatch_async(queue, {      [weak self] in      if let strongSelf = self {         strongSelf.doSomething()      }   })  


我不仅发现语法冗长单调,缺乏直观性,甚至令人感到厌恶,而且使闭包作为独立的处理实体的打算也落空了。我认为要理解对象的生命周期,确切地明白什么时候应该为实例声明一个内部的weak版,还要知道对象存续期间会有哪些影响。但话又说回来,这正是我解决应用程序的问题时分散我注意力的地方,如果Cocoa框架中没有使用ARC的话,这些都是没有必要写的代码。

局部的闭包和块

没有引用或包含任何实例或类变量的函数局部闭包和块本身不会导致Retain Cycle。常见的例子就是UIView的animateWithDuration方法:


[cpp] view plaincopyfunc myMethod() {      ...      UIView.animateWithDuration(0.5, animations: { () -> Void in         self.someOutlet.alpha = 1.0         self.someMethod()      })   }  


对于dispatch_async和其他GCD相关的方法,我们不用担心没有被类实例强引用的局部闭包和块,它们不会发生Retain Cycle。

代理方案

代理(delegation)是使用弱引用避免Retain Cycle的一个典型场景。将委托声明为weak一直是一种不错的做法(并且还算安全)。在Objective-C中:


[cpp] view plaincopy@property (nonatomic, weak) id 

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: 驱动电源

在工业自动化蓬勃发展的当下,工业电机作为核心动力设备,其驱动电源的性能直接关系到整个系统的稳定性和可靠性。其中,反电动势抑制与过流保护是驱动电源设计中至关重要的两个环节,集成化方案的设计成为提升电机驱动性能的关键。

关键字: 工业电机 驱动电源

LED 驱动电源作为 LED 照明系统的 “心脏”,其稳定性直接决定了整个照明设备的使用寿命。然而,在实际应用中,LED 驱动电源易损坏的问题却十分常见,不仅增加了维护成本,还影响了用户体验。要解决这一问题,需从设计、生...

关键字: 驱动电源 照明系统 散热

根据LED驱动电源的公式,电感内电流波动大小和电感值成反比,输出纹波和输出电容值成反比。所以加大电感值和输出电容值可以减小纹波。

关键字: LED 设计 驱动电源

电动汽车(EV)作为新能源汽车的重要代表,正逐渐成为全球汽车产业的重要发展方向。电动汽车的核心技术之一是电机驱动控制系统,而绝缘栅双极型晶体管(IGBT)作为电机驱动系统中的关键元件,其性能直接影响到电动汽车的动力性能和...

关键字: 电动汽车 新能源 驱动电源

在现代城市建设中,街道及停车场照明作为基础设施的重要组成部分,其质量和效率直接关系到城市的公共安全、居民生活质量和能源利用效率。随着科技的进步,高亮度白光发光二极管(LED)因其独特的优势逐渐取代传统光源,成为大功率区域...

关键字: 发光二极管 驱动电源 LED

LED通用照明设计工程师会遇到许多挑战,如功率密度、功率因数校正(PFC)、空间受限和可靠性等。

关键字: LED 驱动电源 功率因数校正

在LED照明技术日益普及的今天,LED驱动电源的电磁干扰(EMI)问题成为了一个不可忽视的挑战。电磁干扰不仅会影响LED灯具的正常工作,还可能对周围电子设备造成不利影响,甚至引发系统故障。因此,采取有效的硬件措施来解决L...

关键字: LED照明技术 电磁干扰 驱动电源

开关电源具有效率高的特性,而且开关电源的变压器体积比串联稳压型电源的要小得多,电源电路比较整洁,整机重量也有所下降,所以,现在的LED驱动电源

关键字: LED 驱动电源 开关电源

LED驱动电源是把电源供应转换为特定的电压电流以驱动LED发光的电压转换器,通常情况下:LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: LED 隧道灯 驱动电源
关闭