ANTIGRAVITY LABEN
記事一覧/アプリ開発
アプリ開発/2026-06-22上級

リリースビルドだけで落ちる — R8 フルモードが Gson のマッピングを壊したときの調べ方と直し方

デバッグでは動くのにリリースだけでクラッシュする。R8 フルモードが Gson のリフレクションを壊す典型と、mapping.txt を使った原因特定、keep ルールの最小化までを実装視点でまとめます。

android22r8proguardgsonrelease-build

プレミアム記事

ある朝、Crashlytics に見覚えのない NullPointerException が並んでいました。スタックトレースの一番上は a.b.c.d のような難読化された記号だけで、自分が書いた覚えのないクラス名です。手元の端末でデバッグビルドを動かしても、まったく再現しません。リリースして数時間後、特定の画面を開いたユーザーだけが落ちている——個人開発で広告付きの壁紙アプリを長く運用してきた中で、この「リリースビルドだけで落ちる」種類のクラッシュには何度か足をすくわれてきました。

原因はたいてい同じ場所にあります。R8 のフルモード難読化が、Gson がリフレクションで読み書きしているデータクラスのフィールド名を書き換えてしまい、JSON との対応が取れなくなっているのです。デバッグビルドは難読化が無効なので何事もなく動き、リリースでだけ壊れる。だから再現が難しく、原因にたどり着くまでに時間を溶かします。

私自身が実際にこの問題を踏んだときの調べ方と、最終的にどう直したかを書き残しておきます。「とりあえず全部 keep すれば消える」というのは事実ですが、それは設計判断を放棄しているだけで、難読化の恩恵もアプリサイズの縮小も同時に捨てています。壊れたクラスだけを最小限に守る、というところまで一緒に詰めていきます。

なぜデバッグでは起きずリリースでだけ落ちるのか

Android のリリースビルドでは、minifyEnabled true のときに R8 がコードの圧縮(未使用コードの削除)と難読化(クラス名・メソッド名・フィールド名の短縮)を行います。AGP 8.0 以降は R8 が標準のコードシュリンカーで、さらに「フルモード」が既定で有効になりました。フルモードは旧 ProGuard 互換モードより踏み込んだ最適化を行い、keep ルールで明示的に守られていないものは、より積極的に削除・改名します。

問題は、Gson がフィールドを「名前」で突き合わせている点です。{"display_name": "夕焼け"} という JSON を data class Category(val displayName: String) にマッピングするとき、Gson は実行時にリフレクションでフィールド名を読み、そこにアノテーションがなければフィールド名そのものを JSON キーとして使おうとします。ところが R8 がそのフィールドを a という名前に書き換えてしまうと、Gson から見えるのは a であって displayName ではありません。結果、JSON のキーと一致せず、その値は埋まらないまま null になります。

デバッグビルドでは minifyEnabled false なのでフィールド名は元のまま残り、何も壊れません。この非対称性が、再現の難しさの正体です。コードを書いた本人が手元で動かす限り、まず再現しないのです。

手元でリリースの挙動を再現する

原因を推測で潰すと時間がかかります。まず、ローカルでリリースビルドの挙動を確実に再現させてしまうのが近道です。

# リリースビルドを生成して実機/エミュレータに入れる
./gradlew assembleRelease
 
# 署名済みリリース APK をインストール(debuggable でなくても adb install は通る)
adb install -r app/build/outputs/apk/release/app-release.apk

これで手元でも本番と同じ難読化済みコードが動きます。「リリースだけで落ちる」と聞くと本番でしか調べられない気がしますが、assembleRelease を一度回すだけで、自分の端末で何度でも再現できる状態に持ち込めます。私はここを飛ばして本番ログとにらめっこしていた時期があり、それが一番の時間の無駄でした。

落ちる画面を開いてクラッシュさせ、adb logcat でスタックトレースを取ります。ただしこの時点のスタックトレースはまだ難読化された記号のままなので、次に元の名前へ戻す作業が必要になります。

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

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
デバッグでは再現せずリリースだけで落ちるクラッシュを、ローカルで確実に再現させる手順を持ち帰れる
R8 フルモードが Gson のリフレクションを壊す仕組みを理解し、mapping.txt と retrace で難読化済みスタックトレースを復元できる
「全部 keep」で逃げずに、壊れたクラスだけを最小限の keep ルールで守る設計判断を自分のアプリに適用できる
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

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

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

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

関連記事

アプリ開発2026-06-22
壁紙ビューアのスライドショーとページ送りスクラバーを iOS から Android へ逆移植する — SnapHelper との双方向同期でつまずいた点
iOS 版の全画面壁紙ビューアにあったスライドショーと下部スクラバーを Android へ逆移植した実装記録。RecyclerView と SnapHelper で現在ページを確定させ、スクラバーと双方向同期し、自動送りとユーザー操作の競合を状態機械で解いた手順を Kotlin の動くコードで共有します。
アプリ開発2026-06-21
戻るボタンで広告が出る/出ないが安定しない — 入れ子のifを独立ガードのリストに作り替えた記録
戻るボタン押下時のインタースティシャル表示判定が、入れ子のif文で優先度が暗黙化して壊れていました。各条件を理由つきの独立ガードに分解し、決定表からテストを生成した設計をまとめます。
アプリ開発2026-06-17
ダイアログが重なる前に止める — 課金・レビュー誘導・リワード広告を1か所のゲートで束ねる
ペイウォール・レビュー誘導・リワード広告が同じ瞬間に重なって出る不具合を、優先度つきの中央ゲートで根治した実装記録です。Antigravityのエージェントに散らばったshow()呼び出しの掃き出しを任せ、表示ポリシーは自分で握る線引きで進めました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →