タイマーのメモリ解放を試みる
利用する機会も多いと思われるタイマーですが、こちらのメモリ解放が適切にできていないと処理がかなり重くなったり予期せぬエラー(EXC_BAD_ACCESS)などにハマる可能性があります。僕自身もタイマーが適切に扱えていないがためにそういったことにハマった事があったので簡単な例を用いて説明します。
(※以下のコードはSwift3で検証していますが,Swift4,5でも同様です)
タイマーを停止する
timer.invalidate()
たかが一行、されど一行。タイマーを停止するときに使うのはinvalidate()ですよね。
実はinvalidate()はタイマーを停止すると同時に使用していたタイマーを破棄しています。(timer=nilをしているイメージ)
タイマーの停止処理というよりかはタイマーの破棄のために使用すると考えるとメモリ解放につながるのも頷けます。
悪い例
override func viewDidLoad() {
super.viewDidLoad()
// 0.5秒ごとにstartTimer()を呼ぶタイマー
var timer: Timer!
timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(self.startTimer), userInfo: nil, repeats: true)
}
/// 経過時間をカウントして表示
/// 0.5秒ごとに呼び出される
@objc func startTimer() {
count+=1
label.text = String(count)
}
タイマーをscheduledTimerで呼び出す際にrepeatsの引数をtrueと設定すると、停止処理がない限りタイマーはループし続けてしまいます。そうすると上記の例では、NextViewControllerを閉じようとしてもタイマーは動き続けているため、NextViewControllerのメモリは解放されません。
解決策
解決策としてはどこかにタイマーの停止処理を入れる必要があります。
タイマーが使用し終わった後に挿入するのもいいのですが、同じクラス内で複数のタイマーを用いていたり、タイマーの用途が様々であったりするとどのタイミングでタイマーを停止するのか判断が難しいことがあります。
個人的なオススメはclassを破棄する直前に入れることです。
これだと、classを破棄するタイミングで使用したタイマーを停止、破棄すればよく、統一感があり後々デバッグをするときなんかは楽です。
/// 画面が閉じる直前に呼ばれる
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// タイマーを停止する
if let workingTimer = timer{
workingTimer.invalidate()
}
}
確認方法
viewControllerが適切に破棄されているか判断するにはdeinit()を利用します。
deinitが呼び出されていれば、適切にメモリ解放が行われているということになります。
/// クラスのインスタンスが破棄されたときに呼ばれる
/// この部分が呼ばれていないとメモリ解放が適切に行われていない可能性が高い
deinit {
print("NextViewControllerがdeinitされました")
}
print文がコンソールで確認できたらメモリ解放完了です。
まとめ
タイマーをinvalidate()するか否かだけでアプリの安定性は格段に上がるので、常にセットで考えるように心がけるといいと思います。
全体のコードはGitHubにあげてあります。
https://github.com/shu26/zombie