Swift中的内存泄漏

2018 年 5 月 21 日 CocoaChina


在这篇文章中,我会解释什么是内存泄漏(memory leaks),讨论循环引用(retain cycles)和其他事物。


内存泄漏


这确实是我们开发者经常面对的问题之一,我们的代码越来越复杂,随着app的增长,我们也带来了泄漏。


内存泄漏会永久占用一部分内存,让它无法再使用。它是占据空间带来问题的垃圾。


有时候我们分配内存,却再也没有释放,并且也没有app引用去。因为没有对它的引用,也就没有办法释放它,这段内存就无法再使用了。

Apple Docs


我们时不时在不断制造内存泄漏,无论是新手开发者还是业界老鸟。它和我们的经验无关。最重要的是要清除它们,让我们的app更干净,避免崩溃。为什么?因为它们很危险。


内存泄漏很危险


不仅是因为它们增加了app的内存占用(memory footprint),它们还带来了有害的副作用和崩溃。


为什么内存占用会增加呢?它直接来源于一些对象没有被释放。这些对象毫无益处。如果我们重复创建这些对象,占用的内存就会增加。这就太可怕了!这会导致内存警告,最终让app崩溃。


解释有害的副作用需要说明一些细节


设想一个对象在init中被创建时就开始听从一个通知。它响应通知,为一个数据库存储数据,播放视频,或者为分析仪器记录事件。因为对象需要被平衡,我们让它在deinit中被释放时不再听从这个通知。


如果这样的对象发生泄漏的话会发生什么呢?


它永远不会消失,会一直听从通知。每次通知传达,对象就会响应它。如果用户不断重复创建这样的对象,就会有多重实例存在。这些实例都会响应通知,并互相干涉。


这种情况下,最可能发生的就是崩溃了


多重泄漏的对象响应一个app通知,变更数据库,UI,使整个app崩溃。要想了解这类问题的重要性,你可以读一读发表在The Pragmatic Programmer.上的Dead programs tell no lies


泄漏必定会导致差劲的用户体验和App Store的低排名。


泄漏是从哪来的?


泄漏可能来源于第三方SDK或者一些框架。甚至是来源于苹果中的类,比如CALayer和UILabel。这种情况下我们也做不了什么,只能等待SDK的更新或者不用它。


但是泄漏更多的是我们的代码带来的,泄漏的第一原因就是循环引用(retain cycles)


为了避免泄漏,我们必须理解内存管理与循环引用


循环引用


引用这个词来源于Objective-C中的手动引用计数(Manual Reference Counting)。在ARC、Swift和其他对值类型的操作之前,我们都用Objective-C和MRC。可以在这篇文章中了解MRC和ARC


在那段时间,我们需要对内存管理稍加了解。需要理解关于分配,复制,保持和如何用反向操作平衡这些操作,比如释放。基本原则是:只要你创建了一个对象,你就拥有了它,并且要负责释放它。


如今事情就简单多了,但是依旧需要了解一些概念。


在Swift中,当一个对象对另一个对象有强关联,它就在引用(retain)它。这里我说的对象基本是指引用类型或者类。


结构(Struct)和枚举(Enums)是值类型。如果只有值类型,是不可能创建循环引用的。当获取并存储了值类型(结构和枚举),并不存在一个引用。值是被复制,而不是引用,尽管这个值可以含有对对象的引用。


当一个对象引用了另一个,它就拥有了它。第二个对象会始终保持存活,直到被释放。这就是强引用。只有你把属性设为空(nil),第二个对象才会被销毁。


class Server {
}

class Client {
   var server : Server //Strong association to a Server instance
   init (server : Server) {
       self.server = server
   }
}

强引用


如果A引用B,B引用A,就形成了循环引用

A || B + A || B =||


class Server {
   var clients : [Client] //Because this reference is strong
   func add(client:Client){
       self.clients.append(client)
   }
}

class Client {
   var server : Server //And this one is also strong
   init (server : Server) {
       self.server = server
       self.server.add(client:self) //This line creates a Retain Cycle -> Leak!
   }
}

