swift 內存管理

不管在什麼語言裏,內存管理的內容都很重要,所以我打算花上比其他 tip 長一些的篇幅仔細地說說這塊內容。

Swift 是自動管理內存的,這也就是說,我們不再需要操心內存的申請和分配。當我們通過初始化創建一個對象時,Swift 會替我們管理和分配內存。而釋放的原則遵循了自動引用計數 (ARC) 的規則:當一個對象沒有引用的時候,其內存將會被自動回收。這套機制從很大程度上簡化了我們的編碼,我們只需要保證在合適的時候將引用置空 (比如超過作用域,或者手動設爲 nil 等),就可以確保內存使用不出現問題。

但是,所有的自動引用計數機制都有一個從理論上無法繞過的限制,那就是循環引用 (retain cycle) 的情況。

什麼是循環引用

雖然我覺得循環引用這樣的概念介紹不太應該出現在這本書中,但是爲了更清晰地解釋 Swift 中的循環引用的一般情況,這裏還是簡單進行說明。假設我們有兩個類 A 和 B, 它們之中分別有一個存儲屬性持有對方:

class A {
let b: B
init() {
b = B()
b.a = self
}

deinit {
    println("A deinit")
}

}

class B {
var a: A? = nil
deinit {
println(“B deinit”)
}
}
在 A 的初始化方法中,我們生成了一個 B 的實例並將其存儲在屬性中。然後我們又將 A 的實例賦值給了 b.a。這樣 a.b 和 b.a 將在初始化的時候形成一個引用循環。現在當有第三方的調用初始化了 A,然後即使立即將其釋放,A 和 B 兩個類實例的 deinit 方法也不會被調用,說明它們並沒有被釋放。

func application(application: UIApplication!,
didFinishLaunchingWithOptions launchOptions: NSDictionary!)
-> Bool
{

// Override point for customization after application launch.

var obj: A? = A()
obj = nil
// 內存沒有釋放

return true

}
因爲即使 obj 不再持有 A 的這個對象,b 中的 b.a 依然引用着這個對象,導致它無法釋放。而進一步,a 中也持有着 b,導致 b 也無法釋放。在將 obj 設爲 nil 之後,我們在代碼裏再也拿不到對於這個對象的引用了,所以除非是殺掉整個進程,我們已經永遠也無法將它釋放了。多麼悲傷的故事啊..

在 Swift 裏防止循環引用

爲了防止這種人神共憤的悲劇的發生,我們必須給編譯器一點提示,表明我們不希望它們互相持有。一般來說我們習慣希望 “被動” 的一方不要去持有 “主動” 的一方。在這裏 b.a 裏對 A 的實例的持有是由 A 的方法設定的,我們在之後直接使用的也是 A 的實例,因此認爲 b 是被動的一方。可以將上面的 class B 的聲明改爲:

class B {
weak var a: A? = nil
deinit {
println(“B deinit”)
}
}
在 var a 前面加上了 weak,向編譯器說明我們不希望持有 a。這時,當 obj 指向 nil 時,整個環境中就沒有對 A 的這個實例的持有了,於是這個實例可以得到釋放。接着,這個被釋放的實例上對 b 的引用 a.b 也隨着這次釋放結束了作用域,所以 b 的引用也將歸零,得到釋放。添加 weak 後的輸出:

A deinit
B deinit
可能有心的朋友已經注意到,在 Swift 中除了 weak 以外,還有另一個衝着編譯器叫喊着類似的 “不要引用我” 的標識符,那就是 unowned。它們的區別在哪裏呢?如果您是一直寫 Objective-C 過來的,那麼從表面的行爲上來說 unowned 更像以前的 unsafe_unretained,而 weak 就是以前的 weak。用通俗的話說,就是 unowned 設置以後即使它原來引用的內容已經被釋放了,它仍然會保持對被已經釋放了的對象的一個 “無效的” 引用,它不能是 Optional 值,也不會被指向 nil。如果你嘗試調用這個引用的方法或者訪問成員屬性的話,程序就會崩潰。而 weak 則友好一些,在引用的內容被釋放後,標記爲 weak 的成員將會自動地變成 nil (因此被標記爲 @weak 的變量一定需要是 Optional 值)。關於兩者使用的選擇,Apple 給我們的建議是如果能夠確定在訪問時不會已被釋放的話,儘量使用 unowned,如果存在被釋放的可能,那就選擇用 weak。

