- 導入:debugは動くのに、releaseだけ落ちる
- まず前提:R8 は release で有効にするのが基本
- R8/ProGuardクラッシュの典型パターン
- 最短デバッグ手順の全体像
- Step 1:まず mapping.txt を必ず保存する
- Step 2:stack trace は「obfuscated のまま」でもまず回収する
- Step 3:retrace で stack trace を元に戻す
- Step 4:mapping.txt の見方を最低限押さえる
- Step 5:まず keep ルールより「原因の型」を決める
- Step 6:keep ルールは最小単位で足す
- Step 7:release で再確認する(debugで見ても意味が薄い)
- 最近のR8/AGPで意識しておきたい点
- 実務でよくあるハマりポイント
- デバッグ/確認チェックリスト
- よくある質問(Q&A)
- まとめ:R8クラッシュは「元に戻してから考える」が最短
導入:debugは動くのに、releaseだけ落ちる
R8 や ProGuard を有効にした瞬間、急にアプリが落ちる。しかも debug は普通に動く。
これ、Android 開発の中でもかなり嫌な部類のトラブルです。
一番つらいのは、スタックトレースがそのまま読めないことです。
クラス名もメソッド名も潰れていて、「結局どこが悪いの?」になりがちです。
ただ、ここは手順を固定するとかなり楽になります。
結論から言うと、mapping.txt をちゃんと残して、retrace して、keep ルールを最小単位で足す。
この流れに乗せるのが一番速いです。
この記事では、ProGuard/R8 でクラッシュした時の最短デバッグ手順を、mapping の見方を含めて整理します。
先に結論です。
- まず release の stack trace を元の形に戻す
- その上で「何が消されたか」「何が名前変更で壊れたか」を見る
- keep ルールは広く書かず、原因に刺さる最小単位で足す
まず前提:R8 は release で有効にするのが基本
今の Android では、release ビルドで R8 による最適化・難読化・縮小を有効にするのが基本です。
ただし、その分だけ stack trace は読みにくくなります。
R8/ProGuardクラッシュの典型パターン
まず、何で落ちるのかをざっくり分類します。
R8 で壊れる原因はだいたい次のどれかです。
- リフレクションで使うクラスやメソッドが削除された
- ライブラリが必要とする属性や名前情報が消えた
- シリアライズ・JSON変換・DI で名前変更が影響した
- 最適化で前提が変わり、実行時だけ破綻した
特に多いのは、「compile は通るが runtime だけ死ぬ」パターンです。
ここで闇雲に -keep class ** { *; } を足し始めると、あとで収拾がつかなくなります。
最短デバッグ手順の全体像
最初に全体フローを出します。
迷ったらこの順番で進めればOKです。
- release ビルドで本当に再現することを確認する
- stack trace を取る
- mapping.txt を使って retrace する
- どのクラス/属性/ルールが足りないかを見る
- 最小限の keep ルールを追加する
- release で再確認する
Step 1:まず mapping.txt を必ず保存する
R8 デバッグで一番大事なのは、そのビルドに対応する mapping.txt を残すことです。
Android Studio/AGP は mapping を build/outputs/mapping 配下に出力しますが、これは毎回のビルドで上書きされます。
つまり、公開した版ごとに保存していないと、あとで復元できません。
「とりあえずビルドしたら mapping を退避する」
これを習慣にしないと、実機クラッシュが来た時点で詰みます。
mapping.txt のよくある場所
app/build/outputs/mapping/release/mapping.txtflavor を切っているなら variant ごとに場所が変わるので、運用で揃えておくと後が楽です。
Step 2:stack trace は「obfuscated のまま」でもまず回収する
次にやるのは stack trace の回収です。
Crashlytics を使っている場合でも、手元の Logcat でも、とにかく release のクラッシュトレースを確保します。
この時点ではクラス名が a.b.c みたいに潰れていても問題ありません。
まずは材料を集めます。
手元で確認する時の基本
- release ビルドを実機に入れる
- Logcat を Error で絞る
- FATAL EXCEPTION と Caused by を拾う
Crashlytics を使っているなら、deobfuscation 用に mapping のアップロードが重要です。
Step 3:retrace で stack trace を元に戻す
ここが本番です。
R8 には retrace ツールが用意されていて、mapping.txt と stack trace から元のクラス名・メソッド名に戻せます。
基本コマンド例
$ANDROID_HOME/cmdline-tools/latest/bin/retrace \
app/build/outputs/mapping/release/mapping.txt \
trace.txt手元の stack trace を trace.txt に保存して、上の形で流すのが基本です。
verbose を付ける時
$ANDROID_HOME/cmdline-tools/latest/bin/retrace \
--verbose \
app/build/outputs/mapping/release/mapping.txt \
trace.txt
引数型や戻り値まで見たい時は --verbose が便利です。
Step 4:mapping.txt の見方を最低限押さえる
retrace を使えば大半は解決しますが、mapping.txt を直接読めると、keep ルールを書く時に一気に強くなります。
基本形
com.example.feature.LoginUseCase -> a.b:
int retryCount -> a
void execute() -> bこれは、
com.example.feature.LoginUseCaseがa.bに変わった- フィールド
retryCountがaに変わった - メソッド
execute()がb()に変わった
という意味です。
クラス単位・メソッド単位で、何がどう潰れたかが見えます。
mapping を見る時のポイント
- クラッシュしているクラスが「消えた」のか「名前変更で読めなくなった」のか
- フィールド名/メソッド名に依存するライブラリではないか
- JSON 変換や DI で reflection を使っていないか
Step 5:まず keep ルールより「原因の型」を決める
ここでいきなり keep を盛るのは危険です。
先に、どのタイプの問題かを決めます。
パターンA:ClassNotFound / NoSuchMethod / NoSuchField
R8 に削除されたか、名前変更で反射的に見つからなくなった可能性が高いです。
reflection やシリアライザ、DI、WebView の JS ブリッジあたりを疑います。
パターンB:JSON変換失敗
Gson/Moshi/kotlinx.serialization などで、モデルの名前変更やコンストラクタ最適化の影響を受けているケースです。
この場合はモデル全 keep ではなく、対象クラス・対象アノテーション・必要属性を守る方向で詰めます。
パターンC:Crashlytics では読めるが手元だけ読めない
mapping の対応版がズレていることが多いです。
一番よくあるのは「別ビルドの mapping で retrace している」パターンです。
Step 6:keep ルールは最小単位で足す
ここでやっと keep です。
大事なのは、広く守らないことです。
悪い例
-keep class com.example.** { *; }
これ、直ることはあります。
でも「何が原因だったか」が消えますし、R8 のメリットも潰れます。
良い考え方
- 壊れているクラスだけ keep
- 必要なメンバーだけ keepclassmembers
- ライブラリ要件に沿って attributes を keep
例:特定クラスだけ守る
-keep class com.example.auth.LoginResponse例:メンバーだけ守る
-keepclassmembers class com.example.auth.LoginResponse {
<fields>;
}
Crashlytics を使う場合は、可読なクラッシュに必要な属性として SourceFile と LineNumberTable を保持する設定がよく使われます。
例:Crashlytics向けの属性保持
-keepattributes SourceFile,LineNumberTable
-keep public class * extends java.lang.Exceptionただし、これも「何でも keep」ではなく、目的を持って入れるのが基本です。
Step 7:release で再確認する(debugで見ても意味が薄い)
keep ルールを足したら、必ず release で再ビルド・再確認します。
ここで debug だけ見ても意味が薄いです。
壊れているのは release の最適化結果なので、確認対象も release 一択です。
確認ポイント
- クラッシュが消えたか
- 別の場所にクラッシュが移っていないか
- mapping が今回のビルド分として保存されているか
最近のR8/AGPで意識しておきたい点
最近の AGP / R8 では、最適化の既定動作が少しずつ変わっています。
つまり、昔たまたま通っていた keep ルールが、今は足りなくなることがあります。
そのため、古い設定をコピペするだけではなく、
「今どの最適化が効いているか」を前提に見直す方が安全です。
実務でよくあるハマりポイント
1) mapping を保存していない
これが最悪です。
クラッシュ自体は出ているのに、元に戻せない。
release 運用では mapping の保管をフローに組み込んだ方がいいです。
2) Crashlytics に mapping が上がっていない
手元では retrace できるのに、Crashlytics は難読化されたまま。
この場合はアップロード設定を疑います。
3) ライブラリの consumer rules を読んでいない
最近のライブラリは必要な keep ルールを持っていることが多いですが、古いライブラリや独自ライブラリは別です。
R8 側の問題に見えて、実際はライブラリのルール不足ということは普通にあります。
4) keep を広く書きすぎて原因を隠す
一度広く keep すると、「直ったけどなぜか不明」になりやすいです。
そのまま次のクラッシュでまた詰みます。
デバッグ/確認チェックリスト
- release で再現することを確認した
- そのビルドの mapping.txt を保存した
- stack trace を回収した
- retrace で元のクラス/メソッド名に戻した
- ClassNotFound系か、名前変更系か、属性不足系かを分類した
- keep ルールを最小単位で追加した
- release で再確認した
よくある質問(Q&A)
Q. ProGuard と R8、今はどっちを見るべき?
現在の Android では実質 R8 前提で考えるのが自然です。
ただ、設定ファイル形式や mapping は ProGuard 互換の世界観が残っているので、記事やエラー文では両方の名前が出ます。
Q. mapping.txt は毎回保存しないとダメ?
はい。build のたびに上書きされるので、公開版ごとに残しておかないと後から復元できません。
Q. retrace せずに keep を足すのはダメ?
非推奨です。勘で keep を足すと広がり続けます。
まず元の stack trace に戻してから考えた方が圧倒的に速いです。
Q. Crashlytics があれば手元で retrace しなくてもいい?
かなり楽にはなりますが、局所的な検証やその場の release apk での確認では、手元で retrace できた方が強いです。
両方できる状態がベストです。
まとめ:R8クラッシュは「元に戻してから考える」が最短
- まず mapping.txt を保存する
- retrace で stack trace を戻す
- keep は最小単位で足す
迷ったら:そのビルドの mapping.txt があるかを最初に確認する。
そこがないと、最短デバッグ手順そのものが始まりません。

コメント