內(nèi)存管理在iOS開發(fā)中很重要鸦采,在iOS 5之前毙驯,開發(fā)者需要使用MRC(Manual Reference Count)來進(jìn)行對(duì)象的內(nèi)存管理竟坛;為了方便開發(fā)者,從iOS 6開始框冀,蘋果引入了ARC(Automatic Reference Count)來進(jìn)行內(nèi)存管理流椒,這無疑大大地減少了開發(fā)者的工作量。
ARC本質(zhì)還是MRC左驾,它是在Xcode編譯期間镣隶,在代碼適當(dāng)?shù)奈恢锰砑?code>retain ,release
或autorelease
操作极谊。在絕大多數(shù)時(shí)間诡右,我們可以使用ARC愉快地寫代碼,但是為了寫出更加安全和健壯的代碼轻猖,開發(fā)者還是需要了解內(nèi)存管理的知識(shí)帆吻,以防因?yàn)閮?nèi)存管理不當(dāng)導(dǎo)致莫名其妙的問題。
在swift編碼過程中咙边,大多數(shù)時(shí)候開發(fā)者過于相信ARC猜煮,一不小心還是可能踩到循環(huán)引用的坑里面。并且循環(huán)引用的問題往往會(huì)潛伏起來败许,在開發(fā)的初期王带,并不會(huì)導(dǎo)致異常,但是隨著業(yè)務(wù)復(fù)雜度增加市殷、參與的人增多愕撰,潛在的循環(huán)引用的問題就會(huì)出現(xiàn)。而解決循環(huán)引用導(dǎo)致的問題,也會(huì)消耗不少的人力和時(shí)間搞挣,回顧循環(huán)引用產(chǎn)生的原因带迟,一方面是開發(fā)者的經(jīng)驗(yàn)不足,甚至不知道循環(huán)引用的概念囱桨,所以寫不出高質(zhì)量的代碼仓犬;另一方面,有可能是開發(fā)過程中并沒有引入規(guī)范的編碼方式舍肠,一個(gè)人踩了坑搀继,找到了解決方案,并不能推廣給團(tuán)隊(duì)中的其他成員翠语,導(dǎo)致其他人前赴后繼地踩坑律歼。
本篇文章,主要討論了swift中解決循環(huán)引用的方案啡专,并結(jié)合一個(gè)案例险毁,幫助讀者加深理解。
1. Swift中的循環(huán)引用
在swift中们童,對(duì)象引用時(shí)默認(rèn)是強(qiáng)strong
引用畔况,如果兩個(gè)對(duì)象相互持有時(shí),則會(huì)造成循環(huán)引用慧库,為了解決這個(gè)問題跷跪,swift引入了weak
和unowned
兩個(gè)修飾關(guān)鍵字。
相對(duì)于強(qiáng)strong
引用齐板,weak
和unowned
則稱為弱引用和無主引用吵瞻,weak和unowned都不會(huì)對(duì)一個(gè)引用對(duì)象產(chǎn)生強(qiáng)引用,這在基本原理上來說是因?yàn)樗鼈兌疾粫?huì)增加對(duì)象的引用計(jì)數(shù)(retain count)甘磨。
按照我們OC時(shí)代的經(jīng)驗(yàn)橡羞,weak就可以解決循環(huán)引用的問題,開發(fā)者會(huì)感到奇怪济舆,swift有了weak卿泽,為什么swift還要引入unowned關(guān)鍵字?以及滋觉,unowned和weak有什么區(qū)別呢签夭?
1.1 weak, unowned產(chǎn)生背景
之所以有weak和unowned兩個(gè)關(guān)鍵字,則是與swift語(yǔ)言的可選(optional type)類型相關(guān)椎侠,關(guān)于可選類型的技術(shù)點(diǎn)不是本篇探討的重點(diǎn)第租,此處不再贅述。
如果我纪,我是說如果慎宾,如果swift語(yǔ)言沒有optional概念儡羔,那么使用weak足以解決循環(huán)引用的問題。現(xiàn)在璧诵,swift為了保證代碼安全性汰蜘,引入了optional概念。說到底之宿,optional是一個(gè)類型族操,它與Int, String或者諸如自定義的Person類一樣;但是optional類型特殊之處在于它可以用來包裹其他的類型比被,例如var person: Person?
, let number: Int?
就是用optional類型包裹了Person和Int類型色难。這樣說來,swift中定義屬性有兩種形式等缀,即optional和non-optional枷莉,僅僅一個(gè)weak關(guān)鍵字不足以同時(shí)解決optional和non-optional兩種情況,所以swift引入了另一個(gè)關(guān)鍵字unowned尺迂。
1.2 weak與optional相關(guān)
弱(weak)引用允許引用對(duì)象為空笤妙,并且我們知道,在swift中只有optional類型定義的變量或?qū)ο蟛趴梢栽O(shè)置為空噪裕,所以如果我們使用weak關(guān)鍵字定義屬性蹲盘,則必須保證該屬性是optional類型。
1.3 unowned與non-optional相關(guān)
使用unowned時(shí)膳音,我們會(huì)假定一個(gè)對(duì)象a的無主引unowned reference
用b在其持有者a的生命周期中永遠(yuǎn)不會(huì)被置為nil召衔,同時(shí)必須保證一個(gè)無主引用對(duì)象unowned reference
在它的初始化方法中就被賦值,這也意味著該無主引用對(duì)象應(yīng)該定義為non-optional類型祭陷,這樣我們就可以安全使用該對(duì)象苍凛,而不必須進(jìn)行安全檢查。
如果因?yàn)槟承┰虮荆粋€(gè)無主引用對(duì)象從內(nèi)存中被釋放醇蝴,那么當(dāng)我們使用該對(duì)象的時(shí)候,App就會(huì)閃退毒姨。
1.4 參考Apple官方的建議
上面的內(nèi)容對(duì)于讀者來說哑蔫,可能還是感覺含糊不清,我們來看一下Apple文檔中的建議吧弧呐,概括起來大概就是下面兩條原則,
- 當(dāng)一個(gè)對(duì)象有可能在它的生命周期內(nèi)被設(shè)置為nil時(shí)候嵌纲,使用weak關(guān)鍵字俘枫;
- 當(dāng)你可以確保一個(gè)對(duì)象在它的生命周期內(nèi)不會(huì)被設(shè)置為nil時(shí),使用unowned關(guān)鍵字逮走。
同時(shí)鸠蚪,文檔中也提供了兩個(gè)demo,探討了循環(huán)引用的案例和打破循環(huán)引用的方法,
先看一下使用weak的demo茅信,如下代碼所示盾舌,
// weak關(guān)鍵字例子
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
}
class Apartment {
let number: Int
init(number: Int) { self.number = number }
weak var person: Person? // 承租人、占用者
}
這個(gè)demo是一個(gè)人和公寓
的簡(jiǎn)單模型蘸鲸,Person類有一個(gè)Apartment類型的optional屬性apartment妖谴,同時(shí),Apartment類有一個(gè)Person類型的person(承租人)屬性酌摇,該屬性也是optional類型膝舅。
Optional在此處表明的意思就是Person對(duì)象可能會(huì)持有apartment屬性,也可能不會(huì)窑多;Apartment對(duì)象可能會(huì)仍稀、也可能不會(huì)持有person屬性。這會(huì)造成一個(gè)問題埂息,如果Person和Apartment雙方都持有了對(duì)方技潘,就形成了循環(huán)引用,為了打破循環(huán)引用千康,將Apartment類的Person類型的person屬性改為weak修飾崭篡,經(jīng)過這樣修改之后,兩者的關(guān)系如下圖所示 吧秕,
這是一個(gè)很好的demo琉闪,展示了使用weak關(guān)鍵字的場(chǎng)景。使用weak關(guān)鍵字打破了循環(huán)引用之后砸彬,兩個(gè)對(duì)象之間不會(huì)形成緊密的依賴關(guān)系颠毙,能夠在適當(dāng)?shù)臅r(shí)機(jī)從內(nèi)存中釋放。
再看一下使用unowned的demo砂碉,如下代碼蛀蜜,
// unowned關(guān)鍵字例子
class Customer {
let name: String
var card: CreditCard?
init(name: String) { self.name = name }
}
class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
}
在這個(gè)demo中,是消費(fèi)者和信用卡
的模型增蹭,一個(gè)消費(fèi)者可能會(huì)擁有滴某、也可能沒有一張信用卡;而一張信用卡必定有一個(gè)關(guān)聯(lián)的消費(fèi)者滋迈。為了用代碼表示兩者之間的關(guān)系霎奢,一個(gè)Customer類有一個(gè)optional類型的creditCard屬性,而一個(gè)CreditCard類型有一個(gè)non-optional類型饼灿、unowned修飾的customer屬性幕侠。他們之間的關(guān)系如下圖所示,
2. 閉包引起的循環(huán)引用
當(dāng)然除了兩個(gè)對(duì)象相互持有造成循環(huán)引用碍彭,還有一種情況也會(huì)造成循環(huán)引用晤硕,那就是閉包(在swift中叫閉包closure
悼潭,在OC中叫代碼塊block
),并且使用閉包或代碼塊的場(chǎng)景更多舞箍,更容易因?yàn)榫幋a疏忽或不規(guī)范造成潛在的循環(huán)引用舰褪。筆者在開發(fā)過程中就碰到了這種神坑,下面筆者就回顧一下解決bug的過程疏橄,希望能夠拋磚引玉占拍,也希望讀者在開發(fā)過程中能避開這類錯(cuò)誤。
2.1 Bug起因
最近的開發(fā)周软族,同事M在接手另一個(gè)同事K的工作任務(wù)刷喜,他在開發(fā)過程中遇到一個(gè)很奇怪的現(xiàn)象,簡(jiǎn)單描述一下立砸,在HomeVC頁(yè)面上是一些品牌信息列表掖疮,用戶可以點(diǎn)擊進(jìn)入詳情,也可以點(diǎn)擊關(guān)注
按鈕直接關(guān)注颗祝;還可以點(diǎn)擊導(dǎo)航欄更多品牌
按鈕浊闪,跳轉(zhuǎn)到MoreBrandListVC,這個(gè)列表顯示更多的品牌信息螺戳。如下圖所示搁宾,
按照設(shè)計(jì)和編碼原則,HomeVC的品牌關(guān)注狀態(tài)應(yīng)該與MoreBrandListVC對(duì)應(yīng)的品牌關(guān)注狀態(tài)保持一致倔幼,但測(cè)試同事提的比較奇怪的問題就是在HomeVC和MoreBrandListVC之間反復(fù)
切換盖腿,并且多次重復(fù)點(diǎn)擊關(guān)注某個(gè)品牌以后,例如關(guān)注了“品牌3”损同,點(diǎn)擊“返回”pop回到HomeVC時(shí)候翩腐,下拉刷新,發(fā)現(xiàn)在HomeVC頁(yè)面膏燃,“品牌3”的關(guān)注狀態(tài)是未關(guān)注茂卦,與MoreBrandListVC關(guān)注狀態(tài)不一致。
剛開始分析該問題時(shí)候组哩,以為是客戶端多次操作等龙,發(fā)送請(qǐng)求太多,服務(wù)端處理不及時(shí)或者服務(wù)端緩存造成的返回結(jié)果不一致伶贰,準(zhǔn)備強(qiáng)行甩鍋給服務(wù)端開發(fā)人員蛛砰。后來服務(wù)端同事說他沒有做緩存,而且android是OK的幕袱,這就尷尬了暴备。
2.2 多人協(xié)作定位bug
既然服務(wù)端同事來幫分析問題了,咱們就把他叫過來一塊看看Xcode的打印日志们豌,按照測(cè)試的操作步驟涯捻,iOS同事M不久就重現(xiàn)了測(cè)試指出的bug,在Xcode的console控制臺(tái)中看了看服務(wù)端返回的數(shù)據(jù)望迎,跟客戶端顯示的是一致的障癌。服務(wù)端同事也有點(diǎn)啞口無言,但是他突然看出了異常的情況辩尊,就是在Xcode的控制臺(tái)有很多發(fā)送“某品牌關(guān)注狀態(tài)更新的請(qǐng)求”的日志涛浙,按照道理,無論是在HomeVC點(diǎn)擊關(guān)注或下拉刷新摄欲,亦或是在MoreBrandListVC點(diǎn)擊關(guān)注品牌轿亮,操作再?gòu)?fù)雜,最多也就十幾條請(qǐng)求吧胸墙,但是我們看到的實(shí)際情況是Xcode打印了四五十條的請(qǐng)求日志我注。這一點(diǎn)很讓人費(fèi)解,我們定位了一下發(fā)起請(qǐng)求的地方迟隅,原來是在MoreBrandListVC里面但骨,更具體地來說,就是MoreBrandListVC觀察了某個(gè)notification智袭,收到notification之后進(jìn)行了網(wǎng)絡(luò)請(qǐng)求奔缠。
問題應(yīng)該就是出現(xiàn)在MoreBrandListVC上面,在回到HomeVC之后吼野,iOS系統(tǒng)會(huì)在合適的時(shí)機(jī)將MoreBrandListVC對(duì)象從內(nèi)存中釋放校哎;而現(xiàn)在出現(xiàn)的問題是,在HomeVC頁(yè)面瞳步,MoreBrandListVC對(duì)象內(nèi)部還在接收到notification之后發(fā)送網(wǎng)絡(luò)請(qǐng)求闷哆。這說明MoreBrandListVC對(duì)象的內(nèi)存并沒有被合理的釋放,而是一直存在于內(nèi)存中谚攒,造成MoreBrandListVC不能釋放的原因極有可能是循環(huán)引用阳准。
請(qǐng)讀者再看一下測(cè)試重現(xiàn)bug的操作,反復(fù)
在HomeVC和MoreBrandListVC之間切換馏臭,每一次從HomeVC頁(yè)面push到MoreBrandListVC頁(yè)面野蝇,就創(chuàng)建了一個(gè)MoreBrandListVC對(duì)象,而因?yàn)槟巢糠执a原因括儒,導(dǎo)致MoreBrandListVC對(duì)象與另一個(gè)對(duì)象形成了循環(huán)引用绕沈。這樣,內(nèi)存中就存在了多個(gè)MoreBrandListVC對(duì)象帮寻,這多個(gè)對(duì)象再接收到notification之后乍狐,就開始進(jìn)行網(wǎng)絡(luò)請(qǐng)求,這就是Xcode日志中看到四五十條網(wǎng)絡(luò)請(qǐng)求的原因固逗。
找到這樣一個(gè)bug浅蚪,也是一個(gè)挺艱難和復(fù)雜的過程藕帜,簡(jiǎn)單描述一下,重現(xiàn)并定位該bug惜傲,大概是這樣幾個(gè)步驟洽故,
- 測(cè)試部門同事反復(fù)、多次的非常規(guī)操作盗誊,以及他堅(jiān)持不懈追究到底的決心时甚;
- 服務(wù)端和客戶端開發(fā)一起看Xcode打印日志,并由服務(wù)端同事發(fā)現(xiàn)過多請(qǐng)求的異常哈踱;
- 客戶端同事分析現(xiàn)象荒适,定位bug原因是循環(huán)引用。
以上开镣,只是簡(jiǎn)單概括找出bug的過程刀诬,實(shí)際上中間的坑更多。最麻煩的一點(diǎn)大概就是這一塊的bug是之前的版本就已經(jīng)存在了哑子,只是當(dāng)時(shí)沒有測(cè)試出來舅列,而現(xiàn)在不知道隔了多久,萬年老坑被挖出來卧蜓,已經(jīng)不知道當(dāng)時(shí)這塊代碼的維護(hù)者是哪個(gè)人帐要。可以說弥奸,bug時(shí)間越久榨惠,就越難排查。
2.3 逐步排查盛霎,精確打擊
在分析可能造成循環(huán)引用的地方赠橙,我們進(jìn)行了一一排查,大體思路是這樣的愤炸,
- 首先期揪,MoreBrandListVC的對(duì)象創(chuàng)建是在HomeVC內(nèi),那么MoreBrandListVC對(duì)象有沒有反向的持有HomeVC呢规个,仔細(xì)看了看代碼凤薛,并沒有這種情況;
- 其次诞仓,有沒有可能是MoreBrandListVC內(nèi)部與其他block相互持有造成了循環(huán)引用呢缤苫。我在MoreBrandListVC文件里面搜索了
block
關(guān)鍵字,搜索到了4個(gè)墅拭,其中有兩個(gè)是網(wǎng)絡(luò)請(qǐng)求的block回調(diào)活玲;還有兩個(gè)是頁(yè)面CollectionViewCell關(guān)注按鈕點(diǎn)擊的事件回調(diào)。
排除了第一種情況,那么著重分析第二種情況舒憾,最終我們排查出問題在于CollectionViewCell按鈕點(diǎn)擊的事件回調(diào)與MoreBrandListVC對(duì)象造成了循環(huán)引用镀钓,下面的代碼,是因?yàn)殚]包導(dǎo)致了循環(huán)引用珍剑,讀者可以回顧一下掸宛,自己是否寫過這樣的代碼死陆,
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(kCellReuseIdentify, forIndexPath: indexPath) as? CustomCell
// cell?.focusButtonClickBlock = xxx會(huì)導(dǎo)致循環(huán)引用
cell?.focusButtonClickBlock = { (model) -> Void in
self.sendRequest(withModel: model)
}
return cell!
}
上面的代碼招拙,第一眼看起來沒有什么問題,但是正如注釋所言措译,cell?.focusButtonClickBlock = xxx
會(huì)導(dǎo)致循環(huán)引用别凤,這段代碼中有這樣幾個(gè)角色,self
, cell
, focusButtonClickBlock
领虹,self代表當(dāng)前的控制器规哪,cell是自定義的UICollectionViewCell,focusButtonClickBlock代表cell上面按鈕點(diǎn)擊的事件回調(diào)塌衰,這樣劃分了角色诉稍,就能弄清3者相互持有的過程,大體是這樣的最疆,
- self持有了cell杯巨,
- cell持有了focusButtonClickBlock,
- focusButtonClickBlock內(nèi)部持有了self努酸。
3者之間的關(guān)系服爷,如下圖所示,
這樣获诈,3者之間就形成了循環(huán)引用仍源,接下來的事情就是打破這個(gè)循環(huán)引用,使用unowned來打破循環(huán)引用舔涎,bug也迎刃而解笼踩,如下代碼所示,
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(kCellReuseIdentify, forIndexPath: indexPath) as? CustomCell
// 這里添加了[unowned self]
cell?.focusButtonClickBlock = { [unowned self] (model) -> Void in
self.sendRequest(withModel: model)
}
return cell!
}
在focusButtonClickBlock內(nèi)部定義[unowned self]
打破了三者之間的循環(huán)引用亡嫌,因?yàn)榇藭r(shí)閉包持有的self是unowned修飾嚎于,它表明閉包并不對(duì)self是強(qiáng)引用,而是無主引用昼伴,這樣當(dāng)self即ViewController通過點(diǎn)擊返回按鈕匾旭,在NavigationController的堆棧中出棧時(shí),就不會(huì)因?yàn)檠h(huán)引用導(dǎo)致無法從內(nèi)存中釋放圃郊。打破循環(huán)引用之后价涝,三者之間的關(guān)系如下圖所示,
3. 結(jié)尾的釋疑
這里使用了unowned解決了循環(huán)引用持舆,那么為什么不使用weak呢色瘩?其實(shí)也可以使用weak來解決問題伪窖,兩者區(qū)別就是self是否可能為nil,當(dāng)我們點(diǎn)擊cell上面的按鈕時(shí)候可以確保self是存在的居兆,而不是nil覆山,所以使用unowned不會(huì)有什么問題。
但是泥栖,如果在一個(gè)網(wǎng)絡(luò)請(qǐng)求的回調(diào)里面使用unowned簇宽,那么有可能會(huì)導(dǎo)致crash,因?yàn)橛脩粲锌赡茉诰W(wǎng)絡(luò)請(qǐng)求的過程中等的不耐煩吧享,直接從當(dāng)前ViewController頁(yè)面退回前一個(gè)頁(yè)面魏割,這時(shí)候self就是nil,使用unowned導(dǎo)致了崩潰钢颂;而使用weak則不會(huì)有這種問題钞它,因?yàn)閣eak允許被修飾的對(duì)象為nil。
這么說來[weak self]相當(dāng)于self?殊鞭,是可選解包遭垛;而[unowned self]相當(dāng)于self!,是隱式強(qiáng)制解包操灿。
差不多就是這樣吧锯仪。
參考鏈接
- http://stackoverflow.com/questions/24011575/what-is-the-difference-between-a-weak-reference-and-an-unowned-reference
- https://realm.io/news/hector-matos-memory-management/
- http://swifter.tips/retain-cycle/
公眾號(hào)
微信掃描下方圖片,歡迎關(guān)注本人公眾號(hào)foolishlion牲尺,咱們來談技術(shù)談人生卵酪,因?yàn)檫@又不要錢,