當我們在編寫程式時,你可能會發現有些變數的 Reference 是 strong
、weak
或 unowned
,它們代表甚麼意思呢?把所有變數宣告 Strong Reference 是否就會讓變數更強呢?
Strong
、weak
與 unowned
的使用,其實與 Swift 記憶體管理的 Automatic Reference Counting (自動參考計數機制, ARC)有關。首先,我們來好好了解這些的意思。ARC 一如其名就是用於自動參考計數,在電腦科學的定義來說,Reference Counting 是以一項技術,是將資源(例如物件、記憶體或磁盤空間等)被參考的次數保存起來。簡單來說,ARC 可以把參考儲存到記憶體中,並自動清除沒有在使用的參考。
另外,Reference Counting 僅適用於類別 (class) 的實例 (instance),而不適用於結構 (structures) 和枚舉 (enumerations),因為他們兩個都是數值型別 (Value Type),而不是參考型別 (Reference Type)。
進入下一部分前,讓我們來想想為什麼記憶體管理這麼重要?因為記憶體管理在分配記憶體方面很重要,這樣程式才可以根據用戶請求來運行,在不需要程式時,記憶體亦可釋放重用。
但如果你耗盡了所有記憶體,會發生什麼事呢?
- 你想執行的程序作業將會中斷,讓你無法再執行任何程序作業。
- 程序作業可能無法繼續,但會持續執行直到程式達到極限而崩潰。
- 你應該不會想讓使用者用一個有問題的程式。
甚麼是 ARC(自動參考計數機制)?
如官方文件說明:
Swift 的記憶體管理會一直運作,你不需要自己去考慮記憶體管理的問題。當實例不再被使用時,ARC 會自動釋放所佔用的記憶體。
ARC 也會持續追踨相關資訊,像是程式碼之間的關係等,也因此 ARC 能夠有效地管理記憶體資源。
ARC 是如何運作的?
每當你要初始化 init()
一個類別時,ARC 會自動配置記憶體來儲存資料;更具體來說,就是一部份的記憶體配置了給實例,並同時在屬性配置了數值,所以當不再需要實例時,deinit()
就會被呼叫,而 ARC 會將此實例的記憶體空間釋出。
讓我們看看下面的程式碼,雖然程式碼很容易明白,但我還是會解釋一下。這個範例包含了兩個類別實例 Person
和 Gadget
,兩者皆有初始 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
類別的範例,該實例有型別為 String
的 name
屬性。這個 Person
類別有 init
方法,用來設定實例的 name
屬性,意味著我們將會分配資訊到記憶體內。接下來的程式碼有 deinit
方法,用來釋放實例,意味著記憶體內的資訊配置將會被釋放。而且,Person
實例包含了型別為 String
的 gadget
屬性和可選擇型別 (optional) 的 gadget
,因為 Person 不一定擁有 Gadget;同樣的理論也適用於 Gadget
類別。
Strong、Weak 和 Unowned Reference 的差別
這幅圖片應該可以讓你理解各種 Reference 的使用要求:
簡單介紹:Strong vs Weak vs Unowned
- 通常當一個屬性被建立時,除非 Reference 被設置為
weak
或unowned
,否則會預設為 strong, - 當屬性被設為
weak
時,Reference count 不會增加 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 運行,像是這樣:
看看控制台的訊息,你會發現兩個變數都已被銷毀 (deinitialized)。
現在加入下列程式碼,將 iphone
指定給 kelvin
,並將 iphone
的擁有者設為 kelvin。請確認你將把下列這段程式碼放在設定 kelvin
和 iphone
為 nil
之前。
kelvin!.gadget = iphone
iphone!.owner = kelvin
這個例子展示了我們把兩個實例互相鏈結時,strong reference 是怎樣的。請記住,Person
類別內還有一個變數gadget
,這裡我們給它 iphone
變數的數值。下圖闡述了程式碼所做的事:
讓我們在 Playgrounds 再次執行這個程式碼,你應該會注意到控制台只顯示初始化的訊息。
很明顯地,當我們解除 strong reference,reference count 並沒有歸零,而 ARC 也沒有釋放這實例。為什麼呢?讓我盡量用圖像解釋這個情況。因為即使 kelvin
和 iphone
設為 nil
,Person
實例與 Gadget
實例之間的 strong reference 仍然持續。
這就是我們說的 strong reference 循環,亦是造成 app 記憶體洩漏的原因。為了打破 strong reference 循環並防止記憶體洩漏,你將要了解如何使用 weak
和 unowned
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 執行程式碼,並看看結果會怎樣。
如你所見,控制台的訊息顯示兩個變數被釋放了。將 owner
變數更改為 weak 後,兩個實例的關係和前一部分有點不同了:
有了 weak
reference,當你想設定 kelvin
為 nil
時,變數可以很順利地被釋收,因為並沒有指向 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")
}
}
如你所見,Gadget
的 owner
變數現在被定義為一個不可選型別變數,也就是一個 unowned
reference。因此,初始化方法會被修改來接受 owner
為參數。Person
和 Gadget
的關係與 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 執行程式,你應該得到這樣的結果:
如你所見,Person
和 Gadget
的實例都被釋放了。因為我們解除了 Person
實例(也就是 kelvin
)的 strong reference,所以實例就可以被釋放。因此,再沒有 strong reference 指向 Gadget
實例,它也就自動被釋放了。
結論
看過這篇文章後,希望你對 strong、weak 和 unowned references 有更深入的了解。Xcode 的內建功能讓開發者可以解決記憶體問題,例如,若你想偵測記憶體洩漏問題,你可以到 menu 選擇 Product > Profile > Leaks。如果你想我們多撰寫有關記憶體管理的問題,以及解決方法,歡迎留言告訴我們。
原文:Memory Management in Swift: Understanding Strong, Weak and Unowned References