一个循环引用


在本例中,释放client或者server都是不可能的


要想从内存中释放,一个对象必须先解除其所有关联性。因为如果对象本身是一个从属,它无法被释放。同样如果一个对象又一个循环引用,它就不会消失。


当循环中的一个引用是弱引用或无主引用时,循环引用会被破坏。循环必须存在,因为它是我们编码中关联性本质所必须的。问题就在于,不能所有的关联都是强的,其中一个必须是弱关联。


class Server {
   var clients : [Client]
   func add(client:Client){
       self.clients.append(client)
   }
}

class Client {
   weak var server : Server! //This one is weak
   init (server : Server) {
       self.server = server
       self.server.add(client:self) //Now there is no retain cycle
   }
}

一个弱引用破坏循环引用


怎样破坏循环引用


当你处理类类型的属性时,Swift提供了两种方法解决强引用循环:弱引用与无主引用


弱引用与无主引用允许一个引用循环中的实例引用其他实例,而不需要保持很强的关系。这样实例就可以在不创建强引用循环的情况下互相引用。


Apple’s Swift Programming Language


弱引用:一个变量可以选择不持有对其引用对象的拥有权。弱引用可以是空(nil)


无主引用:像弱引用,无主引用对引用对象不保持很强的关系。和弱引用不同的是,无主引用总是被设定为一个值。因此,无主引用总是被设定为不可选择的类型。无主引用不可以是空。


当一个闭包与其占有的实例总是互相引用,并且总是同时被释放时,我们定义闭包中的占有(capture)为循环引用。


相反地,如果被占有的引用在将来可能变为空时,我们定义这个占有为弱引用。弱引用总是选择类型,并且其引用的实例被释放时会自动变为空。

Apple’s Swift Programming Language


class Parent {
   var child : Child
   var friend : Friend
   
   init (friend: Friend) {
       self.child = Child()
       self.friend = friend
   }
   
   func doSomething() {
       self.child.doSomething( onComplete: { [unowned self] in  
             //The child dies with the parent, so, when the child calls onComplete, the Parent will be alive
             self.mustBeAlive()
       })
     
       self.friend.doSomething( onComplete: { [weak self] in
           // The friend might outlive the Parent. The Parent might die and later the friend calls onComplete.
             self?.mightNotBeAlive()
       })
   }
}

弱引用VS无主引用


当我们编码时,忘记weak self这种事并不少见。我们经常在编写区块闭包(block closures),比如flatMap或者包含互动代码的map,或者当我们编码观察者(observers), 委托(delegates)时带来内存泄漏。在这篇文章中你可以读到关于闭包中的泄漏。


怎样清除内存泄漏?


  1. 不要创建它们。深入理解内存管理,为你的项目制定好编码方式并遵照它。如果你很有条理,遵照编码方式,weak self的缺少就会显而易见。核心回顾(Core reviews)也是很有用的

  2. 使用Swift Lint。它会强制要求你遵照一个编码方式从而遵守规则1。它帮你在编译时检查早期问题。比如并非弱的,可能变为循环引用的委托变量声明。

  3. 在运行时检查泄漏,并让它们可见。如果你清楚某一个对象在某一时刻必须存在有多少实例,,你可以使用LifetimeTracker。它是开发模式下一个有用的工具。

  4. 频繁配置(Profile)app,XCode带来的内存分析工具能做的很好。

  5. 用我制作的SpecLeaks做内存泄漏的单元测试,它又快又灵敏,让你可以便捷地建立泄漏测试。


内存泄漏的单元测试


一旦我们知道循环和弱引用是怎么工作的,我们就可以为循环引用写测试代码,就是用弱引用探测循环。通过一个对对象的弱引用,我们可以测试对象是否泄漏。


因为弱引用和其引用实例并没有强的关联,释放一个弱引用引用的实例是可能的。因为ARC会自动把引用实例被释放的弱引用设为空。


让我们看看如果对象x泄漏了会怎样。我们可以为它创建一个弱引用,并称为leakReferece。如果x从内存中释放,ARC会设leakReference为空。所以如果x泄漏,leakReference就一定不是空。


