作为一种现代的高级语言,
Swift基本上负责应用程序中的内存管理,分配和释放内存。 这是由于一种称为
自动引用计数的机制,简称
ARC 。 在本指南中,您将学习ARC如何工作以及如何在Swift中正确管理内存。 了解此机制后,您可以影响位于堆(
heap )上的对象的生存期。
在本指南中,您将通过学习以下知识来增强对Swift和ARC的了解:
- ARC如何运作
- 什么是参考周期以及如何正确修复它们
- 如何创建示例链接循环
- 如何使用Xcode提供的可视化工具查找链接循环
- 如何处理引用类型和值类型
开始使用
下载
源材料。 在
Cycles / Starter文件夹中打开项目。 在指南的第一部分,了解关键概念,我们将专门处理
MainViewController.swif t文件。
在MainViewController.swift的底部添加此类:
class User { let name: String init(name: String) { self.name = name print("User \(name) was initialized") } deinit { print("Deallocating user named: \(name)") } }
这里定义了
User类,它在
print语句的帮助下向我们发出有关类实例的初始化和释放的信号。
现在,在MainViewController的顶部创建User类的实例。
将此代码放在
viewDidLoad()方法之前:
let user = User(name: "John")
启动应用程序。 使用
Command-Shift-Y使Xcode控制台可见,以查看打印语句的输出。
注意,
已初始化用户John ,该
用户出现在控制台上,但未执行
deinit中的print语句。 这意味着该对象未被释放,因为它没有超出
范围 。
换句话说,除非包含该对象的视图控制器超出范围,否则该对象将永远不会被释放。
他在范围内吗?
通过将User类的实例包装在方法中,我们将允许它超出范围,从而允许ARC释放它。
让我们在MainViewController类中创建
runScenario()方法,并在其中移动User类实例的初始化。
func runScenario() { let user = User(name: "John") }
runScenario()定义User实例的范围。 退出该区域时,必须释放
用户 。
现在调用runScenario()将其添加到viewDidLoad()的末尾:
runScenario()
启动应用程序。 现在,控制台输出如下所示:
用户John已初始化
取消分配用户名为:John
这意味着您释放了离开视野的对象。
对象寿命
对象的存在分为五个阶段:
- 内存分配:从堆栈还是从堆
- 初始化:代码在init内部执行
- 使用
- 反初始化:代码在反初始化内部执行
- 可用内存:已分配的内存返回到堆栈或堆
没有直接方法可以跟踪分配和释放内存的步骤,但是可以在init和deinit中使用代码。
参考计数 (也称为
使用计数 )确定何时不再需要某个对象。 此计数器显示“使用”该对象的人数。 当使用计数器为零时,不需要对象。 然后,该对象被取消初始化并释放。

初始化User对象时,其引用计数为1,因为
用户常数引用了此对象。
在runScenario()的末尾,用户超出范围,并且引用计数减少为0。结果,用户未初始化然后释放。
参考周期
在大多数情况下,ARC可以正常工作。 当未使用的对象无限期地未分配时,开发人员通常无需担心内存泄漏。
但并非总是如此! 可能的内存泄漏。
怎么会这样 想象一个情况,不再使用两个对象,但每个对象都引用另一个对象。 由于每个引用计数都不为0,因此不会释放它们。

这是一个
很强的参考周期 。 这种情况会使ARC感到困惑,并且不允许它清除内存。
如您所见,结尾处的引用计数不为0,尽管不再需要任何对象,但是object1和object2将不会被释放。
查看我们的链接
要测试所有这些,请在MainViewController.swift中的User类之后添加以下代码:
class Phone { let model: String var owner: User? init(model: String) { self.model = model print("Phone \(model) was initialized") } deinit { print("Deallocating phone named: \(model)") } }
这段代码添加了一个新的
Phone类,它具有两个属性,一个用于模型,一个用于所有者,以及init和deinit方法。 所有者的属性是可选的,因为手机可能没有所有者。
现在将此行添加到runScenario():
let iPhone = Phone(model: "iPhone Xs")
这将创建Phone类的实例。
握住手机
现在,在name属性之后,立即将以下代码添加到User类:
private(set) var phones: [Phone] = [] func add(phone: Phone) { phones.append(phone) phone.owner = self }
添加用户拥有的电话阵列。 该设置程序被标记为私有,因此必须使用add(phone :)。
启动应用程序。 如您所见,将根据需要释放Phone和User对象类的实例。
用户John已初始化
手机iPhone XS已初始化
取消分配名为:iPhone Xs的电话
取消分配用户名为:John
现在,将其添加到runScenario()的末尾:
user.add(phone: iPhone)
在这里,我们将iPhone添加到
用户拥有的手机列表中,还将手机的
owner属性设置为'
user '。
再次运行该应用程序。 您将看到未释放用户和iPhone对象。 它们之间的强链接周期阻止ARC释放它们。

