TL;DR
- lazyを使ったdata classをシリアライズする処理を含んだAndroidアプリをR8でminifyすると落ちるよ
- Proguardのルールに追加すれば落ちなくなるよ
-keep class kotlin.SynchronizedLazyImpl { *; }
環境
- Android Studio3.4.1
- Kotlin 1.3.31
- Android 9
data classとlazy
APIからのレスポンスをオブジェクトとして落とし込むためにdata classを作成します。
data class User(
val familyName: String,
val givenName: String
)
アプリで表示する時はフルネームで表示することが多いので、姓と名をくっつけてくれるfullNameというプロパティが生えていると便利そうです。
fullNameの書き方はいくつかあります。
(1)
val fullName = "$familyName $givenName"
この書き方が一番シンプルです。Userインスタンスが生成されたタイミングでfullNameに値が代入されます。
(2)
val fullName: String
get() = "$familyName $givenName"
get() =
という書き方は値取得時に毎回計算されます。fullNameを表示するために毎回名前を組み立てることになります。
(3)
val fullName: String by lazy { "$familyName $givenName" }
lazyを使うとfullNameが必要になったタイミングで1回だけ処理が走り、以降は結果だけを返します。
fullNameを必要としない場合は計算されず、また何度も呼ばれても初めの結果をそのまま返すので計算量が減ります。
さて、このクラスが非常に便利なのでActivity間でインスタンスごと渡したいとします。
IntentはSerialiableを受けつけてくれます。
data class User(
val familyName: String,
val givenName: String
) : Serializable {
val fullName: String by lazy { "$familyName $givenName" }
}
送る時はputExtraにそのまま渡すことができます。
val user = User("Qiita", "Taro")
Log.i("MainActivity", user.fullName) // Qiita Taro
val intent = Intent(this, DetailActivity::class.java).apply {
putExtra("user", user)
}
startActivty(intent)
受け取る時は型をキャストします。
val user = intent.getSerializableExtra("user") as User
Log.i("MainActivity", user.fullName) // Qiita Taro
このコード、デバッグだと動くのですが、いざProguardをかけてビルドするとなんと実行時に落ちてしまいます。
原因
スタックトレースを見ると、
java.io.NotSerializableException: b.i
という謎のメッセージが。
普通シリアライズ対象のクラスはデシリアライズできるようProguardの対象から外します。
-keep class com.example.sample.** { *; }
しかしそれでもNotSerializableExceptionが発生してしまいます。
つまりシリアライズできない要素がUserクラスにあるということです。
(1)と(2)の書き方では問題ないので原因はlazyにあるのではないかと推測しました。
そこでlazyの実装を辿ってみると kotlin.SynchronizedLazyImpl
というクラスを見つけました。
package kotlin
public actual fun <T> lazy(lock: Any?, initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer, lock)
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
// 実装内容は省略
}
この関数がProguardによって難読化された結果うまくシリアライズできないのであれば、このクラスをProguardから外せばうまくいきそうです。
-keep class com.example.sample.** { *; }
-keep class kotlin.SynchronizedLazyImpl { *; } # 追加
実行すると問題なく動作できました。