Swift 4.2基礎 --- 自動引用計數(ARC)

Swift使用ARC(自動引用計數器:Automatic Reference Counting)來追蹤和管理應用的內存使用。在大多數情況下,你不需要自己考慮內存管理問題,ARC會自動釋放那些不需要的類實例所佔用的內存。引用計數僅適用於類的實例, 結構和枚舉是值類型,而不是引用類型,不會通過引用被存儲和傳遞。

1. ARC工作原理

每次創建一個類的新實例時,ARC都會分配一塊內存來存儲該實例的信息。 這塊內存保存有關實例類型的信息,以及與該實例相關聯的任何存儲屬性的值。
此外,當不再需要某個實例時,ARC會釋放它佔用的內存,以便這塊內存可以用於其他目的。 這確保類實例在不需要時不會佔用內存空間。
但是,如果ARC要釋放仍在使用中的實例,將無法再訪問該實例的屬性,或者調用該實例的方法。 事實上,如果你試圖訪問該實例,你的應用程序很可能會崩潰。
爲了確保實例在使用時不被銷燬,ARC會跟蹤每個類實例當前有多少屬性、常量和變量引用。只要有一個對該實例的引用仍然存在,則ARC就不會釋放該實例。
爲了實現這一點,每當將一個類實例分配給一個屬性,常量或變量時,該屬性,常量或變量將強制引用該實例。 這個引用稱爲“強”引用,只要強引用存在,ARC就不會釋放該實例。

2. ARC的應用

這是一個自動引用計數器如何工作的示例。 這個例子從一個簡單的類Person開始,它定義一個名爲name的常量存儲屬性:

class Person {
    
    //存儲屬性name
    let name: String

    //使用初始化方法設置name的初始值(也可設置爲可選類型,默認初始值爲nil)
    init(name: String) {
    self.name = name
    print("\(name) is being initialized")
    }
    //當Person實例被銷燬時自動調用該方法
    deinit { print("\(name) is being deinitialized")}
}

下面的代碼定義了三個Person類型的變量,用於設置對一個新的Person實例的多個引用。 因爲這些變量是可選類型(Person ?,而不是Person),所以它們將自動被初始化爲nil,並且當前不引用Person實例。

var reference1: Person?
var reference2: Person?
var reference3: Person?

現在,創建一個新的Person實例並將其分配給以下三個變量之一:

reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"

注意,“John Appleseed is being initialized”消息是在調用Person類的構造器時打印的。這證實已經進行了初始化。
因爲將新的Person實例分配給reference1變量,所以現在有一個從reference1到新的Person實例的強引用。 因爲至少有一個強引用存在,所以ARC確保這個Person在內存中不會被釋放。
如果將同一個Person實例分配給另外兩個變量,則會創建兩個強引用,現在對單個Person實例有三個強引用:

reference2 = reference1
reference3 = reference1

如果通過將其中兩個變量設置爲ni l來斷開兩個強引用(包括原始引用 reference1),那麼單個強引用仍然保留,並且Person實例不會被釋放:

reference1 = nil

//此時不會調用deinit方法,直到最後的強引用斷開(reference3),ARC纔會釋放Person實例
reference2 = nil

當最後一個強引用被斷開時,系統纔會知道你不再使用Person實例,此時ARC纔會釋放它。

reference3 = nil
// Prints "John Appleseed is being deinitialized

3.類實例之間的強引用循環

在上面的示例中,ARC能夠跟蹤創建的新Person實例的引用數,並在不需要該Person實例時釋放它。
如果兩個類實例之間彼此保持強引用,使得每個實例保持另一個實例存活, 這稱爲強引用循環。通過將類之間的一些關係定義爲weak引用或unowned引用(後面會詳細介紹unowned),而不是強引用,可以解決強引用循環問題。 但是,在學習如何解決強引用循環問題之前,讓我們先了解強引用循環是如何引起的。

下面的示例將說明如何創建強引用循環,首先定義兩個類PersonApartment

class Person {

let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }

}

class Apartment {

let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }

}

