strong、weak和unowned的區別

編寫代碼時需注意是否產生了循環引用,因此就產生了什麼時候使用weakunowned問題?這篇文章將介紹 Swift 中的strongweakunowned的區別。

1. ARC

自動引用計數(即 Automated Reference Count,簡稱 ARC)是 Xcode 4.2版本的新特性,其與手動管理內存使用了相同的計數系統。不同點在於:系統在編譯時會幫助我們插入合適的內存管理方法,保留和釋放都會自動進行,避免了手動管理引用計數的一些潛在問題。

Swift 使用自動引用計數跟蹤、管理app的內存。通常情況下,這意味着ARC會自動管理內存,開發者無需關注內存管理。當類的實例不再使用時,ARC會自動釋放其佔用的內存。

爲幫助管理內存,ARC 有時需瞭解類之間的關係。在 Swift 中使用 ARC 與在 Objective-C 中使用 ARC 類似。

引用計數只適用類的實例。結構體和枚舉是值類型,不是引用類型,存儲和傳遞的時候並非使用引用。

2. strong

strong指針通過增加指向對象的引用計數,保護被指向對象不被ARC釋放。即,只要有一個強指針指向該對象,它就不會被釋放。

Swift 中聲明的屬性默認是strong。當對象間引用關係是線性時,使用strong指針不會產生問題。

當兩個實例使用強指針指向彼此時,兩個實例引用計數都不會變爲零,即產生循環引用(strong reference cycle)。

下面是一個循環引用的示例:

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

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

上面定義了兩個類:PersonApartment,代表住戶和公寓。兩個類都實現了deinitializer方法,當類的實例銷燬時進行打印,方便觀察實例佔用的內存是否釋放了。

下面代碼定義了兩個可選類型的變量,初始值爲nil。併爲其分配兩個新的實例:

var john: Person?
var unit4A: Apartment?

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

目前,john變量強引用Person實例,unit4A變量強引用Apartment實例,如下所示:

現在連接兩個實例。person持有apartmentapartment持有person

john?.apartment = unit4A
unit4A?.tenant = John

連接兩個實例後,引用關係如下:

Person的實例強引用了Apartment的實例,Apartment的實例強引用了Person的實例,即產生了循環引用。當移除johnunit4A的引用時,實例的引用計數不會變爲零,也就是實例內存不會被ARC釋放。

john = nil
unit4A = nil

設置johnunit4A變量爲nil後,引用關係如下:

PersonApartment實例之間的強引用無法破除。

3. 解決循環引用

Swift 提供了兩種解決循環引用的方案:weakunownedweakunowned引用其它實例時不會產生強引用,引用計數不會加一。因此,不會產生循環引用。

當一個實例的生命週期短於另一個時(即一個實例可以先被銷燬),使用weak引用。在上面公寓的示例中可能出現公寓沒有住戶的情況。因此,可以使用weak解決循環引用問題。當另一個實例生命週期與當前實例相同,或長於當前實例時,使用unowned引用。

3.1 weak引用

weak引用不會強持有引用的實例,也就不會阻止ARC釋放實例。通過在聲明屬性、變量前添加weak關鍵字的方式使用弱引用。

當實例被銷燬時,ARC 會自動設置弱指針爲nil。由於弱指針在運行時可能被設置爲nil,弱指針應被聲明爲可選類型的變量,而非常量。

和其它可選類型一樣,可以檢查弱引用值是否存在,這樣就不會得到一個無效實例。

設置弱引用爲nil時,不會調用屬性觀察器。

使用weak修飾之前實例Apartment中的tenant屬性,更新後如下:

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

class Apartment {
    let unit: String
    init(unit: String) {
        self.unit = unit
    }
    
    // 使用weak修飾
    weak var tenant: Person?
    deinit {
        print("Apartment \(unit) is being deinitialized")
    }
}

下圖是實例間引用關係:

Person實例強引用Apartment實例,但Apartment實例沒有強引用Person實例。當移除john實例對Person的強引用,Person實例就沒有被強引用了,也就可以被銷燬了。

3.2 unowned

weak一樣,unowned指針也不會對指向的對象產生強引用,但unowned用在另一個實例生命週期一樣或更長的情況。通過在聲明屬性、變量前添加unowned關鍵字的方式使用unowned

weak不同,unowned修飾的引用永遠不爲空。因此,標記爲unowned的值不是可選類型,ARC 也不會將unowned引用設置爲nil

只有確信引用不會被釋放的時候才使用unowned,使用unowned修飾的對象被銷燬後再次訪問會產生運行時錯誤。

現在定義兩個類:CustomerCreditCardCustomer是銀行的客戶,CreditCard是該客戶的銀行卡。CustomerCreditCard類都有一個屬性持有彼此,這種持有關係會產生強引用。

CustomerCreditCard的關係與PersonApartment的關係稍有不同。Customer可能持有CreditCard,也可能不持有CreditCard;但CreditCard不會脫離Customer而存在。即Customer有一個可選類型的card屬性,CreditCard有一個 unowned 的customer屬性。

class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    
    deinit {
        print("Card #\(number) is being deinitialized")
    }
}

下面創建Customer實例,並使用該實例創建CreditCard,如下所示:

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

其引用關係如下:

Customer實例強引用CreditCard實例,CreditCard實例 unowned Customer實例。

john變量取消對實例的強引用後,就沒有強引用指向該實例,該實例就會被銷燬。該實例銷燬後,沒有強引用指向CreditCard,其也會被銷燬。

上面的示例介紹瞭如何使用 safe unowned 引用,Swift 同時提供了 unsafe unowned 引用,其可以避免 runtime 的安全檢查,提高性能。使用 unsafe 相關操作時,開發者需自行檢查其是否存在,確保安全。

使用unowned(unsafe)標記 unsafe unowned 引用。當實例銷燬後,再次訪問實例會直接訪問銷燬前的內存地址。

參考資料:

  1. What is the difference in Swift between 'unowned(safe)' and 'unowned(unsafe)'?
  2. Automatic Reference Counting

歡迎更多指正:https://github.com/pro648/tips

本文地址:https://github.com/pro648/tips/blob/master/sources/strong%E3%80%81weak%E5%92%8Cunowned%E7%9A%84%E5%8C%BA%E5%88%AB.md

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