Memory Management

記憶體管理:了解 Strong、Weak 和 Unowned Reference 輕鬆解決記憶體洩漏

當我們在編寫程式時,你可能會發現有些變數的 Reference 是 strong、weak 或 unowned,是甚麼意思呢?Strong、weak 與 unowned 的使用,其實與 Swift 記憶體管理 Automatic Reference Counting 有關。
記憶體管理:了解 Strong、Weak 和 Unowned Reference  輕鬆解決記憶體洩漏
記憶體管理:了解 Strong、Weak 和 Unowned Reference  輕鬆解決記憶體洩漏
In: Memory Management, Swift 程式語言

當我們在編寫程式時,你可能會發現有些變數的 Reference 是 strongweakunowned,它們代表甚麼意思呢?把所有變數宣告 Strong Reference 是否就會讓變數更強呢?

Strongweakunowned 的使用,其實與 Swift 記憶體管理的 Automatic Reference Counting (自動參考計數機制, ARC)有關。首先,我們來好好了解這些的意思。ARC 一如其名就是用於自動參考計數,在電腦科學的定義來說,Reference Counting 是以一項技術,是將資源(例如物件、記憶體或磁盤空間等)被參考的次數保存起來。簡單來說,ARC 可以把參考儲存到記憶體中,並自動清除沒有在使用的參考。

另外,Reference Counting 僅適用於類別 (class) 的實例 (instance),而不適用於結構 (structures) 和枚舉 (enumerations),因為他們兩個都是數值型別 (Value Type),而不是參考型別 (Reference Type)。

進入下一部分前,讓我們來想想為什麼記憶體管理這麼重要?因為記憶體管理在分配記憶體方面很重要,這樣程式才可以根據用戶請求來運行,在不需要程式時,記憶體亦可釋放重用。

但如果你耗盡了所有記憶體,會發生什麼事呢?

  1. 你想執行的程序作業將會中斷,讓你無法再執行任何程序作業。
  2. 程序作業可能無法繼續,但會持續執行直到程式達到極限而崩潰。
  3. 你應該不會想讓使用者用一個有問題的程式。

甚麼是 ARC(自動參考計數機制)?

如官方文件說明:

Swift 的記憶體管理會一直運作,你不需要自己去考慮記憶體管理的問題。當實例不再被使用時,ARC 會自動釋放所佔用的記憶體。

ARC 也會持續追踨相關資訊,像是程式碼之間的關係等,也因此 ARC 能夠有效地管理記憶體資源。

ARC 是如何運作的?

每當你要初始化 init() 一個類別時,ARC 會自動配置記憶體來儲存資料;更具體來說,就是一部份的記憶體配置了給實例,並同時在屬性配置了數值,所以當不再需要實例時,deinit() 就會被呼叫,而 ARC 會將此實例的記憶體空間釋出。

讓我們看看下面的程式碼,雖然程式碼很容易明白,但我還是會解釋一下。這個範例包含了兩個類別實例 PersonGadget,兩者皆有初始 init 方法來設置實例的屬性,意味著可以將任何資訊配置到記憶體。另外有了 deinit 方法,我們將看到實例被釋放,意味著含有資訊的記憶體將會釋放。

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    var gadget: Gadget?
    deinit {
        print("\(name) is being deinitialized")
    }
}

class Gadget {
    let model: String
    init(model: String) {
        self.model = model
        print("\(model) is being initialized")
    }
    var owner: Person?
    deinit {
        print("\(model) is being deinitialized")
    }
}

上面是一個 Person 類別的範例,該實例有型別為 Stringname 屬性。這個 Person 類別有 init 方法,用來設定實例的 name 屬性,意味著我們將會分配資訊到記憶體內。接下來的程式碼有 deinit 方法,用來釋放實例,意味著記憶體內的資訊配置將會被釋放。而且,Person 實例包含了型別為 Stringgadget 屬性和可選擇型別 (optional) 的 gadget,因為 Person 不一定擁有 Gadget;同樣的理論也適用於 Gadget 類別。

Strong、Weak 和 Unowned Reference 的差別

這幅圖片應該可以讓你理解各種 Reference 的使用要求:

arc-strong-weak-unowned

簡單介紹:Strong vs Weak vs Unowned

  1. 通常當一個屬性被建立時,除非 Reference 被設置為 weakunowned,否則會預設為 strong,
  2. 當屬性被設為 weak 時,Reference count 不會增加
  3. Unowned reference 剛好在兩者中間,既不是 strong,也不是可選擇型別。因為 Reference 是維持被分配的狀態,編譯器會假定物件未被釋放。

Strong reference

我們來看看下面的例子。這裡有一個 Person 型別、Reference 為 “Kelvin” 的變數,以及一個 Gadget 型別、Reference 為 “iPhone 8 Plus” 的變數。

var kelvin: Person?
var iphone: Gadget?

kelvin = Person(name: "Kelvin")
iphone = Gadget(model: "iPhone 8 Plus")

現在如果你把兩個變數都設為 nil,並在 Playground 運行,像是這樣:

Memory Management in Swift

看看控制台的訊息,你會發現兩個變數都已被銷毀 (deinitialized)。

現在加入下列程式碼,將 iphone 指定給 kelvin,並將 iphone 的擁有者設為 kelvin。請確認你將把下列這段程式碼放在設定 kelviniphonenil 之前。

kelvin!.gadget = iphone
iphone!.owner = kelvin

這個例子展示了我們把兩個實例互相鏈結時,strong reference 是怎樣的。請記住,Person 類別內還有一個變數gadget,這裡我們給它 iphone 變數的數值。下圖闡述了程式碼所做的事:

讓我們在 Playgrounds 再次執行這個程式碼,你應該會注意到控制台只顯示初始化的訊息。

memory-management-swift-sample-2

很明顯地,當我們解除 strong reference,reference count 並沒有歸零,而 ARC 也沒有釋放這實例。為什麼呢?讓我盡量用圖像解釋這個情況。因為即使 kelviniphone 設為 nilPerson 實例與 Gadget 實例之間的 strong reference 仍然持續。

這就是我們說的 strong reference 循環,亦是造成 app 記憶體洩漏的原因。為了打破 strong reference 循環並防止記憶體洩漏,你將要了解如何使用 weakunowned reference.

Weak reference

Weak reference 經常被宣告為可選擇型別,因為這樣變數的數值就可以被設為 nil。為了設定 Reference 為 weak,你可以很簡單地將 weak 這個字輸入到屬性或變數宣告中:

weak var owner: Person?

當實例要被釋放時,ARC 就會自動設定 weak reference 為 nil。而且因為數值的改變,我們知道這裡需要使用變數,不然用了常數的話就無法改變數值了。

我們可以使用 weak references 來打破 strong reference 循環。Weak reference 不會一直佔用實例,利用 weak reference,我們可以解決如上一節提到的記憶體洩漏問題。我們如下修改 Gadget 類別,並將 owner 宣告為 weak reference:

class Gadget {
    let model: String
    init(model: String) {
        self.model = model
        print("\(model) is being initialized")
    }
    weak var owner: Person?
    deinit {
        print("\(model) is being deinitialized")
    }
}

現在,重新在 Playgrounds 執行程式碼,並看看結果會怎樣。

memory-management-swift-sample-3

如你所見,控制台的訊息顯示兩個變數被釋放了。將 owner 變數更改為 weak 後,兩個實例的關係和前一部分有點不同了:

有了 weak reference,當你想設定 kelvinnil 時,變數可以很順利地被釋收,因為並沒有指向 Person 實例的 strong reference。

Unowned reference

Unowned reference 與 weak reference 十分相似,它也可以解決 strong reference 循環問題。兩者最大的差異就是 unowned reference 一定要有數值,ARC 不會將 unowned reference 的數值設為 nil。換句話說,這個 reference 將會被宣告為非選擇式型別 (non-optional types)。
如果在取消分配該實例後嘗試訪問無主引用的值,則會出現運行時錯誤。

Unowned reference 只適用於一個情況,就是你確信這個 Reference 的實例從沒被釋放過。如果你在實例已經被釋放後,嘗試取得一個 unowned reference 的數值,運行就會出錯。

因為 unowned reference 不能是可選擇型別的,我們將會稍微更改一下程式碼:

class Person {
    let name: String

    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }

    var gadget: Gadget?
    deinit {
        print("\(name) is being deinitialized")
    }
}

class Gadget {
    let model: String
    unowned var owner: Person

    init(model: String, owner: Person) {
        self.model = model
        self.owner = owner
        print("\(model) is being initialized")
    }

    deinit {
        print("\(model) is being deinitialized")
    }
}

如你所見,Gadgetowner 變數現在被定義為一個不可選型別變數,也就是一個 unowned reference。因此,初始化方法會被修改來接受 owner 為參數。PersonGadget 的關係與 weak reference 的範例有點不同,這裡的 Person 可能擁有一個 gadget,但一個 Gadget 卻一定需要有對應的 owner。

現在來看看實例的配置與釋放是如何運作的。先宣告 kelvin 變數為可選擇的 Person 型別。

var kelvin: Person?

然後,建立一個 Person 實例,並指定一個 Gadget 的實例為 Person 的 gadget 屬性。

kelvin = Person(name: "Kelvin")
kelvin!.gadget = Gadget(model: "iPhone 8 Plus", owner: kelvin!)

下面的圖片說明了兩個實例連結在一起,unowned referce 會是怎樣的。Person 的實例以 strong reference 指向至 Gadget 實例,而 Gadget 實例是以 unowned reference 指向 Person 實例。

讓我們來嘗試將 kelvin 變數設為 nil,來解除 strong reference。

kelvin = nil

在 Playgrounds 執行程式,你應該得到這樣的結果:

memory-management-swift-unowned

如你所見,PersonGadget 的實例都被釋放了。因為我們解除了 Person 實例(也就是 kelvin)的 strong reference,所以實例就可以被釋放。因此,再沒有 strong reference 指向 Gadget 實例,它也就自動被釋放了。

結論

看過這篇文章後,希望你對 strong、weak 和 unowned references 有更深入的了解。Xcode 的內建功能讓開發者可以解決記憶體問題,例如,若你想偵測記憶體洩漏問題,你可以到 menu 選擇 Product > Profile > Leaks。如果你想我們多撰寫有關記憶體管理的問題,以及解決方法,歡迎留言告訴我們。

譯者簡介:Oliver Chen-工程師,喜歡美麗的事物,所以也愛上 Apple,目前在 iOS 程式設計上仍是新手,正研讀 Swift 與 Sketch 中。生活另一個身份是兩個孩子的爸,喜歡和孩子一起玩樂高,幻想著某天自己開發的 App,可以讓孩子覺得老爸好棒!。聯絡方式:電郵 [email protected]

原文Memory Management in Swift: Understanding Strong, Weak and Unowned References

作者
Kelvin Tan
自學的 iOS 手機程式開發者,最近積極於自己的網站 www.daddycoding.com 上寫教學文章。來自馬來西亞,精通四種語言。如有任何問題,歡迎與他聯絡。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。