背景:在使用定時器的時候,一不小心就會遇到循環引用問題,導致控制器不會被銷燬,定時事件也不會被終止。
錯誤代碼
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 self
,block
會對self
強引用。
b. 當使用target
時,NSTimer內部會對self
產生強引用(repeats: YES)
注意:當repeats爲NO時,不會產生循環引用
二、如果避免強引用問題?
a. 使用block
+ weak self
的方式,可以避免循環引用
b. 使用target
的時候,使用代理對象,代理對象裏面的target
屬性對self
弱引用,再利用消息轉發機制,將消息轉發給self去執行selector方法