每個Person實例都有一個類型爲String的name屬性,並且有一個可選的apartment屬性,初始值爲nil。apartment屬性是可選的,因爲Person也許不會一直有一個Apartment。類似地,每個Apartment實例都有一個String類型的unit屬性,並且有一個可選的tenant屬性,初始值爲nil。tenant 屬性是可選的,因爲Apartment也許不會一直有一個Person

然後我們定義兩個可選類型的變量johnunit4A,並創建一個特定的Person實例和Apartment實例,並將它們分配給johnunit4A變量:

var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john變量現在具有對新的Person實例的強引用,並且unit4A變量具有對新的Apartment實例的強引用,其關係如下圖(referenceCycle01)所示:

最後將兩個實例關聯起來,感嘆號(!)用於解包和訪問存儲在john和unit4A可選變量中的實例,以便可以設置這些實例的屬性(強制解析)。Optional Chaining

john!.apartment = unit4A
unit4A!.tenant = john

下面是強引用如何將兩個實例鏈接在一起:


然而鏈接這兩個實例就在它們之間創建了一個強引用循環。
Person實例現在具有對Apartment實例的強引用,並且Apartment實例具有對Person實例的強引用。因此,當您破壞johnunit4A變量所持有的強引用時,引用計數不會減爲零,實例也不會被ARC重新分配:

john = nil
unit4A = nil

當將這兩個變量設置爲nil時,兩個實例對象都未調用各自的析構器。強引用循環阻止了Person和Apartment實例被釋放,從而在應用程序中導致內存泄漏。將john和unit4A變量設置爲nil之後,實例之間的關係如下圖所示:


Person實例和Apartment實例之間的強引用仍然存在,並沒有被斷開。

4.解決類實例之間的強引用循環

當使用類類型的屬性時,Swift提供了兩種方法來解決強引用循環:weak引用和unowned引用。
weakunowned使一個引用循環中的一個實例引用另一個實例,而不保持強引用。 這樣實例之間可以彼此引用而不會產生強引用循環問題。
當另一個實例的生命週期較短時,即當另一個實例可以首先被釋放時,使用weak引用。相反,當另一個實例有相同的生命週期或更長的生命週期時,使用unowned引用。

(1)弱引用

Weak不會對引用的實例保持強引用,因此不會阻止ARC銷燬引用的實例。這樣就可以避免強引用循環。通過將關鍵字weak放在屬性或變量聲明之前來表明弱引用。

因爲弱引用不會對其引用的實例保持強引用,所以該實例有可能在弱引用仍然存在時就被釋放了。當它所引用的實例被釋放時,ARC會自動將弱引用設置爲nil。當ARC將弱引用設置爲nil時,屬性觀察器不會被調用。並且,因爲弱引用需要允許它們的值在運行時被設置爲nil,所以它們總是聲明爲可選類型的變量,而不是常量。

注意: 當ARC將弱引用設置爲nil時,不會調用屬性觀察者。

現在我們使用弱引用來解決上面的強引用循環問題,將Apartment中的tenant屬性聲明爲weak:


class Person {

let name: String
init(name: String) { self.name = name }

//Person對Apartment是強引用
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }

}

class Apartment {

let unit: String
init(unit: String) { self.unit = unit }

//Apartment在某時刻可以沒有Person是合理的,因此將其設置爲weak來解決強引用循環問題
//Person在運行時可能爲nil,因此將其設置爲可選類型的變量
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }

}

來自兩個變量(johnunit4A)的強引用和兩個實例之間的鏈接如前所述創建:


var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john

下面是鏈接這兩個實例後的引用:

Person實例仍然具有對Apartment實例的強引用,但是Apartment實例現在具有對Person實例的弱引用。 這意味着當你通過將john變量設置爲nil來斷開它的強引用時,就沒有對Person實例的更多強引用:


john = nil
// Prints "John Appleseed is being deinitialized"

因爲對Person實例沒有更多的強引用,它被釋放,並且tenant屬性設置爲nil,如下圖所示:

唯一剩下的對Apartment實例的強引用來自unit4A變量。 如果斷開對Apartment實例的強引用,則沒有對Apartment實例的更多強引用。

unit4A = nil
// Prints "Apartment 4A is being deinitialized"

