使用 Proxy 解決對(duì)象之間循環(huán)引用

循環(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ā)掘。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,674評(píng)論 1 32
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 30,262評(píng)論 8 265
  • # 前言 反復(fù)地復(fù)習(xí)iOS基礎(chǔ)知識(shí)和原理,打磨知識(shí)體系是非常重要的,本篇就是重新溫習(xí)iOS的內(nèi)存管理。 內(nèi)存管理是...
    Vein_閱讀 885評(píng)論 0 2
  • 《極限挑戰(zhàn)》第四季第一期的主題是知識(shí)改變命運(yùn),第二場(chǎng)在崇明中學(xué)的六個(gè)問題令我震撼不已。 首先,附上這六個(gè)問題: 1...
    梁景辰閱讀 597評(píng)論 0 3
  • 最近有很多關(guān)于吉芬商品的問題,討論多的是關(guān)于股票到底是不是吉芬商品。 我們先來回顧一下吉芬商品的定義:是指某種生活...
    果大喵喵閱讀 3,525評(píng)論 0 0

友情鏈接更多精彩內(nèi)容