RORK LABEN
PLAY — Google Play の target API level 36 要件が昨日8月31日に発効しました。今日以降、新規アプリと既存アプリの更新は Android 16 対応が必須ですVISIBILITY — API 35 のままのアプリは掲載こそ続きますが、新しい Android 版のユーザーには表示されなくなります。エラーが出ないまま新規インストールだけが減る点に注意が要りますEXTENSION — 間に合わなかった場合は、Play Console から2026年11月1日までの延長申請が出せます。恒久対応の計画とセットで進めるのが実務的ですAPPLE — Apple 側は9月9日にイベント、iOS 27 の正式リリースは9月14日と報じられています。生成したアプリの iOS 27 実機確認はリリース週の前に済ませておきたいところですEXPO — Expo が expo-paste-input を公開しました(8月28日)。React Native の TextInput に画像・GIF・ステッカーの貼り付けを追加するネイティブモジュールですEAS — EAS Observe が8月20日に GA になりました。クラッシュや性能の観測を、ビルドや配信と同じ EAS 上で持てるようになっていますPLAY — Google Play の target API level 36 要件が昨日8月31日に発効しました。今日以降、新規アプリと既存アプリの更新は Android 16 対応が必須ですVISIBILITY — API 35 のままのアプリは掲載こそ続きますが、新しい Android 版のユーザーには表示されなくなります。エラーが出ないまま新規インストールだけが減る点に注意が要りますEXTENSION — 間に合わなかった場合は、Play Console から2026年11月1日までの延長申請が出せます。恒久対応の計画とセットで進めるのが実務的ですAPPLE — Apple 側は9月9日にイベント、iOS 27 の正式リリースは9月14日と報じられています。生成したアプリの iOS 27 実機確認はリリース週の前に済ませておきたいところですEXPO — Expo が expo-paste-input を公開しました(8月28日)。React Native の TextInput に画像・GIF・ステッカーの貼り付けを追加するネイティブモジュールですEAS — EAS Observe が8月20日に GA になりました。クラッシュや性能の観測を、ビルドや配信と同じ EAS 上で持てるようになっています
記事一覧/開発ツール
開発ツール/2026-05-20中級

iOS の 0x8badf00d ウォッチドッグ強制終了に Rork アプリが落ちるときの対処

Rork で作った iOS アプリが起動直後に消える。クラッシュログに 0x8badf00d。Apple のウォッチドッグが React Native の重い初期化を切ったときの正しい直し方を、私の運用中アプリで実際に効いた手順で説明します。

rork58ios12crash3watchdoglaunch2react-native12expo11

iOS の 0x8badf00d ウォッチドッグ強制終了に Rork アプリが落ちるときの対処

App Store に出した直後だけ、レビューに「起動してすぐ消える」と書かれます。手元の Xcode でも Simulator でも再現しません。Crashlytics に残っているのは EXC_CRASH (SIGKILL) と例外コード 0x8badf00d。Apple が「ate bad food」と読ませる有名なシグネチャで、ウォッチドッグタイムアウトの印です。

厄介なのは、この落ち方が「自分のコードのバグ」の顔をしていないことです。スタックトレースの最上位にいるのは大抵サードパーティ SDK の初期化関数で、その関数自体は正しく動いています。iOS が待ちきれずにプロセスごと切った、というだけなのです。

React Native / Expo / Rork のように JS バンドルの評価が起動経路に乗る構成では、ネイティブ単体のときより余白がありません。SDK を一つ足すたびに確率が上がっていきます。原因の切り分け方と、運用中のアプリで実際に効いた対処を順に整理します。

0x8badf00d とは何が起きているのか

iOS は application(_:didFinishLaunchingWithOptions:) が返るまでに使ってよい時間を、デバイスの状態に応じて 5〜20 秒の範囲で動的に決めています。実効的には新しめの端末でも 10 秒前後、古い端末や寒い場所、バッテリー残量が低いとさらに短くなります。この上限を越えると iOS は理由を問わずプロセスを SIGKILL で落とし、例外コード 0x8badf00d を残します。