链接弱
要中断强链接的循环,可以将对象之间的关系指定为弱链接。
默认情况下,所有链接都是强链接,分配会导致引用计数增加。 使用弱引用时,引用计数不会增加。
换句话说,
弱链接不会影响对象的寿命管理 。 弱链接始终被声明为
可选 。 这样,当链接计数变为0时,可以将链接设置为nil。

在此图示中,虚线表示弱链接。 请注意,对象1的引用计数为1,因为变量1引用了它。 对象2的引用计数为2,因为它由变量2和对象1引用。
object2也引用object1,但引用为
WEAK ,这意味着它不影响object1的引用计数。
当释放variable1和variable2时,object1的引用计数为0,这将释放它。 反过来,这释放了对object2的强烈引用,这已经导致了它的发布。
在Phone类中,如下更改owner属性声明:
weak var owner: User?
通过将所有者属性引用声明为“弱”,我们打破了User和Phone类之间的牢固链接循环。

启动应用程序。 现在,用户和电话已正确释放。
无主链接
还有另一个不会增加引用计数的链接修饰符:
unowned 。
无主和 弱者有什么区别? 弱引用始终是可选的,并且在释放所引用的对象时会自动变为nil。
这就是为什么我们应将弱属性声明为类型的可选变量:为什么必须更改此属性。
相反,无主链接绝不是可选的。 如果尝试访问引用已释放对象的无主属性,则会收到一个错误,该错误看起来像是强制展开,其中包含nil变量(强制展开)。

让我们尝试应用
unown 。
在MainViewController.swift的末尾添加一个新的
CarrierSubscription类:
class CarrierSubscription { let name: String let countryCode: String let number: String let user: User init(name: String, countryCode: String, number: String, user: User) { self.name = name self.countryCode = countryCode self.number = number self.user = user print("CarrierSubscription \(name) is initialized") } deinit { print("Deallocating CarrierSubscription named: \(name)") } }
CarrierSubscription具有四个属性:
名称:提供者名称。
CountryCode:国家代码。
号码:电话号码。
用户:链接到用户。
谁是您的提供者?
现在,将其添加到name属性之后的User类中:
var subscriptions: [CarrierSubscription] = []
在这里,我们保留了一系列用户提供程序。
现在,在owner属性之后,将其添加到Phone类:
var carrierSubscription: CarrierSubscription? func provision(carrierSubscription: CarrierSubscription) { self.carrierSubscription = carrierSubscription } func decommission() { carrierSubscription = nil }
这将添加可选的CarrierSubscription属性以及向提供者注册和注销电话的两种方法。
现在,在print语句之前,在init方法内添加CarrierSubscription类:
user.subscriptions.append(self)
我们将CarrierSubscription添加到用户提供者数组中。
最后,将其添加到runScenario()方法的末尾:
let subscription = CarrierSubscription( name: "TelBel", countryCode: "0032", number: "31415926", user: user) iPhone.provision(carrierSubscription: subscription)
我们为用户创建了对提供商的订阅,并将电话连接到该订阅者。
启动应用程序。 在控制台中,您将看到:
用户John已初始化
手机iPhone Xs已初始化
运营商订阅电话已初始化
再一次链接周期! 用户,iPhone和订阅最终并未免费。
你能找到问题吗?

打破链条
从用户到订阅的链接或从订阅到用户的链接都必须是无所有权的,才能打破循环。 问题是要选择哪个选项。 让我们看一下结构。
用户拥有对提供者的订阅,但反之亦然-不,对提供者的订阅不拥有用户。
此外,如果不参考拥有CarrierSubscription的用户,就没有意义。
因此,用户链接必须是无主的。
在CarrierSubscription中更改用户声明:
unowned let user: User
现在,用户为无主用户,这打破了链接循环,并允许您释放所有对象。