我們結合實際編碼中的使用來看看選擇吧。日常工作中一般使用弱引用的最常見的場景有兩個:

設置 delegate 時
在 self 屬性存儲爲閉包時,其中擁有對 self 引用時
前者是 Cocoa 框架的常見設計模式,比如我們有一個負責網絡請求的類,它實現了發送請求以及接收請求結果的任務,其中這個結果是通過實現請求類的 protocol 的方式來實現的,這種時候我們一般設置 delegate 爲 weak:

// RequestManager.swift
class RequestManager: RequestHandler {

func requestFinished() {
    println("請求完成")
}

func sendRequest() {
    let req = Request()
    req.delegate = self

    req.send()
}

}

// Request.swift
@objc protocol RequestHandler {
optional func requestFinished()
}

class Request {
weak var delegate: RequestHandler!;

func send() {
    // 發送請求
    // 一般來說會將 req 的引用傳遞給網絡框架
}

func gotResponse() {
    // 請求返回
    delegate?.requestFinished?()
}

}
req 中以 weak 的方式持有了 delegate,因爲網絡請求是一個異步過程,很可能會遇到用戶不願意等待而選擇放棄的情況。這種情況下一般都會將 RequestManager 進行清理,所以我們其實是無法保證在拿到返回時作爲 delegate 的 RequestManager 對象是一定存在的。因此我們使用了 weak 而非 unowned,並在調用前進行了判斷。

閉包和循環引用

另一種閉包的情況稍微複雜一些:我們首先要知道,閉包中對任何其他元素的引用都是會被閉包自動持有的。如果我們在閉包中寫了 self 這樣的東西的話,那我們其實也就在閉包內持有了當前的對象。這裏就出現了一個在實際開發中比較隱蔽的陷阱:如果當前的實例直接或者間接地對這個閉包又有引用的話,就形成了一個 self -> 閉包 -> self 的循環引用。最簡單的例子是,我們聲明瞭一個閉包用來以特定的形式打印 self 中的一個字符串:

class Person {
let name: String
lazy var printName: ()->() = {
println(“The name is (self.name)”)
}

init(personName: String) {
    name = personName
}

deinit {
    println("Person deinit \(self.name)")
}

}

func application(application: UIApplication!,
didFinishLaunchingWithOptions launchOptions: NSDictionary!)
-> Bool
{
// Override point for customization after application launch.
var xiaoMing: Person = Person(personName: “XiaoMing”)
xiaoMing.printName()

return true

}

// 輸出:
// The name is XiaoMing
printName 是 self 的屬性,會被 self 持有,而它本身又在閉包內持有 self,這導致了 xiaoMing 的 deinit 在自身超過作用域後還是沒有被調用,也就是沒有被釋放。爲了解決這種閉包內的循環引用,我們需要在閉包開始的時候添加一個標註,來表示這個閉包內的某些要素應該以何種特定的方式來使用。可以將 printName 修改爲這樣:

lazy var printName: ()->() = {
[weak self] in
if let strongSelf = self {
println(“The name is (strongSelf.name)”)
}
}
現在內存釋放就正確了:

// 輸出:
// The name is XiaoMing
// Person deinit XiaoMing
如果我們可以確定在整個過程中 self 不會被釋放的話,我們可以將上面的 weak 改爲 unowned,這樣就不再需要 strongSelf 的判斷。但是如果在過程中 self 被釋放了而 printName 這個閉包沒有被釋放的話 (比如 生成 Person 後,某個外部變量持有了 printName,隨後這個 Person 對象被釋放了,但是 printName 已然存在並可能被調用),使用 unowned 將造成崩潰。在這裏我們需要根據實際的需求來決定是使用 weak 還是 unowned。
這種在閉包參數的位置進行標註的語法結構是將要標註的內容放在原來參數的前面,並使用中括號括起來。如果有多個需要標註的元素的話,在同一個中括號內用逗號隔開,舉個例子:

// 標註前
{ (number: Int) -> Bool in
//…
return true
}

// 標註後
{ [unowned self, weak someObject] (number: Int) -> Bool in
//…
return true
}

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