RunLoopとは
- RunLoopとは、スレッドと深く結びついている。RunLoopはスレッドに何か処理をする必要がある場合に、その命令を伝え、逆に何もない時に何の処理もさせないと命令するものである。
スレッドとRunLoopの関係性
- スレッドにはそれぞれRunLoopが存在する
- メインスレッドには自動でRunLoopが生成される
- サブスレッド用のRunLoopは生成する必要がある
- RunLoopはスレッド生成時に同じく生成され、そのスレッドが破棄されると破棄される
RunLoopについての説明
- 大前提アプリはインタラクティブであり、ユーザーのインプットを検知し、反応する。
- RunLoopはそれらを検知し、スレッドに伝える役割を行う
- ユーザーのタッチイベント
- タイマーイベント
- スクリーン上でのタッチポイント
- などだ。
- なので、RunLoopは常にイベントを監視する役割を担っている。
- 以下のような形でRunLoopを見ることができる。
print(runloop.main)
// <_NSMainThread: 0x600000a18140>{number = 1, name = main} このようにプリントされる
RunLoop生成の方法
- 以下のような形でRunLoopを生成できる。
let timerUpdate = Timer.scheduledTimer(
timeInterval: 1.0,
target: self,
selector: #selector(fireNumberUpdateTimer),
userInfo: nil,
repeats: true
)
RunLoop.current.add(timerUpdate, forMode: .common)
よくある間違い
- よく
DisapatchQueue.main
とRunLoop.main
が混合してしまう場合がある。しかし、それらは決して同じものでない。 -
DisaptchQueue.main
はデフォルトでRunLoop
が設定されているものである。 - それに対して、
RunLoop
には.common
という種類(先ほどのタイマーで使った)と.default
の種類があり、.default
がDispatchQueue.main
に自動で設定される。 -
RunLoop.default
はタッチイベントによってスレッドがブロックされないように設定されている。
なぜタイマーで.commonを使ったのか?
-
RunLoop.default
は先ほど記述したようにタッチイベントによってスレッドがブロックされないように設定されているため、タイマーを利用した際に挙動がおかしくなる。 - 以下のコードを見ていただきたい。
import UIKit
class ViewController: UIViewController {
@IBOutlet private var tableView: UITableView!
var number = 0
var numberUpdate = 0
override func viewDidLoad() {
super.viewDidLoad()
print(Thread.current)
print("Main Loop: \(RunLoop.main)")
setupTableView()
Timer.scheduledTimer(
timeInterval: 1.0,
target: self,
selector: #selector(fireTimer), // ←.defaultのものを利用
userInfo: nil,
repeats: true
)
let timerUpdate = Timer.scheduledTimer(
timeInterval: 1.0,
target: self,
selector: #selector(fireNumberUpdateTimer),
userInfo: nil,
repeats: true
)
RunLoop.current.add(timerUpdate, forMode: .common) // ← .commonを指定
}
@objc func fireTimer() {
number += 1
tableView.reloadData()
}
@objc func fireNumberUpdateTimer() {
numberUpdate += 1
tableView.reloadData()
}
private func setupTableView() {
tableView.backgroundColor = .red
tableView.dataSource = self
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
cell.textLabel?.text = number.description
return cell
}
let cell = UITableViewCell(style: .default, reuseIdentifier: "celltwo")
cell.textLabel?.text = numberUpdate.description
return cell
}
}
- 上記のコードで生成したアプリがこちらである。
- 見ていただくとわかる通り、
.common
でタイマーを走らせている部分はTableViewをスクロールし続けても、通常通り時間が進んでいるのに対し、.default
の部分は時間が止まってしまっている。
まとめ
- このようにRunLoopはスレッドと密接に関わり、私たち開発者としてもしっかりとその概念や作りを理解しておかなければ、Timerなどの挙動がおかしい時のデバッグの沼にハマってしまう。