func isLeaking() -> Bool {
   var x : SomeObject? = SomeObject()
   weak var leakReference = x
   x = nil
 
   if leakReference == nil {
       return false //Not leaking
   }
   else{
       return true //Leaking
   }
}

测试对象是否泄漏


如果x确实发生泄漏,弱变量leakReference会指向泄漏的实例。另一方面,如果对象没有发生泄漏,在设定它为空后,它将不再存在。这种情况下,leakReference会是空。


“Swift by Sundell”在文章中解释了几种内存泄漏。这篇文章对我的写作起到很大帮助。


相关推荐:


登录查看更多
0

相关内容

苹果公司在 WWDC 2014 开幕 Keynote 上发布的全新编程语言,具有更多现代化特性,同时容易使用,定位是补充 Objective-C. > Swift is an innovative new programming language for Cocoa and Cocoa Touch. Writing code is interactive and fun, the syntax is concise yet expressive, and apps run lightning-fast. Swift is ready for your next iOS and OS X project — or for addition into your current app — because Swift code works side-by-side with Objective-C.

Swift - Apple Developer

【2020新书】实战R语言4,323页pdf
专知会员服务
100+阅读 · 2020年7月1日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
117+阅读 · 2020年5月10日
【复旦大学-SP2020】NLP语言模型隐私泄漏风险
专知会员服务
24+阅读 · 2020年4月20日
【ICLR2020-】基于记忆的图网络,MEMORY-BASED GRAPH NETWORKS
专知会员服务
108+阅读 · 2020年2月22日
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
69+阅读 · 2020年1月17日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
22+阅读 · 2019年11月7日
在K8S上运行Kafka合适吗?会遇到哪些陷阱?
DBAplus社群
9+阅读 · 2019年9月4日
防代码泄漏的监控系统架构与实践
FreeBuf
5+阅读 · 2019年4月30日
使用 C# 和 Blazor 进行全栈开发
DotNet
6+阅读 · 2019年4月15日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
被动DNS,一个被忽视的安全利器
运维帮
11+阅读 · 2019年3月8日
Python高级技巧:用一行代码减少一半内存占用
AI研习社
5+阅读 · 2018年11月28日
占坑!利用 JenKins 持续集成 iOS 项目时遇到的问题
浅谈浏览器 http 的缓存机制
前端大全
6+阅读 · 2018年1月21日
Arxiv
7+阅读 · 2019年4月8日
Arxiv
3+阅读 · 2019年3月1日
Arxiv
4+阅读 · 2018年10月31日
Parsimonious Bayesian deep networks
Arxiv
5+阅读 · 2018年10月17日
Video-to-Video Synthesis
Arxiv
9+阅读 · 2018年8月20日
Arxiv
6+阅读 · 2018年5月18日
VIP会员
相关资讯
在K8S上运行Kafka合适吗?会遇到哪些陷阱?
DBAplus社群
9+阅读 · 2019年9月4日
防代码泄漏的监控系统架构与实践
FreeBuf
5+阅读 · 2019年4月30日
使用 C# 和 Blazor 进行全栈开发
DotNet
6+阅读 · 2019年4月15日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
被动DNS,一个被忽视的安全利器
运维帮
11+阅读 · 2019年3月8日
Python高级技巧:用一行代码减少一半内存占用
AI研习社
5+阅读 · 2018年11月28日
占坑!利用 JenKins 持续集成 iOS 项目时遇到的问题
浅谈浏览器 http 的缓存机制
前端大全
6+阅读 · 2018年1月21日
相关论文
Arxiv
7+阅读 · 2019年4月8日
Arxiv
3+阅读 · 2019年3月1日
Arxiv
4+阅读 · 2018年10月31日
Parsimonious Bayesian deep networks
Arxiv
5+阅读 · 2018年10月17日
Video-to-Video Synthesis
Arxiv
9+阅读 · 2018年8月20日
Arxiv
6+阅读 · 2018年5月18日
Top
微信扫码咨询专知VIP会员