React Native / Expo の場合、JS スレッドが Hermes でバンドルを評価して最初の requireApplicationRegister を終えるまでの全部が、この計測に乗ります。Rork で量産したアプリでも、ネイティブ側で AdMob・AppOpen・Firebase・RevenueCat・Sentry・Branch を全部同期初期化していると、新しい iPhone でも余裕で 8〜9 秒に届いてしまい、古い端末では落ちます。

ポイントは、開発機で測ったコールドスタートが「2 秒」だから安全、ではないということです。ウォッチドッグの基準は端末側のリアルタイムの状態で決まります。Crashlytics の Apple 環境別クラッシュレートを見ると、私のアプリでもこのタイプは iPhone 8 / SE 第二世代 / iPad 第七世代に集中していました。

クラッシュログから 0x8badf00d を断定する

Crashlytics や Xcode Organizer の Crashes パネルで以下を確認すると、ほぼ確定で切り分けられます。

  • Exception Type: EXC_CRASH (SIGKILL)
  • Exception Codes: 0x8badf00d
  • Termination Reason: FRONTBOARD 0x8badf00d ... (scene-create watchdog transgression: ...)
  • Triggered by Thread の最上位スタックが RCTCxxBridge, facebook::hermes::HermesRuntime, expo-modules-core, あるいは AdMob / Firebase の +[FIRApp configure] など、起動経路の関数

Triggered by Thread: 0 でメインスレッドが固まっている、というのが典型です。JS スレッドや IO スレッドが原因でも、メインスレッドがそれを待っていることが多いです。

制限時間は推測しなくていい — ログに書いてある

見落とされがちなのですが、Termination Reason の文字列にはその場で与えられた猶予そのものが入っています。

Termination Reason: FRONTBOARD 2343432205
<RBSTerminateContext| domain:10 code:0x8BADF00D explanation:
scene-create watchdog transgression: application<com.example.app>:9312
exhausted real (wall clock) time allowance of 19.43 seconds>

exhausted real (wall clock) time allowance of 19.43 seconds の数字が、その端末・そのタイミングで iOS が実際に与えた時間です。何秒までに返せばいいのかを推測する必要はありません。複数のクラッシュを並べてこの値を書き出すと、自分のアプリが実際に戦っている下限が見えてきます。私の手元では、同じビルドでも 19 秒台から 9 秒台まで振れていました。

もう一つ重要なのは、0x8badf00d は起動時だけのコードではないという点です。どのライフサイクルで切られたのかは explanation の先頭で切り分けます。

explanation の先頭切られた場所まず疑うもの
scene-create watchdog transgression起動から最初のシーン生成までAppDelegate の同期初期化・JS バンドルの評価
scene-update watchdog transgressionフォアグラウンド復帰などのシーン更新復帰時の同期 IO・AppOpen 広告の同期 preload
process-exit watchdog transgression終了処理applicationWillTerminate 内の保存処理

起動経路を直したのにクラッシュが減らない、というときは scene-update 側だった、というのが実際よくあります。手を入れる前に explanation を読み分けてください。

まず疑う 4 つのブロッキング処理

私が踏んだ実例から、Rork 構成で 0x8badf00d を引き起こす原因の 9 割は次のどれかでした。

  1. AppDelegate での SDK の連続同期初期化: AdMob, Firebase, RevenueCat, Branch, Sentry を「念のため起動直後に全部」初期化している
  2. MMKV / AsyncStorage / SQLite からの同期読み込み: ユーザー設定、テーマ、A/B フラグなどを didFinishLaunching 内の同期 IO で取ろうとしている
  3. AppOpen 広告の同期 preload: AdMob の GADAppOpenAd.load をネイティブ側で await 相当の処理に組んでしまっている
  4. JavaScript の初回 import の重さ: App.tsx のトップで巨大な定数モジュール(多言語 JSON、配列でハードコードされた壁紙メタ 1 万件など)を import している

特に 1 と 3 は、Rork で生成された雛形をそのまま使ってしまうと起こりやすいパターンです。SDK 各社のドキュメントが揃って「起動直後に呼べ」と書いているせいで、結果として全部が直列に積み上がります。

直し方:起動経路から重い処理を全部追い出す

ネイティブ側(AppDelegate / ExpoAppDelegateSubscriber)

