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方法

你可能感兴趣的:(NSTimer / CADisplayLink循环引用问题分析)