Swift 3 自动引用计数(ARC)

Swift 使用自动引用计数(ARC)来自动化的跟踪和管理应用程序的内存

Swift 语言中,通常情况下开发者不需要去手动释放内存,因为 ARC 会在类的实例不再被使用时,自动释放其占用的内存

但偶尔有些时候还是需要在代码中实现内存管理

ARC 功能

  • 每次使用 init() 方法创建一个类的新的实例的时候,ARC 会分配一大块内存用来储存实例的信息

  • 内存中会包含实例的类型信息,以及这个实例所有相关属性的值

  • 当实例不再被使用时,ARC 释放实例所占用的内存,并让释放的内存能挪作他用

  • 为了确保使用中的实例不会被销毁,ARC 会跟踪和计算每一个实例正在被多少属性,常量和变量所引用

  • 实例赋值给属性、常量或变量,它们都会创建此实例的强引用,只要强引用还在,实例是不允许被销毁的

Swift ARC 范例

  1. import Cocoa
  2. class Person
  3. {
  4. let name: String
  5. init(name: String)
  6. {
  7. self.name = name
  8. print("\(name) 开始初始化")
  9. }
  10. deinit
  11. {
  12. print("\(name) 被析构")
  13. }
  14. }
  15. // 值会被自动初始化为 nil,目前还不会引用到 Person 类的实例
  16. var reference1: Person?
  17. var reference2: Person?
  18. var reference3: Person?
  19. // 创建 Person 类的新实例
  20. reference1 = Person(name: "简单教程")
  21. //赋值给其他两个变量,该实例又会多出两个强引用
  22. reference2 = reference1
  23. reference3 = reference1
  24. //断开第一个强引用
  25. reference1 = nil
  26. //断开第二个强引用
  27. reference2 = nil
  28. //断开第三个强引用,并调用析构函数
  29. reference3 = nil

编译运行以上 Swift 范例,输出结果为

  1. $ swift main.swift
  2. 简单教程 开始初始化
  3. 简单教程 被析构

类实例之间的循环强引用

在上面的范例中,ARC 会跟踪新创建的 Person 实例的引用数量,并且会在 Person 实例不再被需要时销毁它

但是,我们可能会写出这样的代码,一个类永远不会有 0 个强引用

这种情况发生在两个类实例互相保持对方的强引用,并让对方不被销毁

这就是所谓的循环强引用

范例

下面的代码演示了一个不经意产生的循环强引用

范例定义了两个类:Person 和 Apartment,用来建模公寓和它其中的居民

  1. import Cocoa
  2. class Person
  3. {
  4. let name: String
  5. var apartment: Apartment?
  6. init(name: String)
  7. {
  8. self.name = name
  9. }
  10. deinit
  11. {
  12. print("\(name) 被析构")
  13. }
  14. }
  15. class Apartment
  16. {
  17. let number: Int
  18. var tenant: Person?
  19. init(number: Int)
  20. {
  21. self.number = number
  22. }
  23. deinit
  24. {
  25. print("Apartment #\(number) 被析构")
  26. }
  27. }
  28. // 两个变量都被初始化为 nil
  29. var p1: Person?
  30. var number73: Apartment?
  31. // 赋值
  32. p1 = Person (name: "简单教程")
  33. number73 = Apartment(number: 73)
  34. // 意感叹号是用来展开和访问可选变量 简单教程 和 number73 中的实例
  35. // 循环强引用被创建
  36. p1!.apartment = number73
  37. number73!.tenant = p1
  38. // 断开 简单教程 和 number73 变量所持有的强引用时,引用计数并不会降为 0,实例也不会被 ARC 销毁
  39. // 注意,当你把这两个变量设为nil时,没有任何一个析构函数被调用。
  40. // 强引用循环阻止了 Person 和 Apartment类实例的销毁,并在你的应用程序中造成了内存泄漏
  41. p1 = nil
  42. number73 = nil

运行以上范例,我们会发现析构函数没有被调用,因为 Person 和 Apartment 相互之间引用了对方的实例,造成了循环强引用

要怎么解决这个问题呢?

解决实例之间的循环强引用

Swift 提供了两种办法用来解决使用类的属性时所遇到的循环强引用问题

1.弱引用 2.无主引用

弱引用和无主引用允许循环引用中的一个实例引用另外一个实例而不保持强引用这样实例能够互相引用而不产生循环强引用

使用原则

1.对于生命周期中会变为 nil 的实例使用弱引用

2.对于初始化赋值后再也不会被赋值为 nil 的实例,使用无主引用

范例

