開発におけるフリーズ — 本質、原因、防止

著者: IT Sectr 公開日: 2026-07-28 読了時間: 9 分

フリーズ(ハング)とは、モバイルアプリケーションが長時間にわたってユーザーの操作に応答しなくなる状態です。ラグ(動作の遅延)やグリッチ(不正確な動作)とは異なり、フリーズはUIを完全にブロックします:タッチが処理されず、アニメーションが停止し、画面が“固まります”。原因は、同期操作によるメインスレッドのブロック、マルチスレッドコードでのデッドロック、または異常に長いガベージコレクションです。Apple Main Thread Checkerのドキュメントによると、iOSのクラッシュレポートの40%以上がメインスレッドのブロッキングに関連しています。Androidでは、同様の状況がANR(システムダイアログ「アプリが応答しません」)を引き起こします。

重要なポイント

  • フリーズ — 長時間にわたるUIの完全なブロック(数秒から数十秒)、ラグやグリッチとは異なります
  • 主な原因 — I/Oによるメインスレッドのブロック、スレッド間のデッドロック、無限ループ、長時間のGCによるメモリリーク
  • 診断には、iOSのMain Thread Checker、AndroidのANRログ/data/anr/traces.txt、スレッドダンプ分析が含まれます
  • 解決 — 長時間の可能性がある操作をバックグラウンドスレッドにオフロード、Structured Concurrencyの使用、UIスレッドでのsynchronizedの回避
  • 予防 — StrictMode、DebugスキームのMain Thread Checker、デッドロックの静的解析、応答時間を測定する定期的なテスト

モバイル開発におけるフリーズとは

フリーズ(ハング)とは、モバイルアプリケーションにおいて、アプリが数秒以上にわたって入力イベントの処理とインターフェースの更新を停止する状態です。技術的には、メインスレッドがブロックされ、次のランループイテレーションを実行できないことを意味します。

フリーズ、ラグ、ANRの違い

ラグは最大500msの遅延で、ユーザーは遅さに気づきますがアプリは動作し続けます。フリーズは1秒から数十秒続きます。AndroidのANRは、5秒以上続きシステムによって検出されたフリーズの特殊なケースです。すべてのフリーズがANRにつながるわけではありませんが、すべてのANRはシステムによって記録されたフリーズです。

フリーズの結果

Androidでは、5秒以上のフリーズはANRダイアログをトリガーし、アプリを閉じるように促します。iOSでは、システムにウォッチドッグがあります — アプリが10~20秒間イベントに応答しない場合、ウォッチドッグはコード0x8badf00d(ate bad food)でプロセスを終了します。ユーザーにはアプリが突然ホーム画面に閉じられるように見えます。

AndroidとiOSでのフリーズの原因

100ms以上かかりメインスレッドで実行される操作は、フリーズの原因になる可能性があります。ブロッキングの主な原因を見てみましょう。

UIスレッドでの同期I/O

大きなファイルの読み取り、非同期なしのネットワークリクエスト、同期メソッド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は無限に繰り返されるコールスタックをキャプチャするスタックショットを介して検出します。

  • Android — 閉じられていないCursor、enqueue()の代わりにexecute()による同期リクエスト、UIスレッドでのFileInputStream.read()
  • iOS — performSelectorOnMainThread:withObject:waitUntilDone:YES、NSURLConnection sendSynchronousRequestの起動、dataWithContentsOfURLによる画像読み込み
  • クロスプラットフォーム — 専用isolateなしのFlutter compute、同期的なReact Native NativeModule

フリーズの診断方法

フリーズの診断には、ブロッキングの瞬間にすべてのスレッドの状態をキャプチャできるツールが必要です。

AndroidのANRログ

ANRが発生するたびに、Androidシステムは各アプリスレッドのスタックダンプを含むファイル/data/anr/traces.txtを保存します。このファイルの分析が主要な診断方法です:mainスレッドを見つけ、どのメソッドで停止したかを確認します。スタックがThread.sleep、InputStream.read、またはLock.lockで終わっている場合 — 原因が見つかりました。

iOSのスタックショット

Xcodeはアプリがフリーズしたとき(SIGSTOPシグナル)にスタックショット — すべてのスレッドスタックのスナップショット — を取得できます。スキームで「Logging」→「Include Stackshot Logs」を有効にします。コード0x8badf00dでクラッシュした場合、Devices & Simulatorsからクラッシュログを抽出し、スタックが固まっているcom.apple.main-threadを見つけます。

XcodeのMain Thread Checker

Main Thread Checkerは、アプリの実行中にバックグラウンドスレッドからのUIKit呼び出しを自動的に検出します。スキームで有効にします(Diagnostics → Main Thread Checker)。各警告はフリーズの潜在的な原因であり、特にネットワークリクエストのcompletionHandlerクロージャで発生した場合です。

AndroidでのStrictModeによるブロッキング検出の例:

kotlin
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())
    }
}

UIブロッキングの解消方法

フリーズの解消は、長時間の可能性があるすべての操作をバックグラウンドスレッドに移動することから始まります。各プラットフォームの具体的なテクニックを見てみましょう。

コルーチンによる構造化された並行処理

