NSTimer 循環引用造成的 retain cycle

NSTimer 的 retain cycle 的問題

有時我們常常需要在 Controller 中建立一個定時被呼叫的函數,所以會使用到 NSTimer,NSTimer 很容易產生 retain cycle 的狀況,下列一個是很常見的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ViewController: UIViewController {

var timer : Timer?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(fire), userInfo: nil, repeats: false)
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

@objc func fire() {
//Do some thing
}

deinit {
timer?.invalidate()
timer = nil;
}
}

發生了什麼

由於 timer 建立時引用了 ViewController ,而 timer 本身又是 ViewController 的成員變數,所以這樣就造成了 retain cycle ,就算我們在 deinit 解構的時候讓 timer invalidate 跟讓 timer assignnil ,但是由於已經產生了 retain cycle ,所以 ViewControllerdeinit 完全不會被呼叫到。

解決方法?

或許我們可以先想到,如果我們使用 weak selftarget呢 ? 答案依舊相同,可以參考一下官方文檔是這樣寫的:

The timer maintains a strong reference to target until it (the timer) is invalidated.

timer 被建立時的引用永遠是 strong 的,所以這個方法行不通。

那如果我們將 timer 設成 weak 屬性呢? 一樣也是行不通的,參考官方文檔:

Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.

雖然 timer 不會被 ViewController 引用了,但是 self target 的問題一樣沒有被解決,然後 timer 也會被加到 run loop 中,所以依舊不會被釋放。

iOS 10 Closure 的解決方法

在 iOS10 中,提供了有 Closure 的建立方法 :

1
2
3
class func scheduledTimer(withTimeInterval interval: TimeInterval, 
repeats: Bool,
block: @escaping (Timer) -> Void) -> Timer

所以利用這種方法,我們可以改寫成

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 ViewController: UIViewController {

var timer : Timer?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] timer in
self?.fire()
})
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

func fire() {
//Do some thing
}

deinit {
timer?.invalidate()
timer = nil;
}
}

使用 Closure 的寫法,就可以避免 retain cycle 了,那如果不是 iOS10呢?

可以加入一個中介層(proxy),或者自己實做跟 iOS10 api 相同的事情,可以參照這幾個連結的解法 :

https://gist.github.com/onevcat/2d1ceff1c657591eebde

http://www.jianshu.com/p/4cfae008bff5

參考資料:

http://nelson.logdown.com/posts/2017/04/05/how-to-fix-nstimer-retain-cycle