Swift:ARC和内存管理

作为一种现代的高级语言, 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() // Prints 5, 6 print("\(x), \(y)") // Prints 6, 6 

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 // Use unowned newID here... } 

这里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 // Other code... } class Contact { var number: Number? // Other code... } 


奖励:循环使用引用类型和值类型。


Swift具有引用类型(类和闭包)和值类型(结构,枚举)。 值类型在传递时将被复制,引用类型使用链接共享相同的值。

这意味着在值类型的情况下,将没有循环。 为了使循环发生,我们至少需要2种引用类型。

让我们回到Cycles项目,并在MainViewController.swift的末尾添加以下代码:

 struct Node { // Error var payload = 0 var next: 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) // Not deallocated bert.friends.append(ernie) // Not deallocated } 

启动应用程序。 请注意: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 // get ernie 

结论


您现在已经对Swift中的内存管理有了很好的了解,并且知道ARC的工作原理。 我希望该出版物对您有所帮助。

苹果:自动引用计数

Source: https://habr.com/ru/post/zh-CN451130/


All Articles