Kotlin CoroutinesをviewModelScope.launch(Dispatchers.IO)とともに使用すると、ネットワーク操作やデータベース読み取りがバックグラウンドスレッドで実行されることが保証されます。Dispatchers.MainはUIの更新にのみ使用されます。重要:すべてのsuspend関数は構造化される必要があります — 子コルーチンは親がキャンセルされるとキャンセルされ、スレッドリークを防ぎます。

iOSの非同期キュー

Grand Central Dispatchで、バックグラウンドタスクにはDispatchQueue.global(qos: .userInitiated)、UI更新にはDispatchQueue.main.asyncを使用するのが標準的なパターンです。メインキューでのsync()は避けてください — これは確実なデッドロックです。async/await(Swift 5.5+)を使用すると、MainActorを介したメインスレッドへの自動復帰により、より読みやすい非同期コードになります。

UIスレッドでのsynchronizedの回避

メインスレッドでのKotlinのsynchronizedブロックとSwiftの@synchronizedは危険です:別のスレッドがすでにこのロックを取得している場合、メインスレッドは待機中にフリーズします。ロックの代わりにアトミック型(AtomicInteger、Swiftのatomicプロパティ)またはシリアルキューを使用してください。

Androidでのコルーチンを使った非同期データ読み込みの例:

kotlin
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

スレッドポリシーにpenaltyDeathを指定してStrictModeを設定すると、メインスレッドでネットワーク呼び出しやディスクI/Oが検出されたときにアプリが即座にクラッシュします。開発者は問題を無視できなくなります。プロダクションビルドでは、penaltyLogを使用してクラッシュなしで統計を収集します。

DebugスキームのMain Thread Checker

iOSでは、DebugスキームでMain Thread Checkerを有効にし、CIでこのオプションを使用してテストを実行するよう設定します。テストにバックグラウンドスレッドからのUIKit呼び出しが含まれている場合 — 失敗するはずです。これがTestFlightに送信する前に問題を特定する唯一の信頼できる方法です。

マルチスレッドチェックを含むピアレビュー

コードレビュープロセスに必須項目を追加します:ネットワーク呼び出し、ファイル操作、データベースアクセス、または重い計算がバックグラウンドスレッドで実行されることを確認します。デッドロックは静的アナライザーで検出できます:FacebookのInferとXcodeのThread Safety Checkerは、実行前に潜在的なロックを見つけます。

  • Android — StrictMode、Infer、Android Lint Multithread、viewModelScopeを使用したKotlin Coroutines
  • iOS — Main Thread Checker、TSAN(Thread Sanitizer)、Xcode Analyze、MainActorを使用したSwift async/await
  • クロスプラットフォーム — Flutter compute isolate、requestAnimationFrameを使用したReact Native interaction manager

よくある質問

フリーズとANRの違いは何ですか?

ANR(Application Not Responding)は、メインスレッドが5秒以上フリーズしたときに表示されるAndroidシステム通知です。フリーズはより広い概念で、任意の期間のUIブロッキングを指します。iOSにはANRはありませんが、10~20秒のタイムアウトを持つウォッチドッグがあります。

Androidでtraces.txtを読むには?

ファイルは/data/anr/traces.txtにあります。アクセスにはrootアクセスまたはadb shellが必要です:root権限でadb shell cat /data/anr/traces.txt \> traces.txtを実行します。スタックで「main」スレッドを見つけます — 最後に呼び出されたメソッドがブロッキングの原因を示します。

iOSでアプリがフリーズするのにクラッシュしないのはなぜですか?

フリーズが10秒未満の場合、ウォッチドッグは作動せず、アプリはブロッキング操作が完了するまで単に「ハング」します。ユーザーはクラッシュを見ませんが、フラストレーションを感じます。このようなケースを検出するには、カスタム実行時間トレースとともにMetricKitを使用します。

フリーズに対してアプリをテストするには?

画面が1秒未満で開くことを確認するUIテストを使用します。タップから次の画面表示までの時間測定をCIに追加します。Androidでは、IdlingResourceを使用して非同期操作を待機するEspressoを使用します。iOSでは、読み込み時間を確認するためにXCTWaiterを使用したXCTestを使用します。

SwiftUIはフリーズを引き起こす可能性がありますか?

SwiftUI自体はフリーズを引き起こしませんが、bodyプロパティでの複雑な計算は引き起こします。重い操作のためにbodyの計算に500msかかる場合、UIがフリーズします。解決策は、計算をTask.detachedにオフロードし、メインアクターで@Stateを非同期に更新することです。

まとめ

  • フリーズ — メインスレッドのブロッキング、デッドロック、または無限ループによって引き起こされる、数秒から数十秒にわたるUIの完全なブロッキング
  • 診断 — Androidでは/data/anr/traces.txt、iOSではStackshotとMain Thread Checker
  • 主な原因 — 同期I/O、スレッド間のデッドロック、無限再帰、長時間のGC
  • 解決 — 正しいディスパッチャーを使用したコルーチン、MainActorを使用したasync/await、すべてのI/O操作のバックグラウンドスレッドへのオフロード
  • 予防 — penaltyDeath付きStrictMode、Main Thread Checker、デッドロックの静的解析(Infer、TSAN)
  • Androidではフリーズ > 5秒 = ANR;iOSでは > 10~20秒 = ウォッチドッグクラッシュ(0x8badf00d)
  • 推奨:DebugスキームでThread Sanitizerを有効にし、データ競合とデッドロックを検出するためにTSANを使用したテストをCIで実行するよう設定します

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください