課題
AIと一緒に、スマホのパズルアプリやタイマーアプリを個人で作っている。iPhone版はいくつか公開していて、10月はAndroid版をGoogle Playに出す準備と、新しいアプリの審査の準備が重なっていた。
その5日ほどの間に、同じ形のつまずきが3回続いた。
自動テストは通っている。エミュレータや実機でも動いている。設定ファイルも見た。なのに、実際に配るビルドだけ中身が違っていた。
解決できるようにすること
「確かめたもの」と「配るもの」が、本当に同じものかを見分けられるようにする。
具体的には、Androidのリリースビルド、ビルドの途中でライブラリの分が合流したマニフェスト、iOSの提出ビルドに入る設定ファイルを、配る前に確かめる手順を持ち帰れるようにする。
先に結論
3回とも、確かめ方そのものは間違っていなかった。確かめていた相手が、配るものとは別物だった。
- 開発中のビルドでは遊べていたアプリが、コードを縮める処理(R8)を通したリリースビルドだと起動直後に落ちた
- 自分で書いたマニフェストには無い広告IDの権限が、ライブラリの分が合流した配る形のマニフェストには入っていた
- 実機での動きは「トラッキングなし」のとおりだったのに、提出ビルドの中にはトラッキング許可の説明文が残っていた
もう1件、テスト用の偽物の部品と本物の部品で、返ってくるタイミングが違っていた件もある。こっちはまだ直し切れていない。
この記事でわかること
- debugビルドでは出ない、Androidのリリースビルドだけで起きる落ち方の例と直し方
- 広告を入れていないアプリでも、広告IDの権限が入ってくることがある理由
- 動きでは確かめられない「ビルドの中身」のずれに、どこで気づけるか
- 配る前に「配るものそのもの」を確かめるための手順
こう思う人もいるかもしれない。
「実機で動くのを確かめたなら、それで十分じゃないの?」
確かめたのが、配るのと同じビルドならそうなる。今回は、そこがずれていた。実機で動かしたのは開発中のビルドで、配るのはリリースビルド。実機で見たのは動きで、審査で見られたのはビルドの中身。確かめた結果は正しくても、確かめた相手が違っていた。
実例1: 遊べていたアプリが、リリースビルドだと起動直後に落ちた
10月7日、準備中の新しいパズルアプリで、ストア用の画像を撮るためにAndroidのエミュレータでリリースビルドを動かした。すると、起動した直後に落ちた。
前の日には、同じアプリをエミュレータで一通り遊べていた。調べていくと、リリースビルドのときだけ起きる落ち方だった。
原因はR8が「使っていない」と判断して消したもの
R8は、Androidのリリースビルドでコードを小さくする仕組み。使われていないと判断したクラスやメソッドを削ってくれる。開発中に使うdebugビルドでは、この処理は動かない。
今回削られていたのは、クラスの名前を頼りに後から作る仕組み(リフレクション)で使われる、引数なしのコンストラクタだった。広告のライブラリが引き込んでいる部品(WorkManager)が、起動時にこのやり方で内部のデータベースを作る。R8から見ると、コードの中で直接呼ばれていないので「使っていない」ように見えて、消されていた。
自分の環境は、Androidのビルドツール(AGP)9.0.1。このバージョンのR8は、削り方が強いフルモードが既定になっていた。Firebaseの部品でも、同じ理由で警告が出ていた(こっちは落ちる原因ではなかった)。
同じ作りの、クローズドテスト中のアプリでも落ちた
ここで気になったのが、調合がテーマの色分けパズルアプリ。iPhone版は公開済みで、Android版はGoogle Playのクローズドテスト(限られた人だけにテスト版を配る仕組み)に出したばかりだった。最初の版は、その日の未明に審査を通っていた。
同じ作り方なので、同じソースと設定でリリースビルドを作って動かすと、やっぱり同じ原因で落ちた。
クローズドテストに出すAABファイル(Google Playへ提出するアプリの形式)を作ったとき、記録に残っている確認は、バージョン番号、対象のAndroidのバージョン、マニフェストの中身、署名まで。リリースビルドを起動した記録は無かった。
テスターの募集はまだ投稿していなかった。でも、もし参加した人がいたら、その端末でも起動直後に落ちていた可能性が高い。Google Play側のリリース前レポートはまだ作られていなかったので、Play側の記録で確かめることはできていない。
実は8月にも、似た落ち方を見ていた
この記事の下書きのためにAIが記録を見直したところ、もう一つ見つかったことがある。
8月24日、同じ色分けパズルアプリの画面を直したとき、エミュレータでリリースビルドを起動しようとして、WorkManagerの初期化で落ちていた。そのときの記録は「このエミュレータ環境固有のクラッシュで、今回の変更とは無関係」として、debugビルドに切り替えて見た目を確かめている。
ビルドツールは、そのときもAGP 9.0.1だった。落ちた場所の書き方も、10月の記録と同じ形をしている。ただ、8月の落ち方が10月と同じ原因だったかは、記録の上では確かめられていない。ここは「同じだったかもしれない」までしか言えない。
それでも、「この環境のせい」と判断して確かめる相手をdebugビルドに替えたことで、配るビルドを確かめる機会を一度手放していた、とは言える。地味だけど、ここが一番持ち帰りたいところだったりする。
直し方と、ほかのアプリへの展開
直し方は、リフレクションで使われるコンストラクタを残すルールを書くこと。android/app/proguard-rules.proに次の2行を置いて、build.gradle.ktsのreleaseの設定で読み込むようにした。
-keep class * extends androidx.room.RoomDatabase { <init>(); }
-keep class * implements com.google.firebase.components.ComponentRegistrar { <init>(); }
これは自分の環境(AGP 9.0.1、広告ライブラリ経由のWorkManager 2.7.0)での例。依存のバージョンが違えば、要るルールも変わるかもしれない。
直したあと、2本ともリリースビルドで落ちずに起動して、警告も消えたことをエミュレータで確かめた。
クローズドテストのほうは、AIが修正版のAABを作り、自分がGoogle Playにアップロードして、審査への送信を承認した。修正版はその日の夜のうちに承認された。
同じ設定を持つ公開中の残り4本にも同じルールを入れて、それぞれのリリースビルドがエミュレータで落ちずに起動することを確かめた。Android版はどれもまだGoogle Playで公開していなかったので、配ってしまった後の修正にはならずに済んだ。
リリース前のチェックリストにも、「Android版を配る前に、エミュレータか実機でリリースビルドを起動して確かめる」を書き足した。
10月9日には、自分の手元のAndroid実機に、クローズドテストから修正版を入れて確かめた。起動直後に落ちず、遊び方の画面もゲームも普通に遊べて、広告はテスト用の広告だった。ここでやっと「配ったものが動く」ところまで確かめられた。
以前になぜCIが成功したのに、実機だけクラッシュしたのか?で、自動のチェックが全部通っても実機で落ちた話を書いた。今回はその一歩先で、実機やエミュレータで動かしていても、動かしたビルドが配るビルドと違えば同じことが起きる、という話になる。
クローズドテストの準備そのものの流れは、Google Playのクローズドテスト準備で、AIの調査を何度も訂正した話。審査送信までの順番と確認のコツにまとめている。
実例2: 広告を入れていないアプリに、広告IDの権限が入っていた
2つ目は、その2日前の10月5日。Google Playの「アプリのコンテンツ」で、広告の有無と広告IDを使うかどうかを申告する準備をしていたときの話。
広告を入れていない、作業の時間を計るタイマーアプリのAndroid版をリリースビルドで作ってみると、マニフェストにcom.google.android.gms.permission.AD_IDという広告IDの権限が入っていた。
マニフェストは、アプリが使う権限などを宣言するファイル。自分たちで書いたマニフェストには、こんな権限は書いていない。
広告を使っていないのにAD_IDとはどういうことか、と自分からAIに聞いた。
ライブラリのマニフェストが、ビルドのときに合流していた
AIがマニフェストの結合の記録(manifest merger report)を調べると、この権限は広告のライブラリではなく、利用状況を記録するFirebase Analyticsの計測ライブラリ(play-services-measurement)が宣言していた。
Androidのビルドでは、アプリのマニフェストに、使っているライブラリのマニフェストが合流して1つになる。自分で書いたファイルに無くても、配る形のマニフェストには入ってくる。同じライブラリからは、ほかに広告関連の権限が2つ(ACCESS_ADSERVICES_AD_ID・ACCESS_ADSERVICES_ATTRIBUTION)も入っていた。
このまま「広告IDは使用しない」と申告すると、配るAABの中身と食い違う。逆に「使用する」と申告すると、今度はiPhone版のプライバシーの申告(識別子を収集しない)や、プライバシーポリシーの書き方と合わなくなる。
「使わない形に揃える」ほうを選んだ
選択肢は2つあった。
- 案A: マニフェストから権限を取り除き、広告IDを使わない形にする
- 案B: 権限は残して、「使用する(目的: アナリティクス)」と申告する
自分は案Aの「広告IDを使わない形に揃える」ほうを選んだ。揃える先として記録に残っているのは、iPhone版の申告と、プライバシーポリシーと、「広告なし」という打ち出し。
AIがアプリのマニフェストで3つの権限にtools:node="remove"を付けて取り除き、Firebaseの公式の設定(google_analytics_adid_collection_enabledをfalse)も足した。リリースAABを作り直して、配る形のマニフェストから3つの権限が消えていること、結合の記録で計測ライブラリ由来のAD_IDが除外(REJECTED)になっていることを確かめた。
ただ、作り直したビルドを起動して、利用状況の記録がちゃんと送られるかまでは、まだ確かめていない。このアプリのAndroid版を出す前に確かめることにしている。
ちなみに、広告を入れているアプリのほうは、広告の申告と広告IDの申告を実画面で済ませてから、クローズドテストを審査に出して承認された。ただ、全部の申告を先に済ませていたので、「申告しないとクローズドテストのリリースを作れないのか」は確かめていない。確かめるには申告を外す必要があるので、試していない。
実例3: 動きは「トラッキングなし」なのに、提出ビルドに説明文が残っていた
3つ目は10月8日、iPhone版の話。準備中の新しいパズルアプリは、最初の版を広告なしで出すことにしていた。広告の仕組みを起動しない設定にして、審査用の候補ビルドを作った。
このビルドは、自分がTestFlight(Appleのテスト配信の仕組み)で入れて確かめた。
- 起動しても、トラッキング許可の画面も広告も出ない
- 広告を見る代わりに使えるようにした機能が、広告なしで使える
- クリアしても広告が出ない
動きとしては、どれも広告なしのとおりだった。審査用のビルドはこれに決めて、App Store Connect(Appleの開発者向け管理画面)に必要な情報を入れていった。プライバシーの回答も、「トラッキングなし」で公開した。
ところが、最後に「審査用に追加」しようとしたところで止められた。App Store Connectには、アプリにNSUserTrackingUsageDescriptionが含まれていて、プライバシーの回答をトラッキングに使うと更新するか、バイナリを更新して新しいビルドをアップロードするように、という趣旨のメッセージが出ていた。
NSUserTrackingUsageDescriptionは、iOSでトラッキングの許可を求めるときに表示する説明文。iOSのアプリの設定ファイル(Info.plist)に書く。広告を入れているアプリ用に書いてあったこの説明文が、広告を止めた版にも残っていた。
実機では、広告の仕組みを起動しないので、許可の画面は出ない。だから動きを見ても気づけない。でもビルドの中には説明文が入っていて、「トラッキングなし」という回答と食い違っていた。App Store Connectは、そこを見ていた。
AIがInfo.plistから説明文を外し(広告を戻すときのために、コメントとして残した)、ビルド番号を上げて作り直した。自分がTestFlightで起動を確かめてから、App Store Connectのビルドを差し替えて、審査へ提出できた。
記事の下書きのためにAIが記録を見直して、気づいたこともある。ビルドの前に確かめるチェックリストには、iOSの設定ファイルに「この説明文が入っていること」を確かめる項目がある。広告を入れたアプリにとっては正しい項目。でも広告なしの版では、確かめる向きが逆で、「入っていないこと」を見る必要があった。
その後に作り始めた別のアプリでは、最初の雛形の段階から、この説明文を入れず、実例1のR8のルールを入れておいた。
まだ直していない1件: テスト用の偽物は、本物より先に「見終わった」と返していた
最後の1件は、まだ途中の話。
10月6日、準備中の新しいパズルアプリをエミュレータで一通り遊んで確かめていたら、見るとご褒美がもらえる広告(リワード広告)を最後まで見ても、ヒントがもらえなかった。
原因は、複数のアプリで共通して使っている広告の部品にあった。広告を表示する関数が、本物の広告のライブラリでは「表示が始まった時点」で戻ってくる。「最後まで見た」という知らせは、その後から届く。なのに関数は戻ってきた時点の結果を返していたので、答えはいつも「まだ見ていない」になっていた。
自動テストでは、この違いが見えていなかった。テストで使う偽物の広告の部品は、呼ばれたらすぐに「見終わった」と知らせて、「見た」と返す作りだったから。偽物と本物で、返ってくる順番とタイミングが違っていた。
新しいアプリのほうは、閉じた・見終わったという知らせを使う形にアプリ側で直して、テスト用の偽物も本物の動きに合わせた。ほかのアプリでは、ご褒美そのものは知らせの中で渡す作りなので、ご褒美が届かないわけではない。
ただ、共通の部品の戻り値の直しは、まだ残っている。ほかのアプリで戻り値に頼っているところがどこまであるかも、これから確かめる。ここは解決済みとは書けない。
3つに共通していたこと

並べてみると、どれも「確かめたもの」と「配るもの」が別物だった。次の表は、記録をもとにAIと一緒に整理したもの。
| 確かめていたもの | 配るもの | 違っていたところ | 気づいたきっかけ | |
|---|---|---|---|---|
| 実例1 | エミュレータで動かした開発中のビルド、AABのマニフェストと署名 | R8を通したリリースビルド | 起動に要るコンストラクタが削られていた | 別の作業でリリースビルドを起動した |
| 実例2 | 自分で書いたマニフェスト | ライブラリの分が合流したマニフェスト | 広告ID関連の権限が3つ入っていた | 申告の準備でAABのマニフェストを見た |
| 実例3 | TestFlightでの動き | 提出ビルドのInfo.plist | トラッキング許可の説明文が残っていた | App Store Connectが審査への追加を止めた |
| 未解決 | テスト用の偽物の広告部品 | 本物の広告のライブラリ | 「見終わった」を返すタイミング | エミュレータで広告を最後まで見た |
記録を並べてAIと整理した見立てでは、気づいたきっかけにも差がある。配る前の確認の流れの中で見つかったのは実例2。実例1は別の作業のついでで、実例3は審査の入り口で止められて、やっと分かった。
もう一つ。配る前に気づけたのは実例2と実例3。実例1は、クローズドテストの最初の版がもう審査を通った後だった。審査を通った日のうちに見つかったので、テスターの募集を始める前に差し替えられた。
配る前に確かめる手順
自分が今回の後で決めたことと、同じ観点で見ておきたいことをまとめると、次のようになる。
- Androidは、配る前にリリースビルドを起動する。debugビルドではR8が動かないので、R8が原因の落ち方は出ない。たとえば
flutter build apk --releaseで作ってadb installで入れ、起動したあとadb logcat -b crashにFATALが出ていないかを見る(自分のチェックリストに書いた確かめ方) - 配る形のマニフェストを見る。自分で書いた
AndroidManifest.xmlではなく、ビルド後のAABやマニフェストの結合の記録で、どの権限が入っているかを見る。ストアでの申告(広告ID、データの扱い)は、この中身と合わせる - 権限を取り除いたら、作り直したビルドでもう一度見る。
tools:node="remove"を書いただけで終わりにせず、配る形のマニフェストから消えたことまで確かめる - iOSは、提出ビルドに入る設定と、ストアの回答を突き合わせる。機能を止めた版では、
Info.plistに止めた機能用の説明文が残っていないかを見る。たとえばgrep NSUserTrackingUsageDescription ios/Runner/Info.plistで残りを探せる - チェックリストは「入っていること」だけでなく「入っていないこと」も。広告あり・なしのように版によって正解が逆になる項目は、どちらの向きで確かめるかを分けて書く
- テスト用の偽物は、本物と同じ順番で返すようにする。本物が「表示開始で戻り、結果は後から届く」なら、偽物もそうする。そのうえで、エミュレータか実機で本物を一度は最後まで通す
- 「この環境のせい」と決める前に、配るビルドで確かめ直す。リリースビルドだけ落ちたときに開発中のビルドへ切り替えて済ませると、確かめる相手が配るものから外れてしまう。切り替えるなら、配るビルドで確かめる宿題を残しておく
2の「ビルド後のAABのマニフェストを見る」は、自分の記録にはどのコマンドで見たかまでは残っていない。Android Studioの「Merged Manifest」の画面や、GoogleのbundletoolでAABのマニフェストを書き出す方法などがある。
こんな人に向いている
FlutterなどでAndroidとiOSの両方にアプリを出そうとしている人に向いている。特に、普段はdebugビルドやシミュレータ、テストの偽物で確かめていて、リリースビルドを動かす機会が少ない人には、他人事ではないはず。
ほかにも、広告を入れていないのに広告IDの申告で迷っている人や、機能を止めた版を出すときに設定ファイルを見直すか迷っている人の判断材料になると思う。
まとめ
テストが通った、実機で動いた、設定ファイルを見た。どれも本当に確かめた結果だった。でも、確かめた相手が配るものと同じとは限らなかった。今回の3件は、どれも「配るものそのもの」を確かめたときに、やっとずれが見えた。
| 項目 | 内容 |
|---|---|
| 結論 | debugビルド、自分で書いたマニフェスト、実機での動き、テスト用の偽物で確かめても、配るリリースビルド・結合後のマニフェスト・提出ビルドの中身・本物の部品とは違うことがあった |
| 教訓 | 「何を確かめたか」と一緒に「それは配るものと同じか」を確かめる。動きでは見えない中身のずれは、中身を直接見る |
| 対象読者 | AndroidとiOSにアプリを出す個人開発者、リリースビルドを動かす機会が少ない人、ストアの申告と設定ファイルの食い違いに不安がある人 |
同じ状況に陥ったときの回避策
- Android版を配る前に、エミュレータか実機でリリースビルドを起動する。debugビルドで動いたことは、リリースビルドが動く根拠にならない
- R8で起動直後に落ちたら、リフレクションで作られるクラスのコンストラクタを
proguard-rules.proで残せないかを見る - 広告IDなどの申告の前に、配る形のマニフェストで権限を確かめる。広告を入れていなくても、計測のライブラリから入ってくることがある
- 機能を止めた版を出すときは、その機能のために書いた説明文や設定が、提出ビルドに残っていないかを見る
- チェックリストの項目が、版によって「入っていること」「入っていないこと」のどちらを確かめるべきかを分けておく
- テスト用の偽物が本物と違う順番で返していないかを見る。本物で一度は最後まで通す
- リリースビルドだけ落ちたときは、「環境のせい」で片づける前に、配るビルドの問題ではないかを疑う
次に読むなら
配るものそのものを確かめられたら、次は配った後に、データやログが本当に届いているかまで見ておくと安心できる。
- GoogleService-Info.plistは置いてあったのに、iOSアプリのFirebaseが約1か月動いていなかった話 — 設定ファイルが「置いてある」のにアプリの中に入っていなかった件と、データが届いているかをストアの数字と並べて確かめる手順が分かる
この記事は、筆者が個人で開発しているモバイルアプリの開発記録(2026年10月5日〜9日。一部8月の記録を含む)を基に、独自調整したAIが下書きを作成し、筆者が事実確認・編集を行った上で公開しています。

