ProGuardで地獄を見た話 〜リリースビルドでユーザーから阿鼻叫喚〜

※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
)

これがリリースビルドでは ab といったクラスやフィールド名に変換され、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 の役割は、

  1. コードの最適化(未使用コード削除、インライン化)
  2. 難読化(クラス名・メソッド名を短縮化)
  3. 圧縮(APKサイズ削減)

です。

しかし Androidアプリはリフレクションやシリアライズ、外部ライブラリに依存することが多く、それらが難読化や削除で壊れるリスクが常に存在します。

「デバッグでは動くのにリリースで壊れる」という最悪の状況が発生するのは、このためです。

教訓

  1. 公式ドキュメントのProGuardルールを必ず確認する
  2. JSONやシリアライズ対象のクラスは難読化しない
  3. リフレクションに頼らない設計を心がける
  4. リリースビルドで必ず実機テストする

まとめ

ProGuard(R8)は、アプリを軽量化しセキュリティを高める強力なツールですが、設定を誤ると「ユーザーから大量のクレームが来る」という地獄に直結します。

私自身、デバッグで完璧に動作していたアプリが、リリースすると即クラッシュという恐怖を何度も味わいました。

いま振り返れば「公式のサンプルルールをちゃんと読んでおけば…」という単純な話なのですが、当時はそれに気づかず、何日も原因を探して苦しむ羽目になったのです。

これを読んでいるあなたが、同じような罠にハマらないことを願っています。

タイトルとURLをコピーしました