2021-12-15

iOS开发:CADisp...


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

一般情况ViewController的deinit

比如在主ViewController里push进VC2

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class VC2: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemCyan
}

override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
}

deinit {
print("VC2 deinit")
}
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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”一直继续打印,说明循环引用了
你可能会想用弱引用解决

1
2
3
4
5
6
7
8
9
10
11
12
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掉的最后最后一步,这样提高效率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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是刚刚写的代理类

1
2
3
4
5
6
7
8
9
10
11
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绑定的。

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