Swift高阶 - 内存管理:ARC, Strong, Weak and Unowned详解

image

内存管理是任何编程语言中的核心概念。 尽管有不少教程解释了Swift自动引用计数的基本原理,但我发现没有一个能够从编译器的角度对其进行解释。 在本文中,咱们将学习iOS内存管理,引用计数和对象生命周期等基础知识以外的内容。git

让咱们从基础开始,逐步进入ARC和Swift Runtime的内部,首先思考如下问题:程序员

  • 内存是什么?
  • Swift编译器是如何实现自动引用计数的?
  • 强,弱和无主引用是如何实现的?
  • Swift对象的生命周期是怎么样的?
  • 什么是side table?

内存管理

从硬件层面,内存只是一长串字节。 在虚拟内存中它被分红三个主要部分:github

  • 栈区,全部局部变量都存放在哪里。
  • 全局数据,其中包含静态变量,常量和类型元数据。
  • 堆区,全部动态分配的对象都在其中。 基本上,全部具备生命周期的东西都存储在这里。

咱们将继续交替使用“对象”和“动态分配的对象”。 这些是Swift引用类型以及值类型的一些特殊状况。编程

内存管理是控制程序内存的过程。 了解它的工做原理相当重要,不然您可能会遇到随机崩溃和莫名的小bug。swift

ARC

内存管理与全部权的概念紧密相关。 全部权会决定哪些代码会形成对象被销毁[1]。安全

自动引用计数(ARC)属于Swift的全部权系统,它规定了一组用于管理和转让全部权的约定。bash

能够指向对象的变量别名叫作引用。 Swift引用具备两个强度级别:强和弱。 此外,弱引用包含无主引用和弱引用。app

Swift内存管理的本质是:若是一个对象被强引用指向,Swift会保留它,不然将其释放。 剩下的只是实现细节。编程语言

理解Strong, Weak and Unowned

强引用的目的是使对象保持存活状态。 强引用可能会致使几个有意义的问题[2]:ide

  • 循环引用。 考虑到Swift语言不是循环收集(cycle-collecting)的,一个对象的强引用R若是同时被对象强引用(多是间接的),则会致使循环引用。 咱们必须编写大量代码来显式打破循环。
  • 并不是老是可使强引用在对象构造上当即有效,例如代理(delegates)。

弱引用解决了反向引用的问题。 若是有指向对象的弱引用,则能够销毁该对象。 弱引用访问再也不存在的对象时将返回nil。 这称为调零或归零(zeroing)。

无主引用是弱函数的另外一种形式,旨在用于严格的有效性不变式。 无主引用是非归零的。 当试图经过无主引用读取不存在的对象时,程序将因断言错误而崩溃。 它们用于跟踪和修复一致性问题颇有用。

class MyClass {	
    lazy var foo = { [weak self] in	
        // Must be validated	
        guard let self = self else { return }	
        self.doSomething()	
    }()	
    func doSomething() {}	
}
复制代码

无主引用无需在使用时进行验证:

lazy var bar = { [unowned self] in	
  // No validation needed
  self.doSomething()	
}()	
复制代码

在这个示例中,使用无主引用是明智的,由于属性barself具备相同的生存期。

咱们对Swift内存管理的进一步讨论会处于较低的抽象层面。 咱们将深刻研究如何在编译器级别实现ARC,以及每一个Swift对象在销毁以前要经历的步骤。

Swift Runtime

ARC机制在Swift Runtime库中声明。 它包含了诸如运行时类型系统之类的核心功能,例如:动态转换,泛型和协议一致性注册[3]

Swift Runtime 使用HeapObject结构体表示每一个动态分配的对象。 它包含构成Swift对象的全部数据:引用计数和类型元数据。

HeapObject中每一个Swift对象都有三个引用计数:每种引用都有一个。 在SIL生成阶段,swiftc编译器会在适当的地方插入swift_retain()swift_release()函数。 这是经过拦截HeapObject的初始化和销毁来完成的。

