内存管理是任何编程语言中的核心概念。 尽管有不少教程解释了Swift自动引用计数的基本原理,但我发现没有一个能够从编译器的角度对其进行解释。 在本文中,咱们将学习iOS内存管理,引用计数和对象生命周期等基础知识以外的内容。git
让咱们从基础开始,逐步进入ARC和Swift Runtime的内部,首先思考如下问题:程序员
从硬件层面,内存只是一长串字节。 在虚拟内存中它被分红三个主要部分:github
咱们将继续交替使用“对象”和“动态分配的对象”。 这些是Swift引用类型以及值类型的一些特殊状况。编程
内存管理是控制程序内存的过程。 了解它的工做原理相当重要,不然您可能会遇到随机崩溃和莫名的小bug。swift
内存管理
与全部权的概念紧密相关。 全部权会决定哪些代码会形成对象被销毁[1]。安全
自动引用计数(ARC)属于Swift的全部权系统,它规定了一组用于管理和转让全部权的约定。bash
能够指向对象的变量别名叫作引用
。 Swift引用具备两个强度级别:强和弱。 此外,弱引用包含无主引用和弱引用。app
Swift内存管理的本质是:若是一个对象被强引用指向,Swift会保留它,不然将其释放。 剩下的只是实现细节。编程语言
强引用的目的是使对象保持存活状态。 强引用可能会致使几个有意义的问题[2]:ide
R
若是同时被对象强引用(多是间接的),则会致使循环引用。 咱们必须编写大量代码来显式打破循环。弱引用解决了反向引用的问题。 若是有指向对象的弱引用,则能够销毁该对象。 弱引用访问再也不存在的对象时将返回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()
}()
复制代码
在这个示例中,使用无主引用是明智的,由于属性bar
和self
具备相同的生存期。
咱们对Swift内存管理的进一步讨论会处于较低的抽象层面。 咱们将深刻研究如何在编译器级别实现ARC,以及每一个Swift对象在销毁以前要经历的步骤。
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 是实现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对象具备本身的生命周期,在下图中我用有限状态机
表示。 方括号表示触发状态转换的条件。
在Live状态时,对象处于活动状态。 其引用计数被初始化为 strong:1, unown:1和 weak:1(side table从+1开始)。 一旦有弱引用指向对象,便会建立side table。 弱引用
指向side table
而不是对象。
一旦强引用计数达到零,则对象从Live状态进入Deiniting状态。 处于Deiniting状态表示deinit()
正在进行中。 在这一点上,强引用操做无效。 若是存在关联的side table
,经过弱引用访问将返回nil
。 经过unowned
访问将触发断言失败。 经过新的unowned引用仍然能够存储。 今后状态开始,可能选择两条分支:
在Deinited状态下,deinit()
已经执行完成,该对象还有未完成的unown引用(至少是初始值:1)。 此时,经过强和弱引用进行存储和读取没法发生。 Unowned引用存储也不会发生。 经过Unown读取会触发断言错误。 该对象能够今后处进入两条分支:
side table
要移除,而且对象进入Freed状态。在Freed状态以前,对象已彻底释放,但它的 side table仍处于活动状态。 在此阶段,弱引用计数将置0,而且 side table会被销毁。 对象将转换为最终状态。
除指向对象的指针外,在Dead状态下对象已被所有销毁。 指向“HeapObject”的指针也从堆中释放出来,在内存中找不到该对象的任何痕迹。
自动引用计数并非什么神奇的东西,咱们对它越了解,咱们的代码就越不容易出现内存管理错误。 这里是要记住的几个关键点:
swift_retain()
和swift_release()
。做者: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…