因爲不再有對Apartment實例的強引用,所以它也被釋放了:

注意:在使用垃圾回收的系統中,弱指針有時用於實現簡單的緩存機制,因爲只有當內存壓力觸發垃圾回收時,纔會釋放沒有強引用的對象。然而,對於ARC,值在其最後一個強引用被刪除後立即被釋放,這使得弱引用不適合這樣做。

(2) unowned引用

像弱引用一樣,unowned引用也不會對它所引用的實例保持強引用。然而,與弱引用不同,當另一個實例有相同的生命週期或更長的生命週期時使用unowned引用。通過將關鍵字unowned放在屬性或變量聲明之前,來指明它是一個unowned引用。
一個unowned引用應該總是有一個值。因此,ARC永遠不會將unowned引用的值設置爲nil,這意味着使用非可選類型定義unowned引用。

注意: 只有當您確定這個引用總是引用着一個還未被釋放的實例時,才使用unowned引用。
如果試圖在實例被釋放後訪問unowned引用的值,將得到一個運行時錯誤。

因爲unowned引用是非可選的,所以每次使用它時,不需要解包,可以直接訪問。但是,當它所引用的實例被釋放時,ARC不能將它設置爲nil,因爲非可選類型的變量不能設置爲nil。另外,也不可再訪問該引用,否則會觸發一些運行時錯誤。只有確定一個引用始終引用一個實例時,才使用unowned 引用。

下面的示例定義了兩個類,CustomerCreditCard,它們對銀行客戶和該客戶可能使用的信用卡進行建模。這兩個類都將另一個類的實例存儲爲屬性。這種關係有可能創建一個強引用循環。
CustomerCreditCard的關係與上面弱引用示例中Apartment(公寓) 和Person 的關係略有不同。在這個數據模型中,客戶可能擁有信用卡,也可能沒有,但是信用卡總是與客戶相關聯的。一個CreditCard實例的生命週期永遠不會超過它所引用的客戶。爲了表示這一點,Customer類有一個可選的card屬性,但是CreditCard類有一個unowned的和非可選的Customer屬性。
此外,只能通過將一number的值和一個customer實例傳遞給自定義的CreditCard構造器來創建新的CreditCard實例。這確保在創建CreditCard實例時,CreditCard實例總是有一個與之關聯的customer實例。
因爲信用卡總是會有一個客戶,所以將用unowned修飾它的customer屬性,以避免強引用循環:

class Customer {
    let name: String
  //每個客戶可以持有或不持有信用卡,所以將屬性card定義可選類型的變量
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    deinit { print("\(name) is being deinitialized") }
}

class CreditCard {
    let number: UInt64
  /*
     1.每張信用卡總有個客戶與之對應,與每張信用卡相關聯的客戶不能爲空,而且不能更換,因此將customer屬性定義爲非可選的常量;
     2.由於信用卡始終擁有客戶,爲了避免強引用循環問題,所以將客戶屬性定義爲unowned
     */
    unowned let customer: Customer
 // 只能通過向初始化方法傳遞number和customer來創建CreditCard實例,確保CreditCard實例始終具有與其關聯
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card #\(number) is being deinitialized") }
}

注意:該示例中的類Customer(客戶)和CreditCard(信用卡)之間的關係與弱引用示例中的Apartment(公寓)和 Person(人)之間的關係略微不同。

下一個代碼片段定義了一個名爲john的可選類型Customer變量,該變量將用於存儲對特定客戶的引用。 由於是可選的,該變量的初始值爲nil:

var john: Customer?

現在可以創建一個Customer實例,並使用它初始化和分配一個新的CreditCard實例作爲該客戶的card屬性:

john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

下面是引用的關係,現在已經鏈接了兩個實例:


現在Customer的實例對CreditCard的實例是強引用,而CreditCard的實例對Customer的實例是unowned引用。
由於customer屬性是unowned引用,所以當斷開john變量對Customer實例的強引用後,則沒有對Customer實例的強引用了:

因爲沒有對Customer實例的強引用,所以它被釋放。Customer實例被釋放之後,就沒有對CreditCard實例的強引用,CreditCard實例也被釋放。

