因為 Playground 本身會持有所有聲明在其中的東西,因此本節中的示例代碼需要在 Xcode 項目環境中運行。在 Playground 中可能無法得到正確的結果。
不管在什么語言里,內存管理的內容都很重要,所以我打算花上比其他 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
將在初始化的時候形成一個引用循環?,F在當有第三方的調用初始化了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 是被動的一方??梢詫⑸厦娴?nbsp;
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 deinitB 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.swiftclass 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}
新聞熱點
疑難解答