在这篇文章中,我会解释什么是内存泄漏(memory leaks),讨论循环引用(retain cycles)和其他事物。
这确实是我们开发者经常面对的问题之一,我们的代码越来越复杂,随着app的增长,我们也带来了泄漏。
内存泄漏会永久占用一部分内存,让它无法再使用。它是占据空间带来问题的垃圾。
有时候我们分配内存,却再也没有释放,并且也没有app引用去。因为没有对它的引用,也就没有办法释放它,这段内存就无法再使用了。
我们时不时在不断制造内存泄漏,无论是新手开发者还是业界老鸟。它和我们的经验无关。最重要的是要清除它们,让我们的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!
}
}
要想从内存中释放,一个对象必须先解除其所有关联性。因为如果对象本身是一个从属,它无法被释放。同样如果一个对象又一个循环引用,它就不会消失。
当循环中的一个引用是弱引用或无主引用时,循环引用会被破坏。循环必须存在,因为它是我们编码中关联性本质所必须的。问题就在于,不能所有的关联都是强的,其中一个必须是弱关联。
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()
})
}
}
当我们编码时,忘记weak self这种事并不少见。我们经常在编写区块闭包(blockclosures),比如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
}
}
“Swift by Sundell”在文章中解释了几种内存泄漏。这篇文章对我的写作起到很大帮助。