フリーズ(ハング)とは、モバイルアプリケーションが長時間にわたってユーザーの操作に応答しなくなる状態です。ラグ(動作の遅延)やグリッチ(不正確な動作)とは異なり、フリーズはUIを完全にブロックします:タッチが処理されず、アニメーションが停止し、画面が“固まります”。原因は、同期操作によるメインスレッドのブロック、マルチスレッドコードでのデッドロック、または異常に長いガベージコレクションです。Apple Main Thread Checkerのドキュメントによると、iOSのクラッシュレポートの40%以上がメインスレッドのブロッキングに関連しています。Androidでは、同様の状況がANR(システムダイアログ「アプリが応答しません」)を引き起こします。
重要なポイント
フリーズ(ハング)とは、モバイルアプリケーションにおいて、アプリが数秒以上にわたって入力イベントの処理とインターフェースの更新を停止する状態です。技術的には、メインスレッドがブロックされ、次のランループイテレーションを実行できないことを意味します。
ラグは最大500msの遅延で、ユーザーは遅さに気づきますがアプリは動作し続けます。フリーズは1秒から数十秒続きます。AndroidのANRは、5秒以上続きシステムによって検出されたフリーズの特殊なケースです。すべてのフリーズがANRにつながるわけではありませんが、すべてのANRはシステムによって記録されたフリーズです。
Androidでは、5秒以上のフリーズはANRダイアログをトリガーし、アプリを閉じるように促します。iOSでは、システムにウォッチドッグがあります — アプリが10~20秒間イベントに応答しない場合、ウォッチドッグはコード0x8badf00d(ate bad food)でプロセスを終了します。ユーザーにはアプリが突然ホーム画面に閉じられるように見えます。
100ms以上かかりメインスレッドで実行される操作は、フリーズの原因になる可能性があります。ブロッキングの主な原因を見てみましょう。
大きなファイルの読み取り、非同期なしのネットワークリクエスト、同期メソッドapplyとそれに続くcommitによるSharedPreferencesへのデータ保存 — これらすべての操作はメインスレッドをブロックします。Androidでは、10MBのファイルを同期的に読み取ると、フラッシュメモリの速度に応じて200~500msかかる可能性があります。iOSでは、completionHandlerなしの同期URLSession読み込みは、サーバーの応答時間の間UIをブロックします。
2つのスレッドが互いに保持されているリソースを待機するとき、デッドロックが発生します。モバイルアプリケーションの典型的なシナリオは、スレッドAがLock1をロックしてLock2を待機し、スレッドBがLock2をロックしてLock1を待機するものです。両方のスレッドが永久にフリーズします。そのうちの1つがメインスレッドの場合、アプリケーションは完全にフリーズします。
ロジックエラー — たとえば、終了条件なしのwhile(true)やベースケースなしの再帰 — はメインスレッドでの無限実行につながります。Androidは5秒後にANRを介してこれを検出し、iOSは無限に繰り返されるコールスタックをキャプチャするスタックショットを介して検出します。
フリーズの診断には、ブロッキングの瞬間にすべてのスレッドの状態をキャプチャできるツールが必要です。
ANRが発生するたびに、Androidシステムは各アプリスレッドのスタックダンプを含むファイル/data/anr/traces.txtを保存します。このファイルの分析が主要な診断方法です:mainスレッドを見つけ、どのメソッドで停止したかを確認します。スタックがThread.sleep、InputStream.read、またはLock.lockで終わっている場合 — 原因が見つかりました。
Xcodeはアプリがフリーズしたとき(SIGSTOPシグナル)にスタックショット — すべてのスレッドスタックのスナップショット — を取得できます。スキームで「Logging」→「Include Stackshot Logs」を有効にします。コード0x8badf00dでクラッシュした場合、Devices & Simulatorsからクラッシュログを抽出し、スタックが固まっているcom.apple.main-threadを見つけます。
Main Thread Checkerは、アプリの実行中にバックグラウンドスレッドからのUIKit呼び出しを自動的に検出します。スキームで有効にします(Diagnostics → Main Thread Checker)。各警告はフリーズの潜在的な原因であり、特にネットワークリクエストのcompletionHandlerクロージャで発生した場合です。
AndroidでのStrictModeによるブロッキング検出の例:
class App : Application() {
override fun onCreate() {
super.onCreate()
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build())
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyDeath()
.build())
}
}
フリーズの解消は、長時間の可能性があるすべての操作をバックグラウンドスレッドに移動することから始まります。各プラットフォームの具体的なテクニックを見てみましょう。
Kotlin CoroutinesをviewModelScope.launch(Dispatchers.IO)とともに使用すると、ネットワーク操作やデータベース読み取りがバックグラウンドスレッドで実行されることが保証されます。Dispatchers.MainはUIの更新にのみ使用されます。重要:すべてのsuspend関数は構造化される必要があります — 子コルーチンは親がキャンセルされるとキャンセルされ、スレッドリークを防ぎます。
Grand Central Dispatchで、バックグラウンドタスクにはDispatchQueue.global(qos: .userInitiated)、UI更新にはDispatchQueue.main.asyncを使用するのが標準的なパターンです。メインキューでのsync()は避けてください — これは確実なデッドロックです。async/await(Swift 5.5+)を使用すると、MainActorを介したメインスレッドへの自動復帰により、より読みやすい非同期コードになります。
メインスレッドでのKotlinのsynchronizedブロックとSwiftの@synchronizedは危険です:別のスレッドがすでにこのロックを取得している場合、メインスレッドは待機中にフリーズします。ロックの代わりにアトミック型(AtomicInteger、Swiftのatomicプロパティ)またはシリアルキューを使用してください。
Androidでのコルーチンを使った非同期データ読み込みの例:
class DataViewModel : ViewModel() {
private val _data = MutableStateFlow<List<Item>>(emptyList())
val data: StateFlow<List<Item>> = _data.asStateFlow()
fun loadData() {
viewModelScope.launch(Dispatchers.IO) {
val result = fetchFromNetwork()
_data.emit(result)
}
}
}
ツール、アーキテクチャ原則、コードレビュープロセスの組み合わせが、フリーズを体系的に防ぐのに役立ちます。
スレッドポリシーにpenaltyDeathを指定してStrictModeを設定すると、メインスレッドでネットワーク呼び出しやディスクI/Oが検出されたときにアプリが即座にクラッシュします。開発者は問題を無視できなくなります。プロダクションビルドでは、penaltyLogを使用してクラッシュなしで統計を収集します。
iOSでは、DebugスキームでMain Thread Checkerを有効にし、CIでこのオプションを使用してテストを実行するよう設定します。テストにバックグラウンドスレッドからのUIKit呼び出しが含まれている場合 — 失敗するはずです。これがTestFlightに送信する前に問題を特定する唯一の信頼できる方法です。
コードレビュープロセスに必須項目を追加します:ネットワーク呼び出し、ファイル操作、データベースアクセス、または重い計算がバックグラウンドスレッドで実行されることを確認します。デッドロックは静的アナライザーで検出できます:FacebookのInferとXcodeのThread Safety Checkerは、実行前に潜在的なロックを見つけます。
よくある質問
ANR(Application Not Responding)は、メインスレッドが5秒以上フリーズしたときに表示されるAndroidシステム通知です。フリーズはより広い概念で、任意の期間のUIブロッキングを指します。iOSにはANRはありませんが、10~20秒のタイムアウトを持つウォッチドッグがあります。
ファイルは/data/anr/traces.txtにあります。アクセスにはrootアクセスまたはadb shellが必要です:root権限でadb shell cat /data/anr/traces.txt \> traces.txtを実行します。スタックで「main」スレッドを見つけます — 最後に呼び出されたメソッドがブロッキングの原因を示します。
フリーズが10秒未満の場合、ウォッチドッグは作動せず、アプリはブロッキング操作が完了するまで単に「ハング」します。ユーザーはクラッシュを見ませんが、フラストレーションを感じます。このようなケースを検出するには、カスタム実行時間トレースとともにMetricKitを使用します。
画面が1秒未満で開くことを確認するUIテストを使用します。タップから次の画面表示までの時間測定をCIに追加します。Androidでは、IdlingResourceを使用して非同期操作を待機するEspressoを使用します。iOSでは、読み込み時間を確認するためにXCTWaiterを使用したXCTestを使用します。
SwiftUI自体はフリーズを引き起こしませんが、bodyプロパティでの複雑な計算は引き起こします。重い操作のためにbodyの計算に500msかかる場合、UIがフリーズします。解決策は、計算をTask.detachedにオフロードし、メインアクターで@Stateを非同期に更新することです。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。