下面的代码演示了弱引用的使用

  1. import Cocoa
  2. class Module
  3. {
  4. let name: String
  5. var sub: SubModule?
  6. init(name: String)
  7. {
  8. self.name = name
  9. }
  10. deinit
  11. {
  12. print("\(name) 主模块")
  13. }
  14. }
  15. class SubModule
  16. {
  17. let number: Int
  18. weak var topic: Module?
  19. init(number: Int)
  20. {
  21. self.number = number
  22. }
  23. deinit
  24. {
  25. print("子模块 topic 数为 \(number)")
  26. }
  27. }
  28. var toc: Module?
  29. var list: SubModule?
  30. toc = Module(name: "ARC")
  31. list = SubModule(number: 4)
  32. toc!.sub = list
  33. list!.topic = toc
  34. toc = nil
  35. list = nil

编译运行以上 Swift 范例,输出结果为

  1. $ swift main.swift
  2. ARC 主模块
  3. 子模块 topic 数为 4

范例 2

下面的代码演示了无主引用的使用

  1. import Cocoa
  2. class Student
  3. {
  4. let name: String
  5. var section: Marks?
  6. init(name: String)
  7. {
  8. self.name = name
  9. }
  10. deinit
  11. {
  12. print("\(name)")
  13. }
  14. }
  15. class Marks
  16. {
  17. let marks: Int
  18. unowned let stname: Student
  19. init(marks: Int, stname: Student)
  20. {
  21. self.marks = marks
  22. self.stname = stname
  23. }
  24. deinit
  25. {
  26. print("学生的分数为 \(marks)")
  27. }
  28. }
  29. var module: Student?
  30. module = Student(name: "ARC")
  31. module!.section = Marks(marks: 98, stname: module!)
  32. module = nil

编译运行以上 Swift 范例,输出结果为

  1. $ swift main.swift
  2. ARC
  3. 学生的分数为 98

闭包引起的循环强引用

循环强引用还会发生在将一个闭包赋值给类实例的某个属性,并且这个闭包体中又使用了实例。

这个闭包体中可能访问了实例的某个属性,例如 self.someProperty,或者闭包中调用了实例的某个方法,例如 self.someMethod

这两种情况都导致了闭包 "捕获" self,从而产生了循环强引用

范例

下面的范例演示了当一个闭包引用了 self 后是如何产生一个循环强引用的

范例中定义了一个叫 HTMLElement 的类,用一种简单的模型表示 HTML 中的一个单独的元素

  1. import Cocoa
  2. class HTMLElement
  3. {
  4. let name: String
  5. let text: String?
  6. lazy var asHTML: () -> String = {
  7. if let text = self.text
  8. {
  9. return "<\(self.name)>\(text)</\(self.name)>"
  10. } else
  11. {
  12. return "<\(self.name) />"
  13. }
  14. }
  15. init(name: String, text: String? = nil)
  16. {
  17. self.name = name
  18. self.text = text
  19. }
  20. deinit
  21. {
  22. print("\(name) is being deinitialized")
  23. }
  24. }
  25. // 创建实例并打印信息
  26. var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
  27. print(paragraph!.asHTML())

HTMLElement 类产生了类实例和 asHTML 默认值的闭包之间的循环强引用

范例的 asHTML 属性持有闭包的强引用

但是,闭包在其闭包体内使用了self(引用了 self.name 和 self.text ),因此闭包捕获了self,这意味着闭包又反过来持有了 HTMLElement 实例的强引用

这样两个对象就产生了循环强引用

解决闭包引起的循环强引用:

在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用

弱引用和无主引用

当闭包和捕获的实例总是互相引用时并且总是同时销毁时,将闭包内的捕获定义为无主引用

当捕获引用有时可能会是 nil 时,将闭包内的捕获定义为弱引用

如果捕获的引用绝对不会置为 nil,应该用无主引用,而不是弱引用

范例

前面的 HTMLElement 范例中,无主引用是正确的解决循环强引用的方法

下面这种 HTMLElement 类定义方法可以避免循环强引用

  1. import Cocoa
  2. class HTMLElement
  3. {
  4. let name: String
  5. let text: String?
  6. lazy var asHTML: () -> String = {
  7. [unowned self] in
  8. if let text = self.text {
  9. return "<\(self.name)>\(text)</\(self.name)>"
  10. } else {
  11. return "<\(self.name) />"
  12. }
  13. }
  14. init(name: String, text: String? = nil)
  15. {
  16. self.name = name
  17. self.text = text
  18. }
  19. deinit
  20. {
  21. print("\(name) 被析构")
  22. }
  23. }
  24. //创建并打印HTMLElement实例
  25. var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
  26. print(paragraph!.asHTML())
  27. // HTMLElement实例将会被销毁,并能看到它的析构函数打印出的消息
  28. paragraph = nil

编译运行以上 Swift 范例,输出结果为

  1. $ swift main.swift
  2. <p>hello, world</p>
  3. p 被析构