🐶

iOSのwatchdogの挙動について

2022/05/11に公開

はじめに

この記事ではAddressing Watchdog Terminationsをもとにwatchdogによるアプリ終了への対処法について記載していきます。
(自分のための翻訳メモみたいなものなので可読性については保証できていません)

概要

ユーザーは、アプリがすばやく起動し、タッチやジェスチャーに反応することを期待しています。オペレーティングシステムは、アプリの起動時間と応答性を監視して応答のないアプリを終了させるwatchdogを採用しています。watchdogの終了には、クラッシュレポートの「終了理由」に0x8badf00d("ate bad food"と読む)というコードが使用されます。

Exception Type:  EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note:  EXC_CORPSE_NOTIFY
Termination Reason: Namespace SPRINGBOARD, Code 0x8badf00d

watchdogは、かなりの時間メインスレッドをブロックするアプリを終了させます。メインスレッドを長時間ブロックする方法は、次のようなものがあります:

  • 同期通信
  • 大きなJSONファイルや3Dモデルなどの大容量データの処理
  • 大規模Core Dataストアの軽量マイグレーションを同期的に実行
  • Visionによる分析要求
    メインスレッドのブロックがなぜ問題なのかを理解するために、最も一般的な例として、同期通信からUIにデータをロードする場合を考えてみます。メインスレッドが通信リクエストで忙しいと、通信が完了するまで、システムは複数のスクロールイベントなどのUIイベントを処理することができません。通信完了までに時間がかかると、ユーザーがスクロールしてからアプリがスクロール・イベントに応答するまでにかなりの時間がかかります。このため、アプリが応答しないように感じられます。

App Responsiveness Watchdogの情報を読み解く

アプリの起動やイベントへの応答が遅い場合、クラッシュレポートにある終了に関する情報には、アプリがどのように時間を費やしたかについての重要な情報が含まれています。例えば、起動後すぐにUIをレンダリングしないiOSアプリは、クラッシュレポートに次のように記載されています:

Termination Description: SPRINGBOARD, 
    scene-create watchdog transgression: application<com.example.MyCoolApp>:667
    exhausted real (wall clock) time allowance of 19.97 seconds 
    | ProcessVisibility: Foreground 
    | ProcessState: Running 
    | WatchdogEvent: scene-create 
    | WatchdogVisibility: Foreground 
    | WatchdogCPUStatistics: ( 
    |  "Elapsed total CPU time (seconds): 15.290 (user 15.290, system 0.000), 28% CPU", 
    |  "Elapsed application CPU time (seconds): 0.367, 1% CPU" 
    | )

Termination Descriptionにscene-createが表示されている場合、アプリはUIの最初のフレームをウォールクロックの許容時間内に画面にレンダリングできなかったことを意味します。scene-createの代わりにscene-updateがTermination Descriptionに表示された場合、メインスレッドが忙しいため、アプリがUIを十分に速く更新できなかったことを意味します。

Elapsed total CPU timeは、システム上のすべてのプロセスについて、CPUがウォールクロック時間内にどれだけの時間を稼働したかを示しています。このCPU時間は、アプリケーションCPU時間と同様に、CPUコア全体のCPU使用率に対するもので、100%を超えることもあります。たとえば、1つのCPUコアの使用率が100%で、2つ目のCPUコアの使用率が20%の場合、合計のCPU使用率は120%になります。

Elapsed application CPU timeは、ウォールクロック時間内にアプリがCPU上で実行された時間を示しています。この数値がどちらかの極端な値の場合、問題のヒントとなります。この数値が高い場合、アプリはすべてのスレッドで重要な作業を実行しています。この数値はすべてのスレッドを集約したものであり、メインスレッドに固有のものではありません。この数値が低い場合は、ネットワーク接続などのシステムリソースを待っているため、アプリはほとんど待機状態です。

Background Task Watchdogの情報を読み解く(watchOS)

App Responsiveness Watchdogに加え、watchOSにはバックグラウンドタスクのwatchdogがあります。この例では、アプリがWatch Connectivityのバックグラウンド・タスクの処理を時間内に完了しなかったケースです。

Termination Reason: CAROUSEL, WatchConnectivity watchdog transgression. 
    Exhausted wall time allowance of 15.00 seconds.
Termination Description: SPRINGBOARD,
    CSLHandleBackgroundWCSessionAction watchdog transgression: xpcservice<com.example.MyCoolApp.watchkitapp.watchextension>:220:220 
    exhausted real (wall clock) time allowance of 15.00 seconds 
    | <FBExtensionProcess: 0x16df02a0; xpcservice<com.example.MyCoolApp.watchkitapp.watchextension>:220:220; typeID: com.apple.watchkit> 
      Elapsed total CPU time (seconds): 24.040 (user 24.040, system 0.000), 81% CPU 
    | Elapsed application CPU time (seconds): 1.223, 6% CPU, lastUpdate 2020-01-20 11:56:01 +0000

Watchdogが作動した理由の特定