编译是Xcode Build System的步骤之一

若是您是Objective-C老程序员,而且想知道autorelease在哪里,能够告诉你:纯Swift对象没有这个东西。

如今,让咱们继续弱引用。 它们的实现方式与Side table的概念紧密相关。

想要详细了解SideTable,请阅读我以前的一篇文章:Swift弱引用管理之Side Table

Side Tables介绍

Side tables 是实现Swift弱引用的核心。

大多数状况,对象没有任何“弱”引用,所以为每一个对象中的弱引用计数保留存储空间是浪费的。 此信息存储在外部的 side table中,只有在确实须要时才会分配。

弱引用变量不是直接指向对象,而是指向side table,而side table又指向对象。 这解决了两个问题:为弱引用计数节省内存,直到对象真正须要它才建立; 容许安全地将弱引用归零,由于它不会直接指向对象,而且再也不是竟态条件的主体。

当两个线程竞争同一资源时,若是对资源的访问顺序敏感,就称存在竞态条件。

Side table只包含一个引用计数 和 一个对象的指针。 它们在Swift Runtime 中声明以下(C ++ 代码)[5]:

class HeapObjectSideTableEntry {
  std::atomic<HeapObject*> object;
  SideTableRefCounts refCounts;
  // Operations to increment and decrement reference counts
}
复制代码

Swift对象生命周期

Swift对象具备本身的生命周期,在下图中我用有限状态机表示。 方括号表示触发状态转换的条件。

1

Live状态时,对象处于活动状态。 其引用计数被初始化为 strong:1, unown:1和 weak:1(side table从+1开始)。 一旦有弱引用指向对象,便会建立side table。 弱引用指向side table而不是对象。

一旦强引用计数达到零,则对象从Live状态进入Deiniting状态。 处于Deiniting状态表示deinit()正在进行中。 在这一点上,强引用操做无效。 若是存在关联的side table,经过弱引用访问将返回nil。 经过unowned访问将触发断言失败。 经过新的unowned引用仍然能够存储。 今后状态开始,可能选择两条分支:

  • 快速判断若是没有weak,unowned的引用和side table。 该对象将转换为Dead状态,并当即从内存中删除。
  • 不然,对象将变为Deinited状态。

Deinited状态下,deinit()已经执行完成,该对象还有未完成的unown引用(至少是初始值:1)。 此时,经过强和弱引用进行存储和读取没法发生。 Unowned引用存储也不会发生。 经过Unown读取会触发断言错误。 该对象能够今后处进入两条分支:

  • 若是没有弱引用,则能够当即释放该对象。 它过渡到Dead状态。
  • 不然,仍然有一个side table要移除,而且对象进入Freed状态。

Freed状态以前,对象已彻底释放,但它的 side table仍处于活动状态。 在此阶段,弱引用计数将置0,而且 side table会被销毁。 对象将转换为最终状态。

除指向对象的指针外,在Dead状态下对象已被所有销毁。 指向“HeapObject”的指针也从堆中释放出来,在内存中找不到该对象的任何痕迹。

总结

自动引用计数并非什么神奇的东西,咱们对它越了解,咱们的代码就越不容易出现内存管理错误。 这里是要记住的几个关键点:

  • 弱引用指针指向side table。 无主和强引用指针指向对象。
  • 自动引用计数是在编译器级别实现的。 swiftc编译器会在适当的时候插入swift_retain()swift_release()
  • Swift对象不会当即销毁。 它们在生命周期中经历了五个阶段:live -> deiniting -> deinited -> freed -> dead

做者:Vadim Bulavin 翻译:乐Coding

推荐

[1] : github.com/apple/swift…

[2] : github.com/apple/swift…

[3] : github.com/apple/swift…

[4] : github.com/apple/swift…

[HeapObject] : (github.com/apple/swift…

[6] : github.com/apple/swift…


logo
相关文章
相关标签/搜索