Open4

iOSアプリ開発におけるMain Thread

kamimikamimi

iOS開発においては、メインスレッドでUIの更新を行うことが原則となっている。

例えば

// バックグラウンドのスレッド
DispatchQueue.global(qos: .userInitiated).async {
    if let url = URL(string: urlString) {
        if let data = try? Data(contentsOf: url) {
            self.parse(json: data)
            return
        }
    }

    self.showError()
}

func showError() {
    // メインスレッドに戻す
    DispatchQueue.main.async {
        let ac = UIAlertController(title: "Loading error", message: "There was a problem loading the feed; please check your connection and try again.", preferredStyle: .alert)
        ac.addAction(UIAlertAction(title: "OK", style: .default))
        self.present(ac, animated: true)
    }
}

https://www.hackingwithswift.com/read/9/4/back-to-the-main-thread-dispatchqueuemain

Swift Concurrency時代ではこう。

await MainActor.run {
    self.showError()
}

Objective-Cではこう

dispatch_async(dispatch_get_main_queue(), ^{
    // メインスレッドで実行 UIの処理など
});

https://qiita.com/valmet/items/6de0921ca6106414228c

kamimikamimi

なぜUIの更新はメインスレッドで行う必要があるのか

理由1. UIApplication はメインスレッドにセットアップされるため

  • 全てのViewはUIApplicationインスタンスの子供なので、基本的に同じスレッドで処理されるべき

理由2. iPhoneのグラフィックレンダリングのパイプラインは、最終的に同期的なため

  • iPhoneの画面への描画、つまりLEDディスプレイ上のピクセルの点灯は、画面上のすべてのピクセルを同時に処理する必要がある。非同期プログラミングは、定義上、同期ではなく並列であり、非同期処理がいつ終了するかは確実ではありません。
    • iPhoneのディスプレイに対して非同期描画を許可してしまうと、画面全体がレンダリングされるときにその部分の処理ができていないため、チラつきや欠落が多発することになりかねません。

例えばUIKitをリファクタして複数のスレッドでUIを更新できるようにしたとしても、、、GPUが処理不能になり、パフォーマンスが落ちる

  • リファクタしたUIKit を使用すると、多くのバックグラウンド スレッドが UI を更新するため、Runloop の最後で画面をレンダリングする必要があるときに問題が発生する
    • 各スレッドが異なるレンダリング情報をコミットするため、より多くのコミット トランザクションを処理する必要があるため、コア アニメーション パイプラインは常に情報を GPU にコミットします。
    • ただし、レンダリングは実際にはシステム リソース (ビデオ メモリと CPU を占有する) を非常に消費する操作であり、スレッド間のコンテキストの頻繁な切り替えと多数のトランザクションによって GPU が処理不能になり、その結果、パフォーマンスに影響が生じ、レンダリングを完了できなくなります
    • レイヤー ツリーの送信は 1/60 秒で行われ、深刻な失速が発生しました。

理由3. メインスレッドでUIを更新する方が、いろんな問題を考慮せず簡単に済むため

UIKitではほとんどがnonatomicになっている(複数スレッドから書き込みな状態で、スレッドセーフではない)。
その理由は、UIKitはとても大きなフレームワークのため、全てのプロパティをスレッドセーフにすることは非現実的だから。

例えばこれがatomicになったとしても、、、以下のようなことを考える必要が出てくる。

  • ビューのプロパティを非同期で変更できると仮定すると、これらの変更は同時に有効になるか、または各スレッドの独自のRunLoopに従うか?
  • UITableViewがバックグラウンドスレッドでセルを削除した後、別のバックグラウンドスレッドがこのセルのインデックスを操作すると、クラッシュすることがあります。
  • バックグラウンドスレッドがビューを削除し、このスレッドのRunLoopが終了していない場合、同時にユーザーがこの「削除されるビュー」をタップするので、私はタッチイベントに応答する必要がありますか?どのスレッドに応答すればよいのでしょうか?

https://www.quora.com/Why-must-the-UI-always-be-updated-on-Main-Thread

https://medium.com/@duwei199714/ios-why-the-ui-need-to-be-updated-on-main-thread-fd0fef070e7f


参考情報

  • Objective-Cのatomicnonatomicについて

https://qiita.com/uasi/items/80660f9aa20afaf671f3

  • Core AnimationのApple公式ドキュメント

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40004514

  • WWDC14で発表があったらしいが、動画はなかったのでWWDC notesを

https://www.wwdcnotes.com/notes/wwdc14/419/

  • Tim Oliverさんがtry! Swiftで発表した内容

https://academy.realm.io/posts/tryswift-tim-oliver-advanced-graphics-with-core-animation/

kamimikamimi

メインスレッドに関するチェック

Swift ConccurencyのMain Actor登場前

  • メインスレッドで実行されているか確認する
print(Thread.isMainThread) // true or false

https://developer.apple.com/documentation/foundation/thread/1412704-ismainthread

  • とある関数をメインスレッドで実行する必要がある場合、以下のように実装すると事前にチェックできる
func updateSomeUI() {
    assert(Thread.main == Thread.current, "Must be run on the main queue")

    // do something
}

以下の動画の27:32で使われていた

https://developer.apple.com/videos/play/wwdc2021/10194


ちなみにassertpreconditionの違いは以下

https://qiita.com/koher/items/ca7f388ab2a4e6747339

Swift ConccurencyのMain Actor登場後

そこでMainActor登場!🪄

以下のように実装することで、自動的に関数がメインスレッドで実行されるので、assertの実装が不要になり、呼び出し元でDispatch.main.async {}MainActor.run {}の実装も不要になる!

// 呼び出し元では、await をつければok
await updateSomeUI()

@MainActor
func updateSomeUI() {
    // do something
}

メインスレッドで複数の呼び出しをしたいときに、MainActor.run {}は役に立つ。
例えば、UI更新をするとき、実行する操作の間に MainのRunLoopを回したくない時など。