バックトレースは、アプリのメインスレッドで何がそんなに時間をかけているのかを特定するのに役立つことがあります。たとえば、アプリがメインスレッドで同期通信を使用している場合、通信関数をメインスレッドのバックトレースで見ることができます。

Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   libsystem_kernel.dylib            0x00000001c22f8670 semaphore_wait_trap + 8
1   libdispatch.dylib                 0x00000001c2195890 _dispatch_sema4_wait$VARIANT$mp + 24
2   libdispatch.dylib                 0x00000001c2195ed4 _dispatch_semaphore_wait_slow + 140
3   CFNetwork                         0x00000001c57d9d34 CFURLConnectionSendSynchronousRequest + 388
4   CFNetwork                         0x00000001c5753988 +[NSURLConnection sendSynchronousRequest:returningResponse:error:] + 116  + 14728
5   Foundation                        0x00000001c287821c -[NSString initWithContentsOfURL:usedEncoding:error:] + 256
6   libswiftFoundation.dylib          0x00000001f7127284 NSString.__allocating_init+ 680580 (contentsOf:usedEncoding:) + 104
7   libswiftFoundation.dylib          0x00000001f712738c String.init+ 680844 (contentsOf:) + 96
8   MyCoolApp                         0x00000001009d31e0 ViewController.loadData() (in MyCoolApp) (ViewController.swift:21)

しかし、メインスレッドのバックトレースには、必ずしも問題の原因が含まれているとは限りません。例えば、あなたのアプリがあるタスクを完了するために、ウォールクロックの合計許容時間である5秒のうち、ちょうど4秒を必要とする場合を想像してみてください。watchdogが5秒後にアプリを終了させた場合、ほぼ全体の時間予算を消費したにもかかわらず、4秒かかったコードは完了したため、バックトレースに表示されません。クラッシュレポートは、watchdogがアプリを終了させた時に何をしていたかのバックトレースに記録します。(記録されたバックトレースフレームが問題の原因ではないとしても)

隠れた同期通信の特定

メインスレッドをブロックし、watchdogの終了につながる同期ネットワークは、時にその危険を覆い隠すような 抽象化レイヤーの背後に隠されています。先述のバックトレースの例では、アプリはフレーム 7 で https の URL を使ってinit(contentsOf:)を呼び出し、同期ダウンロードをトリガーしたことが示されています。このAPIは、イニシャライザから戻る前に、暗黙のうちに同期ネットワークリクエストを行います。このイニシャライザーが迅速に完了し、クラッシュレポートになかったとしても、watchdogの終了に寄与する可能性があります。XMLParserNSDataのようなURLパラメーターを取るイニシャライザーを持つ他のクラスも、同じように動作します。

その他、隠れた同期型ネットワークの一般的な例として、以下のようなものがあります。

  • SCNetworkReachability、到達性 API はデフォルトで同期的に動作します。SCNetworkReachabilityGetFlags(::)のような一見無害な関数が、wotchdogによる終了の引き金になることがあります。
  • gethostbyname(:) や gethostbyaddr(::) のような BSD が提供する DNS 関数は、メインスレッドで呼んでも決して安全ではありません。getnameinfo(::::::) や getaddrinfo(::::) のような関数は、DNS名ではなくIPアドレスだけを扱う場合のみ安全です(つまり、それぞれAI_NUMERICHOSTとNI_NUMERICHOSTを指定します)。

同期通信の問題は、ネットワーク環境に大きく依存します。ネットワーク接続が良好なオフィスで常にアプリをテストしていれば、この種の問題を目にすることはありません。しかし、あらゆる種類のネットワーク環境でアプリを実行するユーザーにアプリをデプロイし始めると、同期通信の問題がよく発生するようになります。Xcodeでは、不利なネットワーク条件をシミュレートして、ユーザーが遭遇する条件下でアプリをテストするのに役立てることができます。詳細はTest under adverse device conditions (iOS)を参照してください。

メインスレッドからコードを移動させる

アプリのUIに必要でない長時間実行されるコードは、すべてバックグラウンドキューに移動しましょう。不要な作業をバックグラウンドキューに移すことで、アプリのメイン・スレッドはアプリの起動をより速く完了し、イベントをより速く処理することができます。ネットワークの例では、メインスレッドで同期通信を実行するのではなく、非同期のバックグラウンドキューに移動させます。この作業をバックグラウンドキューに移すことで、メインスレッドはスクロールイベントが発生したときにそれを処理できるようになり、アプリの応答性が高まります。

長時間実行されるコードがシステムフレームワークの1つである場合、そのフレームワークがメインスレッドから作業を移す別のアプローチを提供しているかどうかを判断します。たとえば、RealityKitで複雑な3Dモデルをロードする際に、同期的なload(contentsOf:withName:)の代わりにloadAsync(contentsOf:withName:)を使うことを検討してみてください。別の例として、VisionpreferBackgroundProcessingを提供します。これは、システムが解析要求の処理をメインスレッドから移すべきことを示すヒントです。

もし通信関連のコードがWatchdogによるアプリ終了の原因になっている場合は、以下の一般的な解決策を検討してください:

  • URLSessionを使用してネットワークコードを非同期で実行する。これは最良の解決策です。非同期ネットワーキング・コードには、スレッドを気にすることなく安全にネットワークにアクセスできるなど、多くの利点があります。
  • SCNetworkReachability を使用する代わりに、NWPathMonitor を使用してネットワークパスが変更されたときに更新を受信します。このシステムは start(queue:) を呼び出すときに渡すキューで更新を配信するため、パスの更新はメインスレッドから外れて安全に機能します。
  • セカンダリスレッドで同期通信を実行する。ネットワーキングコードを非同期で実行することが非常に困難な場合は、セカンダリースレッドで同期通信を実行してwatchdogを回避してください。
  • DNSを手動で解決することは、ほとんどの状況で推奨されません。URLSession を使って、システムがあなたの代わりに DNS 解決を処理するようにしてください。もし切り替えが法外に難しく、手動で DNS アドレスを必要とし続けるなら、CFHost や <dns_sd.h> の API のような非同期の API を使ってください。

Discussion