闭包中的循环链接
当对象具有彼此引用的属性时,将发生对象的链接周期。 像对象一样,闭包是引用类型,并且可能导致引用循环。 闭包捕获它们使用的对象。
例如,如果您为一个类的属性分配一个闭包,并且该闭包使用同一类的属性,那么我们会得到一个链接循环。 换句话说,该对象通过该属性拥有一个到闭包的链接。 闭包包含通过捕获的self值对对象的引用。

在用户属性之后,立即将此代码添加到CarrierSubscription中:
lazy var completePhoneNumber: () -> String = { self.countryCode + " " + self.number }
此闭包计算并返回完整的电话号码。 该属性声明为
lazy ,它将在首次使用时分配。
这是必需的,因为它使用self.countryCode和self.number,在执行初始化程序代码之前它们将不可用。
将runScenario()添加到末尾:
print(subscription.completePhoneNumber())
调用completePhoneNumber()将执行关闭。
启动该应用程序,您将看到已释放用户和iPhone,但由于对象和闭包之间的紧密链接的循环而未发行CarrierSubscription。

捕获列表
Swift提供了一种简单而优雅的方法来打破闭包中强链接的循环。 您声明一个捕获列表,在其中定义闭包与其捕获的对象之间的关系。
为了演示捕获列表,请考虑以下代码:
var x = 5 var y = 5 let someClosure = { [x] in print("\(x), \(y)") } x = 6 y = 6 someClosure()
x在闭包捕获列表中,因此x的值被复制到闭包定义中。 它是按值捕获的。
y不在捕获列表中,它通过引用捕获。 这意味着y的值将与电路被调用时的值相同。
锁定列表有助于识别与循环中捕获的对象有关的弱交互或无主交互。 在我们的情况下,没有适当的选择,因为如果释放CarrierSubscription实例,闭包将不存在。
抓紧自己
用CarrierSubscription替换completePhoneNumber定义::
lazy var completePhoneNumber: () -> String = { [unowned self] in return self.countryCode + " " + self.number }
我们将
[unown self]添加到关闭捕获列表中。 这意味着我们将
自我作为一个
无主的链接而不是一个牢固的链接。
启动应用程序,您将看到CarrierSubscription现在已发布。
实际上,以上语法是更长和更完整的语法的一种简短形式,其中出现了一个新变量:
var closure = { [unowned newID = self] in
这里newID是self的无主副本。 除了封闭之外,自我依然存在。 在前面给出的简短形式中,我们
创建了一个新的self变量 ,该
变量使闭包内部现有的self变得模糊。
谨慎使用无所有权
在您的代码中,self和completePhoneNumber之间的关系被指定为未拥有。
如果您确定不会释放闭包中使用的对象,则可以使用未拥有的对象。 如果他这样做,那您就麻烦了!
在MainViewController.swift的末尾添加以下代码:
class WWDCGreeting { let who: String init(who: String) { self.who = who } lazy var greetingMaker: () -> String = { [unowned self] in return "Hello \(self.who)." } }
现在这是runScenario()的结尾:
let greetingMaker: () -> String do { let mermaid = WWDCGreeting(who: "caffeinated mermaid") greetingMaker = mermaid.greetingMaker } print(greetingMaker())
启动应用程序,您将在控制台中看到崩溃以及类似的信息:
用户John已初始化
手机iPhone XS已初始化
运营商订阅电话已初始化
0032 31415926
致命错误:尝试读取一个未拥有的引用但对象0x600000f0de30已被释放2019-02-24 12:29:40.744248-0600周期[33489:5926466]致命错误:试图读取一个未拥有的引用但对象0x600000f0de30已被重新分配
发生异常是因为闭包等待self.who存在,但是在do块结尾时,一旦美人鱼超出范围,它就会被释放。
这个例子看起来像是用手指吮吸,但是这种事情会发生。 例如,当我们使用闭包在稍后启动某件事时,例如,在网络上的异步调用结束之后。
疏通陷阱
将WWDCGreeting类中的greetingMaker替换为:
lazy var greetingMaker: () -> String = { [weak self] in return "Hello \(self?.who)." }
我们做了两件事:首先,我们用弱者代替了无者者。 其次,由于自我变得虚弱,我们通过自我访问谁的财产? 忽略Xcode警告,我们将尽快修复它。
该应用程序不再崩溃,但是如果您运行它,我们会得到一个有趣的结果:“你好,零。”
也许结果是完全可以接受的,但是如果对象被释放,我们经常需要做一些事情。 可以使用guard语句来完成。
用以下内容替换关闭文本:
lazy var greetingMaker: () -> String = { [weak self] in guard let self = self else { return "No greeting available." } return "Hello \(self.who)." }
保护声明分配从弱自我获取的自我。 如果self为nil,则闭包返回“无问候可用”。 否则,自我将成为有力的参考,因此可以保证对象活到关闭的最后。
在Xcode 10中寻找链接循环
现在您已经了解了ARC的工作原理,什么是链接循环以及如何中断它们,现在该看一个实际应用程序的示例了。
打开“联系人”文件夹中的Starter项目。
启动应用程序。

这是最简单的联系人管理器。 尝试单击一个联系人,添加几个新联系人。
文件分配:
ContactsTableViewController:显示所有联系人。
DetailViewController:显示所选联系人的详细信息。
NewContactViewController:允许您添加新联系人。
ContactTableViewCell:显示联系人详细信息的表单元格。
联系人:联系人模型。
号码:电话号码型号。
但是,对于这个项目,一切都是不好的:有一个循环的链接。 起初,由于泄漏内存较小,用户不会注意到问题,出于同样的原因,很难发现泄漏。
幸运的是,Xcode 10具有内置工具来查找最小的内存泄漏。
再次启动该应用程序。 使用向左滑动和删除按钮删除3-4个联系人。 好像它们完全消失了,对不对?

它流向哪里?
当应用程序运行时,单击“调试内存图”按钮:

在“调试”导航器中观察“运行时问题”。 它们用紫色正方形标记,内部带有白色感叹号:

在导航器中选择有问题的联系人对象之一。 周期清晰可见:相互关联的Contact和Number对象保持住。

看起来您应该研究代码。 请记住,联系人可以不带数字而存在,反之亦然。
您将如何解决此循环? 从联系人链接到号码还是从号码链接到联系人? 虚弱还是无主? 自己先尝试!
如果您需要帮助...有2种可能的解决方案:要么使“联系人”到“号码”之间的链接弱,要么使“号码到联系人”不存在。
Apple的文档建议父对象强烈引用“子”,反之亦然。 这意味着我们为Contact提供了对Number的强烈引用,以及Number-Contact的未拥有链接:
class Number { unowned var contact: Contact
奖励:循环使用引用类型和值类型。
Swift具有引用类型(类和闭包)和值类型(结构,枚举)。 值类型在传递时将被复制,引用类型使用链接共享相同的值。
这意味着在值类型的情况下,将没有循环。 为了使循环发生,我们至少需要2种引用类型。
让我们回到Cycles项目,并在MainViewController.swift的末尾添加以下代码:
struct Node {
将无法正常工作! 结构是一个值类型,不能对其自身的实例进行递归。 否则,这样的结构将具有无限的大小。
将结构更改为类。
class Node { var payload = 0 var next: Node? }
对自身的引用对于类(引用类型)是完全可以接受的,因此编译器没有问题。
现在,将其添加到MainViewController.swift的末尾:
class Person { var name: String var friends: [Person] = [] init(name: String) { self.name = name print("New person instance: \(name)") } deinit { print("Person instance \(name) is being deallocated") } }
这是runScenario()的结尾:
do { let ernie = Person(name: "Ernie") let bert = Person(name: "Bert") ernie.friends.append(bert)
启动应用程序。 请注意:ernie和bert均未释放。
链接和意义
这是导致链接循环的引用类型和值类型组合的示例。
尽管ernie和bert本身是值类型,但它们仍未释放,彼此保持在其Friends数组中。
尝试将好友存档设置为“无主”,Xcode将显示错误:“无主”仅适用于类。
要修复此循环,我们必须创建一个包装对象,并使用它来将实例添加到数组。
在Person类之前添加以下定义:
class Unowned<T: AnyObject> { unowned var value: T init (_ value: T) { self.value = value } }
然后在Person类中更改friends的定义:
var friends: [Unowned<Person>] = []
最后,替换runScenario()中do块的内容:
do { let ernie = Person(name: "Ernie") let bert = Person(name: "Bert") ernie.friends.append(Unowned(bert)) bert.friends.append(Unowned(ernie)) }
启动应用程序,现在ernie和bert已正确发布!
friends数组不再是Person对象的集合。 现在,这是一个
Unperson对象的
集合,这些对象充当Person实例的包装。
要从Unowned获取Person对象,请使用value属性:
let firstFriend = bert.friends.first?.value
结论
您现在已经对Swift中的内存管理有了很好的了解,并且知道ARC的工作原理。 我希望该出版物对您有所帮助。
苹果:自动引用计数