NSTimer / CADisplayLink循環引用問題分析

背景:在使用定時器的時候,一不小心就會遇到循環引用問題,導致控制器不會被銷燬,定時事件也不會被終止。

錯誤代碼

class ViewController: UIViewController {
    var displayLink: CADisplayLink?
    // var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(test), userInfo: nil, repeats: true)
        displayLink = CADisplayLink(target: self, selector: #selector(test))
        displayLink?.add(to: .current, forMode: .default)
    }
    
    @objc func test() {
        print("\(#function)")
    }

    deinit {
        print("\(#function)")
        displayLink?.invalidate()
        // timer?.invalidate();
    }
}

分析

如圖,控制器Vc強引用displayLink對象,CADisplayLink對象內部的target強引用着Vc,循環引用,誰也別想銷燬,導致內存泄漏。

解決辦法

target與控制器之間弱引用

因CADisplayLink是系統的類,無法改變target的引用方式,所以可以新建一箇中間類,中轉target,解決循環引用問題。

控制器將要被釋放的時候,發現沒有被強引用,控制器被銷燬,同時定時器也被銷燬,Proxy也被銷燬,沒有內存泄漏。

Proxy示例代碼如下

class Proxy: NSObject {
    // 弱指針
    weak var target: NSObject?
    
    class func with(target: NSObject) -> Proxy {
        let proxy = Proxy()
        proxy.target = target
        return proxy;
    }
    
    /// 重點:消息轉發機制
    override func forwardingTarget(for aSelector: Selector!) -> Any? {
        return target
    }
}

Controller變更代碼如下

// target的變化
displayLink = CADisplayLink(target: Proxy.with(target: self), selector: #selector(test))
// timer = Timer.scheduledTimer(timeInterval: 1, target: Proxy.with(target: self), selector: #selector(test), userInfo: nil, repeats: true)

總結

一、爲什麼會產生循環引用?

a. 當使用block時,沒有使用weak selfblock會對self強引用。
b. 當使用target時,NSTimer內部會對self產生強引用(repeats: YES)
注意:當repeats爲NO時,不會產生循環引用

二、如果避免強引用問題?

a. 使用block + weak self的方式,可以避免循環引用
b. 使用target的時候,使用代理對象,代理對象裏面的target屬性對self弱引用,再利用消息轉發機制,將消息轉發給self去執行selector方法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章