// 危険:すべてが didFinishLaunching に同期で乗っている
- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [FIRApp configure];
  [GADMobileAds.sharedInstance startWithCompletionHandler:nil];
  [RCPurchases configureWithAPIKey:@"YOUR_REVENUECAT_KEY"];
  [Branch.getInstance initSessionWithLaunchOptions:launchOptions
                              andRegisterDeepLinkHandler:^(NSDictionary *params, NSError *error){}];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

これを次のように、必須のものだけ残して残りは dispatch_after または最初のフレーム描画後に押し出します。

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  // クラッシュレポートだけは最優先で先に上げる
  [FIRApp configure];
 
  // 残りは UI が出てから初期化
  dispatch_async(dispatch_get_main_queue(), ^{
    [GADMobileAds.sharedInstance startWithCompletionHandler:nil];
    [RCPurchases configureWithAPIKey:@"YOUR_REVENUECAT_KEY"];
    [Branch.getInstance initSessionWithLaunchOptions:launchOptions
                                andRegisterDeepLinkHandler:^(NSDictionary *params, NSError *error){}];
  });
 
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

Firebase だけ先に動かすのは、AppOpen 広告や RevenueCat 側でクラッシュが起きてもログを拾えるようにするためです。

JavaScript 側

// App.tsx — 重い import をルートからは外す
import { useEffect, useState } from "react";
import { View } from "react-native";
 
export default function App() {
  const [ready, setReady] = useState(false);
 
  useEffect(() => {
    // 最初の描画が終わってから残りを読み込む
    (async () => {
      const [{ initAnalytics }, { initRemoteConfig }] = await Promise.all([
        import("./bootstrap/analytics"),
        import("./bootstrap/remote-config"),
      ]);
      await Promise.all([initAnalytics(), initRemoteConfig()]);
      setReady(true);
    })();
  }, []);
 
  return <View>{/* スプラッシュ相当の軽い UI をまず出す */}</View>;
}

Promise.all で並列にし、await import() を使ってバンドルから初回起動の依存を切り離します。Hermes は遅延 require と相性がよく、私の壁紙アプリでは平均コールドスタートが 2.4 秒 → 1.3 秒に縮みました。AdMob の eCPM や DAU に直接効くタイプの改善ではないですが、0x8badf00d のリテンション悪化分は確実に取り戻せます。

TestFlight では出ないクラッシュをローカルで再現する

ウォッチドッグの厳しさは「コールド起動 + 低バッテリー + 低温 + 悪い回線」で増します。手元で意図的に再現したいときは、次の条件を重ねます。

  1. iPhone 実機を放電 20% 以下にしてから airplane mode 解除
  2. 「設定 → デベロッパ → Network Link Conditioner」で Very Bad Network を有効化
  3. アプリを完全に閉じてから(フォアグラウンドにある状態ではダメ)アイコンタップ

これだけで、開発機で再現しなかった iPhone 8 のクラッシュを 5 回中 3 回は引けます。Firebase Test Lab の physical デバイスで --orientation portrait --device-ids iphone8 を指定して回す手もありますが、コストが嵩むので、私はこのローカル法を先にやります。

「直った」を実機の分布で確かめる — MetricKit

ローカル再現は当たりを付けるには有効ですが、直ったと言い切るには手元の 1 台では足りません。実際のユーザーの端末で起動時間がどう散らばっているかは、MetricKit が日次で返してくれます。

MXAppLaunchMetrichistogrammedTimeToFirstDraw は、その日の起動を「何秒台が何回」というヒストグラムで渡してきます。平均ではなく分布で見えるのが利点です。裾が 8 秒に張り付いているなら、ウォッチドッグに届いている端末が確実にいます。

import MetricKit
 
final class LaunchMetricsSubscriber: NSObject, MXMetricManagerSubscriber {
  static let shared = LaunchMetricsSubscriber()
 
  func start() {
    MXMetricManager.shared.add(self)
  }
 
  func didReceive(_ payloads: [MXMetricPayload]) {
    for payload in payloads {
      guard let histogram = payload.applicationLaunchMetrics?
              .histogrammedTimeToFirstDraw else { continue }
 
      for case let bucket as MXHistogramBucket<UnitDuration> in histogram.bucketEnumerator {
        let start = bucket.bucketStart.converted(to: .seconds).value
        let end = bucket.bucketEnd.converted(to: .seconds).value
        guard start >= 5 else { continue }   // 危険域だけを送る
        Analytics.logEvent("slow_launch", parameters: [
          "bucket": "\(start)-\(end)",
          "count": bucket.bucketCount,
        ])
      }
    }
  }
 
