Open4
iOSアプリ開発におけるMain Thread
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)
}
}
Swift Concurrency時代ではこう。
await MainActor.run {
self.showError()
}
Objective-Cではこう
dispatch_async(dispatch_get_main_queue(), ^{
// メインスレッドで実行 UIの処理など
});
なぜ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が終了していない場合、同時にユーザーがこの「削除されるビュー」をタップするので、私はタッチイベントに応答する必要がありますか?どのスレッドに応答すればよいのでしょうか?
参考情報
- Objective-Cの
atomic
とnonatomic
について
- Core AnimationのApple公式ドキュメント
- WWDC14で発表があったらしいが、動画はなかったのでWWDC notesを
- Tim Oliverさんがtry! Swiftで発表した内容
SwiftUI と メインスレッド(MainActor)
- 共有ステートである、
EnvironmentObject
やObservableObject
にアクセスするSwiftUIのViewは、常にMain Actorの上にある
メインスレッドに関するチェック
Swift ConccurencyのMain Actor登場前
- メインスレッドで実行されているか確認する
print(Thread.isMainThread) // true or false
- とある関数をメインスレッドで実行する必要がある場合、以下のように実装すると事前にチェックできる
func updateSomeUI() {
assert(Thread.main == Thread.current, "Must be run on the main queue")
// do something
}
以下の動画の27:32で使われていた
ちなみにassert
とprecondition
の違いは以下
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を回したくない時など。