Swift中的内存泄漏



原创: Leandro Pérez Cocoa开发者社区 昨天

在这篇文章中,我会解释什么是内存泄漏(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会是空。



0