LoginSignup
8
7

More than 1 year has passed since last update.

RunLoopとは?

Posted at

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.mainRunLoop.mainが混合してしまう場合がある。しかし、それらは決して同じものでない。
  • DisaptchQueue.mainはデフォルトでRunLoopが設定されているものである。
  • それに対して、RunLoopには.commonという種類(先ほどのタイマーで使った)と.defaultの種類があり、.defaultDispatchQueue.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
    }
}
  • 上記のコードで生成したアプリがこちらである。
    runLoopExample.gif
  • 見ていただくとわかる通り、.commonでタイマーを走らせている部分はTableViewをスクロールし続けても、通常通り時間が進んでいるのに対し、.defaultの部分は時間が止まってしまっている。

まとめ

  • このようにRunLoopはスレッドと密接に関わり、私たち開発者としてもしっかりとその概念や作りを理解しておかなければ、Timerなどの挙動がおかしい時のデバッグの沼にハマってしまう。
8
7
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7