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