循環(huán)引用(Circular Reference)是指兩個(gè)對(duì)象之間相互強(qiáng)引用螃诅,兩者無法按時(shí)釋放,從而導(dǎo)致內(nèi)存泄漏,是 iOS/macOS 開發(fā)人員經(jīng)常遇見的一種內(nèi)存管理問題揪胃。
這種問題的解決方式一般有兩種,一是對(duì)其中一個(gè)對(duì)象設(shè)置為弱引用氛琢,二是在其中一個(gè)對(duì)象需要釋放時(shí)喊递,強(qiáng)制將另一個(gè)對(duì)象置空,兩種方式的原理都是打破持有引用閉環(huán)阳似。
通常來講骚勘,這些操作不會(huì)出現(xiàn)什么問題,然而一些個(gè)別情況需要我們額外注意:兩個(gè)對(duì)象之間都必須強(qiáng)引用撮奏,并且需要在一個(gè)對(duì)象 delloc 時(shí)才能釋放另一個(gè)對(duì)象调鲸。
這里要特別指出一點(diǎn)盛杰,兩個(gè)對(duì)象必須互相強(qiáng)引用的情況會(huì)很少,大多數(shù)有經(jīng)驗(yàn)的開發(fā)者都會(huì)刻意避免這個(gè)情況發(fā)生藐石。但這并不是必須的即供,比如我們使用 NSTime 時(shí),或者某個(gè) view 必須強(qiáng)引用它的持有者時(shí)于微。
這其中 NSTimer 是個(gè)典型的例子逗嫡,我相信大多數(shù)人都遇見過使用 NSTimer 導(dǎo)致其持用對(duì)象無法釋放的問題,然后不得已在某個(gè)時(shí)機(jī)提前將 timer 置空釋放株依。我不太推薦這樣的做法驱证,因?yàn)殇N毀 timer 的邏輯可能分散在文件各處,難以維護(hù)恋腕,而且我們也可能忘記在特定事件中手動(dòng)銷毀 timer抹锄。另外假如這個(gè) timer 只能在持有者 dealloc/deinit 時(shí)釋放,你的處境會(huì)很尷尬:dealloc/deinit 不會(huì)被執(zhí)行荠藤。
timer = Timer.scheduledTimer(timeInterval: 1,
target: self,
selector: #selector(handleTimer),
userInfo: nil,
repeats: true)
RunLoop.current.add(timer, forMode: .common)
@objc func handleTimer() {
}
deinit {
timer.invalidate() // Not work.
}
這里我會(huì)介紹一種更友好的方式處理這個(gè)問題:代理模式伙单。
假設(shè) A 對(duì)象持有 B 對(duì)象,當(dāng) B 對(duì)象需要持有 A 對(duì)象時(shí)哈肖,我們退一步轉(zhuǎn)而讓其持有 C 對(duì)象(代理對(duì)象)吻育,C 對(duì)象中包含一個(gè)弱指針指向 A 對(duì)象地址,B 對(duì)象要給 A 對(duì)象發(fā)送的所有消息均由代理對(duì)象 C 轉(zhuǎn)發(fā)給 A淤井,這樣 A 與 B 之間的引用閉環(huán)會(huì)被打破布疼。這三者之間的關(guān)系如下:
A <- - - - - +
+ |
|
| | 弱引用指針
|
| |
| 持有
| |
|
v |
B+---------------->C
持有
在 Foundation 框架中有一個(gè)神奇的類 NSProxy 最適合扮演代理的角色。這是一個(gè)抽象類币狠,用來作為目標(biāo)對(duì)象的替身游两。只要實(shí)現(xiàn)了它的兩個(gè)方法,它本身接收到的所有消息均被轉(zhuǎn)發(fā)至目標(biāo)對(duì)象漩绵,太完美了器罐,遺憾的是 Swift 并不能使用,好在我們還可以使用 NSObject 的消息轉(zhuǎn)發(fā)機(jī)制去實(shí)現(xiàn)渐行。
現(xiàn)在轰坊,我們可以自己實(shí)現(xiàn)一個(gè) Proxy,取類名為 WeakProxy:
class WeakProxy: NSObject {
private(set) weak var target: AnyObject?
private override init() { super.init() }
convenience init(_ target: AnyObject?) {
self.init()
self.target = target
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
return target
}
}
上面的代碼中使用了一個(gè)小技巧來隱藏父類的構(gòu)造器祟印,我們只需使用其便利構(gòu)造器肴沫。
最后,只需要這樣一個(gè)小小的修改就可以解決 timer 的持有者無法釋放的問題蕴忆,我們將初始化 timer 時(shí)的 target 指定為這個(gè)代理對(duì)象颤芬,然后我們就可以在 dealloc/deinit 中釋放 timer 了。
timer = Timer.scheduledTimer(timeInterval: 1,
target: WeakProxy(self),
selector: #selector(handleTimer),
userInfo: nil,
repeats: true)
RunLoop.current.add(timer, forMode: .common)
@objc func handleTimer() {
}
deinit {
timer.invalidate() // Work well.
}
WeakProxy 并不僅僅局限于 timer 的使用場(chǎng)景,剩下的各位讀者可以自行發(fā)掘站蝠。