※ChatGPTを使用して記事を作成しています。
開発がある程度進み、「よし、そろそろリリースしてみよう」と思ったときに待っている罠があります。
それが ProGuard / R8 です。
開発環境のデバッグビルドでは問題なく動いていたのに、リリースビルドにした途端にクラッシュ。
ユーザーから「アプリが起動しません」「データが消えました」と問い合わせが殺到…。
今回は、私が実際に経験した ProGuardで地獄を見た話 をケーススタディ形式で紹介します。
ケース1: JSONパースが突然動かなくなった
❌ 失敗例
当時、私は Gson を使ってサーバーからのレスポンスをパースしていました。
デバッグビルドでは問題なかったのに、リリースするとこんなクラッシュが発生しました。
java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING
原因は ProGuardがフィールド名を難読化した ためです。
Gsonはデフォルトでクラスのフィールド名をそのままJSONキーにマッピングするため、難読化されるとマッピングが崩壊します。
data class User( val id: String, val name: String )
これがリリースビルドでは a
や b
といったクラスやフィールド名に変換され、JSONとの対応が取れなくなったのです。
✅ 修正版
ProGuardルールに -keep
を追加し、対象クラスを難読化しないようにしました。
# Gsonモデルは難読化しない -keep class com.example.app.model.** { *; }
ケース2: 反射が動かない問題
❌ 失敗例
一部の処理でリフレクションを使っていました。
たとえばDIライブラリやカスタムフレームワークです。
val clazz = Class.forName("com.example.app.service.MyService") val instance = clazz.newInstance()
デバッグでは動くのに、リリースすると ClassNotFoundException
が出ます。
理由は単純で、ProGuardがクラス名を変えてしまったためです。
✅ 修正版
リフレクションで利用するクラスは保持するようにしました。
-keep class com.example.app.service.MyService
また、最近はなるべくリフレクションに頼らず、依存性注入フレームワーク(HiltやKoin) を使うことで回避できるようになりました。
ケース3: シリアライズの地獄
❌ 失敗例
古いコードで Serializable
を使ってオブジェクトを保存していました。
しかしリリースビルドでは serialVersionUID
が食い違い、デシリアライズに失敗。
val user = User("123", "Taro") // 保存 val fos = context.openFileOutput("user.dat", Context.MODE_PRIVATE) ObjectOutputStream(fos).use { it.writeObject(user) } // 読み込み val fis = context.openFileInput("user.dat") val restored = ObjectInputStream(fis).use { it.readObject() as User }
リリース後、ユーザーから「ログイン情報が毎回消える」とクレームが殺到しました。
理由は、ProGuardがクラス名を変えたためにシリアライズしたデータを復元できなかった からです。
✅ 修正版
解決策は2つありました。
1. ProGuardでクラスを保持する
-keepclassmembers class com.example.app.model.User {
static final long serialVersionUID;
private *;
public *;
}
2. そもそも Serializable
を捨てて、安全な保存方法(DataStoreやRoom) に移行する。
私は後者を選びました。
今では Serializable
を新規開発で使うことはありません。
ケース4: ライブラリ更新での大爆発
❌ 失敗例
あるとき、外部ライブラリを更新したら突然クラッシュが多発しました。
原因は、ライブラリ内のアノテーションプロセッサが生成するクラスをProGuardが消していたこと。
特に RoomやHilt のようなコード生成系ライブラリは注意が必要です。
✅ 修正版
公式ドキュメントにある ProGuard / R8 設定を必ず確認し、必要なルールを追加しました。
例えば Room なら:
# Room Database -keep class androidx.room.** { *; } -dontwarn androidx.room.**
技術的背景
ProGuard / R8 の役割は、
- コードの最適化(未使用コード削除、インライン化)
- 難読化(クラス名・メソッド名を短縮化)
- 圧縮(APKサイズ削減)
です。
しかし Androidアプリはリフレクションやシリアライズ、外部ライブラリに依存することが多く、それらが難読化や削除で壊れるリスクが常に存在します。
「デバッグでは動くのにリリースで壊れる」という最悪の状況が発生するのは、このためです。
教訓
- 公式ドキュメントのProGuardルールを必ず確認する
- JSONやシリアライズ対象のクラスは難読化しない
- リフレクションに頼らない設計を心がける
- リリースビルドで必ず実機テストする
まとめ
ProGuard(R8)は、アプリを軽量化しセキュリティを高める強力なツールですが、設定を誤ると「ユーザーから大量のクレームが来る」という地獄に直結します。
私自身、デバッグで完璧に動作していたアプリが、リリースすると即クラッシュという恐怖を何度も味わいました。
いま振り返れば「公式のサンプルルールをちゃんと読んでおけば…」という単純な話なのですが、当時はそれに気づかず、何日も原因を探して苦しむ羽目になったのです。
これを読んでいるあなたが、同じような罠にハマらないことを願っています。