  // ウォッチドッグに切られた理由の文字列も MetricKit 経由で拾える(iOS 14 以降)
  func didReceive(_ payloads: [MXDiagnosticPayload]) {
    for payload in payloads {
      for crash in payload.crashDiagnostics ?? [] {
        guard crash.exceptionCode?.intValue == 0x8badf00d else { continue }
        Analytics.logEvent("watchdog_kill", parameters: [
          "reason": crash.terminationReason ?? "unknown",
        ])
      }
    }
  }
}

start()didFinishLaunching の中で呼んで構いません。MXMetricManager.shared.add は購読の登録だけで、この記事で追い出そうとしている類の処理は走らないためです。

Expo / Rork の構成でも、この Swift ファイルを config plugin 経由でネイティブターゲットに足せば動きます。JS 側へ橋渡しする必要はなく、Analytics に直接投げてしまうのが一番手数が少ないです。

ただし運用上の制約が一つあります。MetricKit のペイロードは 1 日 1 回、しかも次にアプリが起動されたときにまとめて配送されます。リリース当日に結果は出ません。段階配信を 24 時間止めて待つ、という運用とセットで初めて意味を持ちます。ここを知らずに「翌朝データが無い=実装ミス」と判断して、私は半日を溶かしました。

0x8badf00d が出ない構成を保つチェックリスト

リリース前に Xcode の Instruments → App Launch テンプレートで毎回計測しておくと、SDK を一つ足したときに気付けます。私の場合、各リリースブランチで次の 3 点を CI に乗せています。

  • App Launch (Pre-main + Time to first frame) が新 iPhone で 1.8 秒以下、SE 第二世代で 3.5 秒以下に収まっていること
  • AppDelegate の didFinishLaunching 内に新規 await / 同期 dictionaryWithContentsOfURL: 系を入れていないこと
  • package.jsondependencies 差分で、起動経路から呼ばれる重量 SDK が増えていないこと(増えるなら遅延化)

Crashlytics 側では EXC_CRASH (SIGKILL) 0x8badf00d を別アラートに切り出し、リリースごとに 0.05% を超えたら段階配信を止める、というルールにしています。

次にやること

クラッシュログから 0x8badf00d だと確定したら、まず AppDelegate を覗いて、didFinishLaunching から押し出せる SDK 初期化を 1 つ選んで dispatch_async(main_queue, …) に移してください。1 個押し出すだけでも、SE 第二世代クラスでは目に見えてコールドスタートが軽くなります。続けて JS 側の App.tsx から最重量の import を 1 つ遅延読み込みに置き換えると、ウォッチドッグの余白がはっきり広がります。

同じ問題で困っている方の参考になればうれしいです。お読みいただきありがとうございました。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-05-23
Rork 生成プロジェクト特有の expo start --offline forbidden — テンプレ構成が原因の4パターン
Rork が生成するテンプレート構成が原因で expo start --offline が forbidden になる4つのパターン(tsconfigPaths・expo-router キャッシュ・ネイティブプリビルド・lock 不一致)を、Rork プロジェクト固有の観点で切り分けます。汎用的な Expo 側の対処(プロキシ403・依存検証)は別記事にまとめています。
開発ツール2026-03-26
Rork アプリの起動クラッシュ・白画面 完全解決ガイド
Rorkアプリが起動直後にクラッシュしたり、白画面のまま固まる問題を根本から解決。白画面・即座クラッシュ・遅延クラッシュ・リリルビドのクラッシュなど5パターンの原因と対処法を網羅。Xcode・Logcat・React Native Debuggerでの実際のデバッグ手順付き。
開発ツール2026-03-26
Rork の React Native ビルドエラーを切り分ける — Metro・Gradle・Xcode・Expo
Rork で生成した React Native アプリのビルドが通らないとき、どこから疑うか。Metro Bundler・ネイティブモジュールのリンク・Gradle・Xcode・Expo 設定の順に、症状から原因へ辿る診断フローをまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →