iOSアプリをTestFlightで配布したら、デバッグでは正常なのにリリースだけ起動直後にクラッシュ——ログにはAsyncMethodBuilderCore.Startが出る。原因として最も多いのはリンカー(トリミング)設定です。本記事では切り分け手順と、リンク無効化で止める方法、サイズを増やさず直す恒久対策までまとめます。
発生していた症状(TestFlightのリリースビルドだけ落ちる)
状況を整理すると、典型的な「リリース限定クラッシュ」のパターンでした。
- Visual Studio から iOS 向けにビルドし、TestFlight 経由で iPad にインストール
- 起動後 1〜2 秒で必ずクラッシュ(再現率100%)
- デバッグビルドではクラッシュしないが、リリースビルドでのみクラッシュ
- クラッシュログ(スタックトレース)に、次のような並びが見える
MyClass.Update(...GameTime)(ゲームループのUpdate)AsyncMethodBuilderCore.Start<...>()(asyncメソッドのステートマシン開始付近)-[iOSGameView doTick](iOS側の描画/更新ループ)
- Program.cs にあるプッシュ通知関連コードを外しても落ちるため、原因箇所の特定が難しい
- TestFlight(=配布用)でのログ取得方法が分からず、切り分けが進まない
| 観点 | デバッグビルド | リリースビルド(TestFlight) |
|---|---|---|
| 起動 | 正常 | 起動後すぐクラッシュ |
| 最適化 | 弱い(変数/例外が追いやすい) | 強い(最適化/AOT/リンクが効く) |
| リンカー(トリミング) | 無効〜弱めのことが多い | 有効(未使用コードが削られる) |
| 原因の出方 | 例外が見える/デバッガで止まる | 必要な型/メソッド欠落が「突然クラッシュ」に見えやすい |
「AsyncMethodBuilderCore.Start」が出ている=非同期処理が悪い、とは限らない
AsyncMethodBuilderCore.Start は、C# の async/await をコンパイラがステートマシンに変換した際に使われる“開始処理”の一部です。スタックに出てくると「非同期処理で落ちている?」と疑いがちですが、実際は次のケースがよくあります。
- 本当の原因は別にあり、非同期メソッドが開始された直後に例外が発生して表面化している
- 最適化やリンクの影響で、例外位置が分かりにくい形で出る(Updateの近辺に見える)
- “落ちる場所”と“壊れた原因”が一致しない(削られた型/メソッドが呼ばれた瞬間に破綻)
つまり、スタックに AsyncMethodBuilderCore が見えていても、非同期処理そのものを疑う前に「リリース限定で有効になる仕組み(リンカー/最適化/AOT/設定差分)」を先に当たる方が早いことが多いです。
リリースだけ落ちる代表的な原因と、優先度の高い当たりどころ
iOS(特にTestFlightの配布用IPA)は、デバッグとは別世界だと思った方が安全です。特に .NET 系(Xamarin.iOS / .NET for iOS / MonoGame など)では、リリース構成で次の差分が一気に効いてきます。
| 原因候補 | よくある症状 | 切り分けの最短手 | 代表的な対処 |
|---|---|---|---|
| リンカー(トリミング) | デバッグOK/リリース即死、反射・DI・シリアライズ・動的ロード周りで顕在化 | Releaseのリンクを一時的に無効化して比較 | Don't link で回避 → Preserve/link.xmlで恒久化 |
| AOT/LLVM最適化差分 | 特定端末だけ、特定処理だけで落ちる。タイミング依存の不具合が増える | LLVMを一時OFF、最適化レベル変更 | 再現条件を絞って修正/ライブラリ更新 |
| ビルド構成の設定差(Entitlements/署名/環境差分) | 起動直後に落ちる・権限周りで落ちる。プッシュ通知やバックグラウンドなど | 署名とEntitlementsを比較 | プロビジョニング/Capabilitiesの整合 |
| 例外が握り潰されている | デバッグではメッセージが見えるが、リリースは即クラッシュに見える | ログ出力を強化、クラッシュログの取得 | 例外の捕捉と可視化、シンボル/ログ整備 |
今回のケースは、結果的にリンカー(Linker / トリミング)が原因でした。ここに当たりを付けられると、切り分けが一気に進みます。
結論:Releaseのリンカー設定を変更したらクラッシュが止まった
対処:Release構成のリンカー動作を「リンクしない(Don’t link)」に変更したところ、TestFlight配布版でも起動直後クラッシュが解消しました。
「リリースだけ落ちる」問題の中でも、このパターンは本当に多いです。特に次のような要素が入っているプロジェクトは要注意です。
- JSONシリアライズ(Newtonsoft.Json / System.Text.Json など)
- DI(依存性注入)コンテナ
- リフレクションや属性スキャン
- プラグイン方式(動的に型を探索して呼ぶ)
- ゲーム系(Update/Drawで初回にまとめて初期化する)
対応手順(Visual Studioでの設定変更)
Visual Studio のUIはバージョンやプロジェクト種別で表記が少し変わりますが、基本は次の流れです。
- iOSプロジェクトを右クリックし、プロパティを開く
- iOS > Build(ビルド)(または同等の設定画面)へ移動
- 上部で Configuration: Release になっていることを確認
- Linker behavior(リンカー動作) を
Link Framework SDKs Only(SDKのみリンク)Don't link(リンクしない)
- いったん Clean(可能なら
bin/objを削除)してから、リリースビルドを作り直す - TestFlightにアップロードして、同じiPadで再インストール → クラッシュが止まるか確認
UI操作ではなく .csproj で管理したい場合は、プロジェクト種別によってプロパティ名が異なりますが、Xamarin.iOS 系では概ね次のような指定になります(例)。
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|iPhone'">
<MtouchLink>None</MtouchLink>
</PropertyGroup>
| Linker behavior | 挙動 | メリット | デメリット |
|---|---|---|---|
| Don’t link | リンク(トリミング)しない | 原因切り分けが速い。反射/動的処理で落ちにくい | アプリサイズ増、起動/メモリに影響が出ることがある |
| Link Framework SDKs Only | SDK側のみリンク | サイズ削減と安全性のバランスが良い | ライブラリ側の反射が絡むと落ちることがある |
| Link All | SDK+アプリ側も強くリンク | サイズは最小になりやすい | 反射/動的処理があると最も事故りやすい |
なぜ「リンクを無効化」すると直るのか(リンカーが必要なコードまで削る)
リンカー(Linker / トリミング)は、静的解析によって「使われていない」と判断したコードを削除し、アプリを軽くする仕組みです。ところが iOS アプリでは次の理由で誤判定が起きやすく、リリースだけクラッシュする原因になります。
- リフレクションで型を探して生成している(コード上の参照が見えない)
- 属性や命名規約でメソッドを呼び出す(静的解析が追えない)
- ライブラリ内部で遅延初期化され、初回呼び出しまで参照が現れない
- ゲームループの初回Updateでまとめて初期化しており、起動直後に一気に発火する
デバッグではリンクが弱い(あるいは無効)ため残っていたコードが、リリースでは削られ、実行時に「あるはずの型/メソッドがない」状態になって落ちる……という流れです。スタック上は Update や AsyncMethodBuilderCore.Start が見えますが、根っこは“欠落”であることが多いです。
まずは「A/Bテスト」として使う:Don’t linkで止まれば原因はほぼ確定
本番運用としてずっと Don't link を使うかどうかは別として、切り分けとしては非常に強力です。
- Don’t linkでクラッシュが止まる → リンカー起因の可能性が極めて高い
- 止まらない → リンカー以外(署名/Entitlements、AOT最適化、端末依存、別の例外など)を疑う
「原因箇所の切り分けが難しい」「Program.csの通知コードを外しても落ちる」という状況でも、リンクのON/OFFはアプリ全体に効くため、最初の大きな分岐としておすすめです。
恒久対策:リンクは活かしつつ、削られて困るものだけ保持する
Don't link は手早い反面、IPAサイズの増加やロード時間の悪化につながることがあります。最終的には、リンクを有効に戻しつつ、必要な型/メンバーだけを「消さない」指定を入れるのが安定します。
Preserve属性で守る(反射で参照される型に付ける)
Xamarin.iOS 系では Foundation.Preserve を使えることが多いです。反射やシリアライズで使われる型、DIで生成される型などに付与します。
using Foundation;
namespace MyApp;
[Preserve(AllMembers = true)]
public class MyDto
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
}
ポイントは次の通りです。
- AllMembers=true:プロパティ/コンストラクタを含めて削られにくくする
- 「どれが消されたか分からない」段階では、まず広めに保護して再現を止め、その後に範囲を絞る
link.xml(リンク設定ファイル)で守る(まとめて指定したい場合)
プロジェクトに link.xml を追加し、リンカーに「この型(またはアセンブリ)は残して」と指示できます。代表例は次の形です。
<linker>
<assembly fullname="MyApp">
<type fullname="MyApp.MyClass" preserve="all" />
</assembly>
Visual Studio側では、このファイルのビルドアクションが重要です(例:LinkDescription)。設定が漏れると、ファイルを書いても効かず「直らない」と迷子になりがちです。
「まずアセンブリ丸ごと保持 → 徐々に狭める」が現場では速い
どの型が削られているかを一発で当てるのは難しいため、実務では次の進め方が効率的です。
- リンクは
Link Framework SDKs Onlyに戻す(=アプリは原則リンクしない) - それでも落ちるなら、疑わしい外部ライブラリを アセンブリ単位で保持する(link.xmlで指定)
- 止まったら、保持範囲を型→メンバーへと狭める
“犯人のライブラリ”が分かるだけでも大きな前進で、以降は必要な型だけをピンポイントで守れるようになります。
TestFlight配布版でのログ取得・クラッシュログ収集の現実的なやり方
リリース構成(特にTestFlight)はデバッグのように簡単にログを追えませんが、現場で使える手段はいくつかあります。まずは「クラッシュログを取り、シンボル化できる状態」を作るのが最優先です。
| 手段 | 何が取れるか | メリット | 注意点 |
|---|---|---|---|
| App Store Connect(TestFlightのクラッシュ) | クラッシュ統計・ログ | TestFlight特有の問題を直接追える | 反映に時間差がある。dSYM未整備だと読みにくい |
| Xcode Organizer / Devices and Simulators | 端末ログ・クラッシュログ | 手元のMacで確認できる | Macが必要。シンボルがないと関数名が出にくい |
| macOS「Console」アプリ(端末接続) | 実行中のログ(NSLog等) | 「起動直後に何が起きたか」を追いやすい | 端末をUSB接続する必要がある |
| アプリ内ログ(ファイル保存) | 任意のログ(起動時の状態、例外など) | ユーザー環境でも取れる | 個人情報や機密を出さない設計が必須 |
クラッシュログを「読める形」にする:dSYM(シンボル)を意識する
クラッシュログにメソッド名が出るかどうかは、シンボル(dSYM)が揃っているかで大きく変わります。Visual Studioで配布用アーカイブを作る場合も、シンボルを生成/保管しておくと、後から原因特定が一気に楽になります。
- 配布用ビルドごとに、対応する dSYM を必ず保管する
- 可能なら App Store Connect に dSYM をアップロードし、TestFlightのクラッシュをシンボル化する
「スタックは出ているのに原因箇所が見えない」場合、実はシンボル不足で“肝心の行”が潰れていることがよくあります。
ゲームループ(Update)で表面化しやすい理由
クラッシュログに -[iOSGameView doTick] や Update(GameTime) が出るタイプのプロジェクト(MonoGameなど)では、起動直後に初期化処理が集中しがちです。
- 起動後すぐにゲームループが回り、初回Updateで大量の初期化を行う
- 反射でコンテンツをロードする、JSONから設定を読む、DIでサービスを立ち上げる、などが同時多発する
- その結果「削られた型/メソッドに最初に触れる場所」がUpdate近辺になり、スタックがそこに集まる
このため、Updateやasyncの行をいくら眺めても、真因は「別の場所にある削除(トリミング)」ということが起こります。今回のようにリンカー設定で止まるなら、Update自体のロジックよりも“削られてはいけないもの”の特定に舵を切るのが正解です。
再発防止のチェックリスト(リリース前に見る項目)
| チェック項目 | 狙い | 具体策 |
|---|---|---|
| Releaseのリンク設定を把握する | 「リリースだけ落ちる」を未然に防ぐ | Linker behaviorを明示し、必要ならlink.xml/Preserveを用意 |
| 反射/シリアライズ/DIの有無を洗い出す | リンカーの誤判定ポイントを先に潰す | 初期化コードや外部ライブラリの機能を棚卸しする |
| TestFlightで必ず実機検証する | 配布形態の差分を踏む | 手元インストールOKでもTestFlightで落ちるケースを想定 |
| dSYMを保管/アップロードする | クラッシュログ解析の精度を上げる | ビルドごとにdSYMを保存し、可能ならApp Store Connectへ |
| NuGet/SDKを最新化する | リンク関連の既知不具合を避ける | リリース前に更新を検討(破壊的変更には注意) |
まとめ
iOS(TestFlight)で「リリースビルドだけ起動直後にクラッシュし、AsyncMethodBuilderCore.Start がスタックに見える」場合、まず疑うべきはリンカー(トリミング)です。Releaseの Linker behavior を一時的に Don't link にしてクラッシュが止まるか確認すると、原因を大きく絞れます。止まったら、最終的には Preserve や link.xml で必要な型だけを保持し、サイズと安定性のバランスを取るのが現場で強い解決策です。

コメント