Swift 自動引用計數(ARC)

Swift 使用自動引用計數(ARC)這一機制來跟蹤和管理應用程式的記憶體

通常情況下我們不需要去手動釋放記憶體,因為 ARC 會在類的例項不再被使用時,自動釋放其佔用的記憶體。

但在有些時候我們還是需要在程式碼中實現記憶體管理。

ARC 功能

  • 當每次使用 init() 方法建立一個類的新的例項的時候,ARC 會分配一大塊記憶體用來儲存例項的資訊。

  • 記憶體中會包含例項的型別資訊,以及這個例項所有相關屬性的值。

  • 當例項不再被使用時,ARC 釋放例項所佔用的記憶體,並讓釋放的記憶體能挪作他用。

  • 為了確保使用中的例項不會被銷燬,ARC 會跟蹤和計算每一個例項正在被多少屬性,常量和變數所引用。

  • 例項賦值給屬性、常量或變數,它們都會建立此例項的強引用,只要強引用還在,例項是不允許被銷燬的。

ARC 例項

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) 開始初始化")
    }
    deinit {
        print("\(name) 被析構")
    }
}

// 值會被自動初始化為nil,目前還不會引用到Person類的例項
var reference1: Person?
var reference2: Person?
var reference3: Person?

// 建立Person類的新例項
reference1 = Person(name: "itread01")


//賦值給其他兩個變數,該例項又會多出兩個強引用
reference2 = reference1
reference3 = reference1

//斷開第一個強引用
reference1 = nil
//斷開第二個強引用
reference2 = nil
//斷開第三個強引用,並呼叫解構函式
reference3 = nil

以上程式執行輸出結果為:

itread01 開始初始化
itread01 被析構

類例項之間的迴圈強引用

在上面的例子中,ARC 會跟蹤你所新建立的 Person 例項的引用數量,並且會在 Person 例項不再被需要時銷燬它。

然而,我們可能會寫出這樣的程式碼,一個類永遠不會有0個強引用。這種情況發生在兩個類例項互相保持對方的強引用,並讓對方不被銷燬。這就是所謂的迴圈強引用。

例項

下面展示了一個不經意產生迴圈強引用的例子。例子定義了兩個類:Person和Apartment,用來建模公寓和它其中的居民:

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) 被析構") }
}

class Apartment {
    let number: Int
    init(number: Int) { self.number = number }
    var tenant: Person?
    deinit { print("Apartment #\(number) 被析構") }
}

// 兩個變數都被初始化為nil
var itread01: Person?
var number73: Apartment?

// 賦值
itread01 = Person(name: "itread01")
number73 = Apartment(number: 73)

// 意感嘆號是用來展開和訪問可選變數 itread01 和 number73 中的例項
// 迴圈強引用被建立
itread01!.apartment = number73
number73!.tenant = itread01

// 斷開 itread01 和 number73 變數所持有的強引用時,引用計數並不會降為 0,例項也不會被 ARC 銷燬
// 注意,當你把這兩個變數設為nil時,沒有任何一個解構函式被呼叫。
// 強引用迴圈阻止了Person和Apartment類例項的銷燬,並在你的應用程式中造成了記憶體洩漏
itread01 = nil
number73 = nil

解決例項之間的迴圈強引用

Swift 提供了兩種辦法用來解決你在使用類的屬性時所遇到的迴圈強引用問題:

  • 弱引用
  • 無主引用

弱引用和無主引用允許迴圈引用中的一個例項引用另外一個例項而不保持強引用。這樣例項能夠互相引用而不產生迴圈強引用。

對於生命週期中會變為nil的例項使用弱引用。相反的,對於初始化賦值後再也不會被賦值為nil的例項,使用無主引用。

弱引用例項

class Module {
    let name: String
    init(name: String) { self.name = name }
    var sub: SubModule?
    deinit { print("\(name) 主模組") }
}

class SubModule {
    let number: Int
    
    init(number: Int) { self.number = number }
    
    weak var topic: Module?
    
    deinit { print("子模組 topic 數為 \(number)") }
}

var toc: Module?
var list: SubModule?
toc = Module(name: "ARC")
list = SubModule(number: 4)
toc!.sub = list
list!.topic = toc

toc = nil
list = nil

以上程式執行輸出結果為:

ARC 主模組
子模組 topic 數為 4

無主引用例項

class Student {
    let name: String
    var section: Marks?
    
    init(name: String) {
        self.name = name
    }
    
    deinit { print("\(name)") }
}
class Marks {
    let marks: Int
    unowned let stname: Student
    
    init(marks: Int, stname: Student) {
        self.marks = marks
        self.stname = stname
    }
    
    deinit { print("學生的分數為 \(marks)") }
}

var module: Student?
module = Student(name: "ARC")
module!.section = Marks(marks: 98, stname: module!)
module = nil

以上程式執行輸出結果為:

ARC
學生的分數為 98

閉包引起的迴圈強引用

迴圈強引用還會發生在當你將一個閉包賦值給類例項的某個屬性,並且這個閉包體中又使用了例項。這個閉包體中可能訪問了例項的某個屬性,例如self.someProperty,或者閉包中呼叫了例項的某個方法,例如self.someMethod。這兩種情況都導致了閉包 "捕獲" self,從而產生了迴圈強引用。

例項

下面的例子為你展示了當一個閉包引用了self後是如何產生一個迴圈強引用的。例子中定義了一個叫HTMLElement的類,用一種簡單的模型表示 HTML 中的一個單獨的元素:

class HTMLElement {
    
    let name: String
    let text: String?
    
    lazy var asHTML: () -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }
    
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
    
}

// 建立例項並列印資訊
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())

HTMLElement 類產生了類例項和 asHTML 預設值的閉包之間的迴圈強引用。

例項的 asHTML 屬性持有閉包的強引用。但是,閉包在其閉包體內使用了self(引用了self.name和self.text),因此閉包捕獲了self,這意味著閉包又反過來持有了HTMLElement例項的強引用。這樣兩個物件就產生了迴圈強引用。

解決閉包引起的迴圈強引用:在定義閉包時同時定義捕獲列表作為閉包的一部分,通過這種方式可以解決閉包和類例項之間的迴圈強引用。


弱引用和無主引用

當閉包和捕獲的例項總是互相引用時並且總是同時銷燬時,將閉包內的捕獲定義為無主引用。

相反的,當捕獲引用有時可能會是nil時,將閉包內的捕獲定義為弱引用。

如果捕獲的引用絕對不會置為nil,應該用無主引用,而不是弱引用。

例項

前面的HTMLElement例子中,無主引用是正確的解決迴圈強引用的方法。這樣編寫HTMLElement類來避免迴圈強引用:

class HTMLElement {
    
    let name: String
    let text: String?
    
    lazy var asHTML: () -> String = {
        [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }
    
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
    
    deinit {
        print("\(name) 被析構")
    }
    
}

//建立並列印HTMLElement例項
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())

// HTMLElement例項將會被銷燬,並能看到它的解構函式打印出的訊息
paragraph = nil

以上程式執行輸出結果為:

<p>hello, world</p>
p 被析構