關於記憶體管理
當我們選擇這條職業道路的時候,不可避免的我們都要記憶體管理打交道。無論是C中的malloc、free還是C++中的new、delete。它如此重要又如此麻煩易錯。為了把大家從記憶體管理解脫出來,C++中引入了智慧指標,iOS中引入了ARC(automatic reference counting),其實兩種的原理都是一樣的就是對動態分配的物件進行自動引用技術確保物件能夠正確銷燬,防止出現記憶體洩露。下面我們就一起了解一下Swift中該機制。
Swift中的ARC
在Swift中對於引用型別的物件採用的就是自動記憶體管理,也就是說系統會幫我們處理好記憶體的申請和分配。當我們初始化建立物件的時候系統自動分配記憶體,而在釋放記憶體的時候會遵循自動引用計數原則:當物件的引用計算為0的時候記憶體會自動被回收。這樣一來我們只需要將我們的注意力放在在合適的地方置空引用就行了。再次提醒一下:Swift中的ARC只針對引用型別物件。首先,我們從一段簡單的Objective-C程式碼來認識該機制:
NSObject *box = [NSObject new];
//More code, and then later on…
[box release];
該段程式碼中我們先建立了一個NSObject類物件box,此時物件的retain計數為1,後面我們對box進行了release操作,那麼此時的box的retain計數為0,然後系統會自動銷燬box物件,box也就變為nil。
對於我們中的大多數來說,我們其實並不清楚記憶體中發生的了什麼。這不是什麼諷刺,僅僅是因為我們只是習慣性的享受便利,而放棄去探究其機制。但是這是不正確的,我們總會在某個時間會遇到於記憶體管理相關的內容,如果我們對這個特性僅僅停留在使用上面,而沒有一個清新的認識的話那麼當問題出現的時候,你會發現問題非常難調適(相信使用過C++的人對此肯定深有感觸)。我們來看一個Swift的例子:
class Post
{
var topic:String
init(t:String)
{
self.topic = t
print(“A post lives!”)
}
deinit
{
print(“A post has passed away ?”)
}
}
//An optional of type Post, so the default value is nil so far
var postRef:Post?
//Console prints `A post lives!`, postRef has a strong ref
postRef = Post(t: "iOS")
//This instance of Post now has another strong ref, so its
//retain count now sits at 2
var anotherPostRef = postRef
//postRef equals nil, one strong ref is gone. Retain count is 1
postRef = nil
//ARC knew it still had a strong ref due to keeping a retain count.
//Thus, anotherPostRef is *not* nil
print(anotherPostRef)
上面的例子很好理解,首先Post是一個引用型別適用ARC規則,後面定義了一個可選型別的變數預設為nil,然後初始化了該變數引用技術加1,後來又賦值給另一個變數再加1,在置空postRef此時引用減1,物件引用數不為0沒有銷燬可以繼續列印anotherPostRef。如果我們在最後再新增 anotherPostRef = nil,此時物件就會自動銷燬。
迴圈引用的坑
但是,自動引用計數機制中也有一個坑需要我們注意。那就是迴圈引用,類似於作業系統中的死鎖。下面看一下示例程式碼:
class Post
{
var topic:String
var performance: Analytics?
init(t:String)
{
self.topic = t
print(“A topic lives!”)
}
deinit
{
print(“A topic has passed away ?”)
}
}
class Analytics
{
var thePost:Post?
var hits:Int
init(h: Int)
{
self.hits = h
print(“Some analytics are being served on up.”)
}
deinit
{
print(“Analytics are gone!”)
}
}
var aPost:Post? = Post(t: “ARC”)
var postMetrics:Analytics? = Analytics(h: 3422)
aPost!.performance = postMetrics
postMetrics!.thePost = aPost
//Ruh roh, postMetrics`s deinit func wasn`t called!
aPost = nil
我們可以看到上面的程式碼中,解構函式並沒有呼叫。因為aPost持有物件postMetrics,析構時也要析構postMetrics,而postMetrics物件析構的前提卻是要析構aPost,這就造成了一個死鎖。除非殺死程式否則這兩個物件都不會被釋放掉。
迴圈引用的解決
為了避免這種情況的出現,我們需要使兩個例項不能互相持有對方,將類Analytics中的thePost變數宣告改為:
weak var thePost: Post?
我們在變數前面加上了weak宣告,也就是告訴編譯器表明我們並不希望持有thePost變數。因此當aPost = nil時物件都可以成功析構,會列印如下資訊:
A topic has passed away ?
Analytics are gone!
注意:因為weak宣告的變數在變數析構後會將變數置為nil,所以變數一定時可選(Optional)型別。
除了weak宣告外,還有一個不常用的關鍵字unowned,於weak類似該宣告也是表面非持有關係,當時兩種存在區別。Swift中的weak、unowned對應Objective-C中的weak、unsafe_unretained,這表明前者在物件釋放後 會自動將物件置為nil,而後者依然保持一個“無效的”引用,如果此時呼叫這個“無效的”引用將會引起程式崩潰。對於後者的使 用,我們必須保證訪問的時候物件沒有釋放。相反,weak則友好一點我們也更加習慣使用它,最常見的場景就是:
-
設定delegate時
-
在self屬性儲存為閉包,閉包中存在self時。
第一種情況常見就不說了,對於第二個其實是一個容易忽略的地方。閉包的一個特性就是對於閉包中的所有元素它都自動持有,因此如果閉包了包含了self的話,就會形成一個self -> 閉包 -> self的迴圈引用,可以採用下面的方法解決:
class Person {
let name: String
lazy var printName: ()->() = {
[weak self] in
if let strongSelf = self {
print("The name is (strongSelf.name)")
}
}
init(personName: String) {
name = personName
}
deinit {
print("Person deinit (self.name)")
}
}
var BigNerd: Person? = Person(personName: "BigNerd")
BigNerd!.printName()
BigNerd = nil
//輸出:
The name is BigNerd
Person deinit BigNerd
此處是在閉包內部新增了weak宣告來消除迴圈引用。此處也可以使用unowned,這樣還可以省去if let的判斷,因為整個過程中self沒有被釋放。注意這個前提,前提不成立的話,還是可能會出現問題。