iOS开发:CADisplayLink的循环引用问题

925 阅读2分钟

讨论weak self不能解决的循环引用

一般情况ViewController的deinit

比如在主ViewController里push进VC2

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
    
    @IBAction func pushBtn(_ sender: Any) {
        let vc = VC2()
        navigationController?.pushViewController(vc, animated: true)
    }
}

VC2里简单看下deninit

class VC2: UIViewController {
  override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemCyan
    }
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
    }

    deinit {
        print("VC2 deinit")
    }
}

当从VC2返回navigationController时可以看到"VC2 deinit"打印了

加入CADisplayLink

在VC2里加入CADisplayLink,我们知道CADisplayLink跟着屏幕刷新率走,可以搞一些Core Animation事情

class VC2: UIViewController {
    
    var displayLink: CADisplayLink?
    

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemCyan
    }
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        displayLink = CADisplayLink(target: self, selector: #selector(displayLinkSel))
        displayLink?.frameInterval = 1
        displayLink?.add(to: RunLoop.current, forMode: .default)
    }
    
    @objc func displayLinkSel() {
        print("displayLinkSel")
    }
    
    deinit {
        print("VC2 deinit")
    }
}

这里运行看到每一frame刷新都打印了"displayLinkSel",但是退出时候"VC2 deinit"没有打印,而且"displayLinkSel"一直继续打印,说明循环引用了 你可能会想用弱引用解决

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    weak var weakSelf = self
    displayLink = CADisplayLink(target: weakSelf, selector: #selector(displayLinkSel))
    displayLink?.frameInterval = 1
    displayLink?.add(to: RunLoop.current, forMode: .default)
}
// 或者让它nil
deinit {
    displayLink = nil
    print("VC2 deinit")
}

结果还是一样,没有deinit触发,想验证的话可以lldb看下vc2

解决

写一个代理,这里代理直接让他走消息转发机制,类似objc_mesgSend,我们知道iOS里回去class对象和meta-class对象的方法区找,直接转发就是当objc_mesgSend找不到fail掉的最后最后一步,这样提高效率

class MyWeakProxy: NSObject {
    weak var target: NSObjectProtocol?

    init(target: NSObjectProtocol) {
        self.target = target
        super.init()
    }
    
    override func responds(to aSelector: Selector!) -> Bool {
        return (target?.responds(to: aSelector) ?? false) || super.responds(to: aSelector)
    }

    override func forwardingTarget(for aSelector: Selector!) -> Any? {
        return target
    }
}

然后,我们让target是刚刚写的代理类

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    displayLink = CADisplayLink(target: MyWeakProxy(target: self), selector: #selector(displayLinkSel))
    displayLink?.frameInterval = 1
    displayLink?.add(to: RunLoop.current, forMode: .default)
}
// 记得让displayLink暂停掉,不然还继续走#selector(displayLinkSel)就会经典的找不到方法runtime错误了。
deinit {
    displayLink?.isPaused = true
    print("VC2 deinit")
}

最后解释下为什么weak self不起作用,原因不复杂,就是传入时候strong引用了,相当于weakSelf!,这样设计也有道理,因为这玩意和runloop绑定的。

weak var weakSelf = self
displayLink = CADisplayLink(target: weakSelf!, selector: #selector(displayLinkSel))