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實例,如下所示:

UnownedReferenceCycle01.png

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

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

連接兩個實例后,引用關系如下:

UnownedReferenceCycle02.png

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

john = nil
unit4A = nil

設置johnunit4A變量為nil后,引用關系如下:

UnownedReferenceCycle03.png

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")
    }
}

下圖是實例間引用關系:

UnownedWeakReference01.png

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!)

其引用關系如下:

UnownedUnownedReference01.png

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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容