john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"

上面的最後一個代碼片段顯示,在john變量設置爲nil之後,Customer實例和CreditCard實例都打印了它們被銷燬的消息。

(3) unowned引用和隱式解包的可選屬性

上面weakunowned引用中的兩個示例涵蓋了兩種常見的情況,它們都是爲了解決強引用循環問題。
weak引用中的PersonApartment例子描述了一種情況:兩個類相互引用時,將彼此作爲自己的屬性,且這兩個屬性都允許爲nil,此時有可能會造成強引用循環問題。這種情況最好用weak來解決。unowned引用中的CustomerCreditCard 例子描述了另外一種情況:兩個類相互引用時,將彼此作爲自己的屬性,且一個屬性可以爲nil,而另外一個屬性不可爲nil,此時也有可能會造成強引用循環問題。這種情況最好用unowned來解決。
但是,還有第三種情況,其中兩個屬性應該總是有一個值,並且一旦初始化完成後,這兩個屬性都不應爲nil。 在這種情況下,將一個類的屬性設置爲unowned,另一個類的屬性設置爲隱式可選類型(!)。這使得兩個屬性在初始化完成後可以直接訪問(沒有可選的解包),同時避免了引用循環問題。
讓我們看看如何解決第三種情況造成的引用循環問題:下面的示例定義了兩個類:CountryCity,每個類都將另一個類的實例存儲爲屬性。在這個數據模型中,每個國家都必須有一個首都,每個城市都必須屬於一個國家。爲了表示這一點,Country類中有一個capitalCity屬性,而City類中有一個country屬性:

class Country {
    let name: String
    var capitalCity: City!
    init(name: String, capitalName: String) {
        self.name = name
     // 在Country的初始化方法中來創建City實例,並將此實例存儲在其capitalCity屬性中
     //在Country的初始化方法中調用City的初始化方法。 但是,只有完全初始化一個新的Country實例後,纔不能將self傳遞到City初始化器中
        self.capitalCity = City(name: capitalName, country: self)
    }
}

class City {
    let name: String
    unowned let country: Country
    //City的初始化器使用一個Country實例,並將此實例存儲在其country屬性中
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}

爲了在兩個類之間建立相互依賴關係,City的構造器獲取一個Country實例,並將該實例存儲在其country屬性中。

City的構造器是在Country的構造器內調用的。然而,只有當一個新的Country實例完全初始化之後,Country的構造器才能將self傳遞給City的構造器。(兩階段初始化
爲了滿足該要求,將CountrycapitalCity屬性聲明爲隱式解包的可選屬性,即City!。這意味着capitalCity屬性的默認值爲nil,與其他任何可選值一樣,但可以在不需要解包的情況下訪問其值。(隱式解包選項
因爲capitalCity具有默認值nil,所以只要Country實例在其構造器中設置name屬性,新的Country實例就認爲被完全初始化。這意味着Country構造器可以設置在name屬性後就可以開始引用和傳遞隱式self屬性。因此,當Country構造器正在設置其capitalCity屬性時,Country構造器可以傳遞self作爲City構造器的參數之一。

這意味着可以在單個語句中創建CountryCity實例,也不會創建強引用循環,而且可以直接訪問capitalCity屬性:

var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// Prints "Canada's capital city is called Ottawa"

在上面的示例中,使用隱式解包可選方法意味着滿足所有兩階段類構造器需求(Two-Phase Initialization)。在初始化完成後,capitalCity屬性可以像非可選值一樣使用和訪問,同時避免了強引用循環。

3.閉包的強引用循環

如果將閉包分配給類實例的屬性,並且該閉包的主體捕獲該實例,則也可能發生強引用循環。這個捕獲可能發生,因爲閉包的主體訪問實例的屬性,比如self.someProperty。或者因爲閉包調用實例的方法,比如self.someMethod()。在任何一種情況下,這些訪問都會導致閉包“捕獲”self,從而創建一個強引用循環。
之所以會出現這種強引用循環,是因爲閉包和類一樣都是引用類型。當你將閉包分配給一個屬性時,就增加了一個對該閉包的引用。實質上,它與上面的問題相同 - 兩個強引用相互保持對方存活。但是,這次是一個類實例和一個閉包,而不是兩個類實例。
Swift爲這個問題提供了一個優雅的解決方案,稱爲閉包捕獲列表。然而,在學習如何使用閉包捕獲列表來打破強引用循環之前,首先來了解導致這種循環引用的原因。
下面的示例展示了在使用引用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")
    }
}

