当前位置:首页 > 芯闻号 > 充电吧
[导读]摘要:对许多开发者而言,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 

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

电子数据的存储与共享在我们生活中占据越来越重要的地位,而传统的硬盘存储已然难以满足人们日益增长的数据存储需求,为此网络附加存储(NAS)则以其便捷、高效的特点,逐渐受到广大用户的青睐。但是提到NAS,很多人可能会觉得它是...

关键字: 存储 铁威马NAS 硬盘存储

4月25日,以“分享鸿蒙技术特性,交流鸿蒙生态共建”为主题的HDD·行业沙龙在江西武功山成功举行。华为产品专家们现场带来了诸多精彩分享,吸引了来自政务、金融、新闻资讯等多个行业的四十余家软件服务商到场参加。

关键字: 鸿蒙 华为 智能设备

4月25日,2024(第十八届)北京国际汽车展览会拉开序幕,车展以“新时代·新汽车”为主题,一直持续到5月4日。本次车展将有全球首发车117台(其中跨国公司全球首发车30台),41款概念车及278款新能源车型展出。

关键字: 北京车展 新能源汽车 电动汽车

LED驱动模块RSC6218A 5W-18W迷你高效驱动电源应用,小功率、小体积、高效率

关键字: LED驱动模块 驱动电源应用 LED电源芯片

业内消息,近日台积电在北美技术研讨会上宣布,正在研发 CoWoS 封装技术的下个版本,可以让系统级封装(SiP)尺寸增大两倍以上,实现 120x120mm 的超大封装,功耗可以达到千瓦级别。

关键字: CoWoS 台积电 封装

据外媒报道,字节正在内部探索出售TikTok美国业务多数股权,并援引内部人士披露的信息称 “沃尔玛或为最理想买家”。报道还称,讨论中的一种情况是字节出售美国50%以上TikTok股份,但保留少数股权。

关键字: 字节跳动 TikTok

业内消息,HMD 正在计划重启一些经典的诺基亚功能手机。今年 3 月初,该公司预告了将于 5 月发布的一款功能手机。现在该机的身份已经曝光,新款诺基亚 3210 的谍照已经泄露,展现了新机部分新特性。

关键字: 诺基亚 功能机 HMD

业内消息,近日有一位网友在各大社交媒体发文表示,自己离职后,公司将自己所有的期权全部作废。

关键字: 期权 微博

业内消息,在昨天的中关村论坛未来人工智能先锋论坛上,生数科技联合清华大学正式发布中国首个长时长、高一致性、高动态性视频大模型——Vidu。Vidu是自Sora发布之后全球率先取得重大突破的视频大模型,性能全面对标Sora...

关键字: Sora 清华 AI Vidu

近日,2024中关村论坛年会发布了10项重大科技成果名单,其中“转角氮化硼光学晶体原创理论与材料”备受关注。

关键字: 激光
关闭
关闭