因?yàn)?Playground 本身會持有所有聲明在其中的東西,因此本節(jié)中的示例代碼需要在 Xcode 項(xiàng)目環(huán)境中運(yùn)行。在 Playground 中可能無法得到正確的結(jié)果。
不管在什么語言里,內(nèi)存管理的內(nèi)容都很重要,所以我打算花上比其他 tip 長一些的篇幅仔細(xì)地說說這塊內(nèi)容。
Swift 是自動管理內(nèi)存的,這也就是說,我們不再需要操心內(nèi)存的申請和分配。當(dāng)我們通過初始化創(chuàng)建一個對象時,Swift 會替我們管理和分配內(nèi)存。而釋放的原則遵循了自動引用計(jì)數(shù) (ARC) 的規(guī)則:當(dāng)一個對象沒有引用的時候,其內(nèi)存將會被自動回收。這套機(jī)制從很大程度上簡化了我們的編碼,我們只需要保證在合適的時候?qū)⒁弥每?(比如超過作用域,或者手動設(shè)為 nil
等),就可以確保內(nèi)存使用不出現(xiàn)問題。
但是,所有的自動引用計(jì)數(shù)機(jī)制都有一個從理論上無法繞過的限制,那就是循環(huán)引用 (retain cycle) 的情況。
雖然我覺得循環(huán)引用這樣的概念介紹不太應(yīng)該出現(xiàn)在這本書中,但是為了更清晰地解釋 Swift 中的循環(huán)引用的一般情況,這里還是簡單進(jìn)行說明。假設(shè)我們有兩個類 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
的實(shí)例并將其存儲在屬性中。然后我們又將A
的實(shí)例賦值給了b.a
。這樣a.b
和b.a
將在初始化的時候形成一個引用循環(huán)?,F(xiàn)在當(dāng)有第三方的調(diào)用初始化了A
,然后即使立即將其釋放,A
和B
兩個類實(shí)例的deinit
方法也不會被調(diào)用,說明它們并沒有被釋放。func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool { // Override point for customization after application launch. var obj: A? = A() obj = nil // 內(nèi)存沒有釋放 return true}
因?yàn)榧词?nbsp;
obj
不再持有A
的這個對象,b 中的b.a
依然引用著這個對象,導(dǎo)致它無法釋放。而進(jìn)一步,a 中也持有著 b,導(dǎo)致 b 也無法釋放。在將obj
設(shè)為nil
之后,我們在代碼里再也拿不到對于這個對象的引用了,所以除非是殺掉整個進(jìn)程,我們已經(jīng) 永遠(yuǎn) 也無法將它釋放了。多么悲傷的故事啊..在 Swift 里防止循環(huán)引用
為了防止這種人神共憤的悲劇的發(fā)生,我們必須給編譯器一點(diǎn)提示,表明我們不希望它們互相持有。一般來說我們習(xí)慣希望 "被動" 的一方不要去持有 "主動" 的一方。在這里 b.a 里對 A 的實(shí)例的持有是由 A 的方法設(shè)定的,我們在之后直接使用的也是 A 的實(shí)例,因此認(rèn)為 b 是被動的一方??梢詫⑸厦娴?nbsp;
class B
的聲明改為:class B { weak var a: A? = nil deinit { println("B deinit") }}
在
var a
前面加上了weak
,向編譯器說明我們不希望持有 a。這時,當(dāng)obj
指向nil
時,整個環(huán)境中就沒有對A
的這個實(shí)例的持有了,于是這個實(shí)例可以得到釋放。接著,這個被釋放的實(shí)例上對 b 的引用a.b
也隨著這次釋放結(jié)束了作用域,所以b
的引用也將歸零,得到釋放。添加weak
后的輸出:A deinitB deinit
可能有心的朋友已經(jīng)注意到,在 Swift 中除了
weak
以外,還有另一個沖著編譯器叫喊著類似的 "不要引用我" 的標(biāo)識符,那就是unowned
。它們的區(qū)別在哪里呢?如果您是一直寫 Objective-C 過來的,那么從表面的行為上來說unowned
更像以前的unsafe_unretained
,而weak
就是以前的weak
。用通俗的話說,就是unowned
設(shè)置以后即使它原來引用的內(nèi)容已經(jīng)被釋放了,它仍然會保持對被已經(jīng)釋放了的對象的一個 "無效的" 引用,它不能是 Optional 值,也不會被指向nil
。如果你嘗試調(diào)用這個引用的方法或者訪問成員屬性的話,程序就會崩潰。而weak
則友好一些,在引用的內(nèi)容被釋放后,標(biāo)記為weak
的成員將會自動地變成nil
(因此被標(biāo)記為 @weak
的變量一定需要是 Optional 值)。關(guān)于兩者使用的選擇,Apple 給我們的建議是如果能夠確定在訪問時不會已被釋放的話,盡量使用unowned
,如果存在被釋放的可能,那就選擇用weak
。我們結(jié)合實(shí)際編碼中的使用來看看選擇吧。日常工作中一般使用弱引用的最常見的場景有兩個:
設(shè)置delegate
時在self
屬性存儲為閉包時,其中擁有對self
引用時前者是 Cocoa 框架的常見設(shè)計(jì)模式,比如我們有一個負(fù)責(zé)網(wǎng)絡(luò)請求的類,它實(shí)現(xiàn)了發(fā)送請求以及接收請求結(jié)果的任務(wù),其中這個結(jié)果是通過實(shí)現(xiàn)請求類的 protocol 的方式來實(shí)現(xiàn)的,這種時候我們一般設(shè)置
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() { // 發(fā)送請求 // 一般來說會將 req 的引用傳遞給網(wǎng)絡(luò)框架 } func gotResponse() { // 請求返回 delegate?.requestFinished?() }}
req
中以weak
的方式持有了 delegate,因?yàn)榫W(wǎng)絡(luò)請求是一個異步過程,很可能會遇到用戶不愿意等待而選擇放棄的情況。這種情況下一般都會將RequestManager
進(jìn)行清理,所以我們其實(shí)是無法保證在拿到返回時作為delegate
的RequestManager
對象是一定存在的。因此我們使用了weak
而非unowned
,并在調(diào)用前進(jìn)行了判斷。閉包和循環(huán)引用
另一種閉包的情況稍微復(fù)雜一些:我們首先要知道,閉包中對任何其他元素的引用都是會被閉包自動持有的。如果我們在閉包中寫了
self
這樣的東西的話,那我們其實(shí)也就在閉包內(nèi)持有了當(dāng)前的對象。這里就出現(xiàn)了一個在實(shí)際開發(fā)中比較隱蔽的陷阱:如果當(dāng)前的實(shí)例直接或者間接地對這個閉包又有引用的話,就形成了一個 self -> 閉包 -> self 的循環(huán)引用。最簡單的例子是,我們聲明了一個閉包用來以特定的形式打印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
持有,而它本身又在閉包內(nèi)持有self
,這導(dǎo)致了xiaoMing
的deinit
在自身超過作用域后還是沒有被調(diào)用,也就是沒有被釋放。為了解決這種閉包內(nèi)的循環(huán)引用,我們需要在閉包開始的時候添加一個標(biāo)注,來表示這個閉包內(nèi)的某些要素應(yīng)該以何種特定的方式來使用??梢詫?nbsp;printName
修改為這樣:lazy var printName: ()->() = { [weak self] in if let strongSelf = self { println("The name is /(strongSelf.name)") }}
現(xiàn)在內(nèi)存釋放就正確了:
// 輸出:// The name is XiaoMing// Person deinit XiaoMing
如果我們可以確定在整個過程中
self
不會被釋放的話,我們可以將上面的weak
改為unowned
,這樣就不再需要strongSelf
的判斷。但是如果在過程中self
被釋放了而printName
這個閉包沒有被釋放的話 (比如 生成Person
后,某個外部變量持有了printName
,隨后這個Person
對象被釋放了,但是printName
已然存在并可能被調(diào)用),使用unowned
將造成崩潰。在這里我們需要根據(jù)實(shí)際的需求來決定是使用weak
還是unowned
。這種在閉包參數(shù)的位置進(jìn)行標(biāo)注的語法結(jié)構(gòu)是將要標(biāo)注的內(nèi)容放在原來參數(shù)的前面,并使用中括號括起來。如果有多個需要標(biāo)注的元素的話,在同一個中括號內(nèi)用逗號隔開,舉個例子:
// 標(biāo)注前{ (number: Int) -> Bool in //... return true}// 標(biāo)注后{ [unowned self, weak someObject] (number: Int) -> Bool in //... return true}
新聞熱點(diǎn)
疑難解答