HTMLElement類定義了一個name屬性,可選的text屬性,還有懶加載屬性asHTML 。默認情況下,asHTML屬性被分配一個閉包。

例如,可以將asHTML屬性設置爲一個閉包,如果text屬性爲nil,該閉包返回默認值,以防止表示返回空的HTML標記:

let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
    return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// Prints "<h1>some default text</h1>"

下面是使用HTMLElement類創建和打印新實例的方法:

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"

不幸的是,如上所述,HTMLElement類在其實例和用於asHTML默認值的閉包之間創建了一個強引用循環。如下圖所示:

注意:即使閉包多次引用self,它也只捕獲對HTMLElement實例的一個強引用。

如果將paragraph變量設置爲nil並且斷開其對HTMLElement實例的強引用,則由於強引用循環,HTMLElement實例及其閉包都不會被釋放:

paragraph = nil

注意HTMLElement析構器中的消息沒有被打印出來,這表明HTMLElement實例沒有被釋放。

4.解決閉包的強引用循環

通過將捕獲列表作爲閉包定義的一部分,這可以解決閉包和類實例之間的強引用循環。捕獲列表定義了在閉包主體中捕獲一個或多個引用類型時要使用的規則。與兩個類實例之間的強引用循環一樣,將每個捕獲的引用聲明爲weak引用或unowned引用,而不是強引用。至於選擇weak引用,還是unowned引用,取決於代碼不同部分之間的關係。

(1). 定義捕獲列表

捕獲列表中的每一項都是weakunowned關鍵字與類實例(例如self)引用的配對,或者是與用某個值初始化的變量的配對(例如delegate = self.delegate!)這些配對寫在一對用逗號分隔的方形大括號中。
將捕獲列表放在閉包的參數列表之前,如果提供了它們,則返回類型:

lazy var someClosure: (Int, String) -> String = {
    [unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
    // closure body goes here
}

如果閉包沒有指定參數列表或返回類型,因爲它們可以從上下文中推斷出來,請將捕獲列表放在閉包的最開始位置,後面跟着in關鍵字:

lazy var someClosure: () -> String = {
    [unowned self, weak delegate = self.delegate!] in
    // closure body goes here
}
(2).weak 和 unowned 引用

當閉包和它捕獲的實例總是相互引用,並且總是在同一時間被釋放時,在閉包中定義一個unowned引用。
相反,當捕獲的引用在將來的某個時刻可能變成nil時,將捕獲定義爲weak引用。weak引用總是可選類型,當它們引用的實例被釋放時,它會自動變成nil。這使你能夠檢查它們是否存在於閉包的主體中。

注意:如果捕獲的引用永遠不會變成nil,那麼它應該總是被捕獲爲一個unowned引用,而不是一個weak引用。

unowned引用是用於解決HTMLElement示例中強引用循環的適當捕獲方法,該方法來自上面的Closures的強引用循環。 以下是編寫HTMLElement類以避免循環的方法:

unowned引用是用於從上面的閉包的強引用循環中解析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) is being deinitialized")
    }

}

除了在asHTML閉包中添加捕獲列表之外,HTMLElement的這種實現與先前的實現相同。 在這種情況下,捕獲列表是[unowned self],這意味着將self捕獲爲unowned用而不是強引用
可以像以前一樣創建和打印HTMLElement實例:

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"

下面是使用捕獲列表時引用關係圖:


這一次,閉包對self的捕獲是一個unowned引用,並沒有對它捕獲的HTMLElement實例保持強引用。 如果將paragraph變量中的強引用設置爲nil,則會釋放HTMLElement實例,這可以從下面示例中的析構器消息的打印中看出:

paragraph = nil
// Prints "p is being deinitialized"

有關捕獲列表的更多信息,請查看Capture Lists

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章