LoginSignup
8
5

More than 3 years have passed since last update.

Android / Kotlinのlazyを使ったdata classをシリアライズすると落ちてしまう

Last updated at Posted at 2019-06-01

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の対象から外します。

proguard-rules.pro
-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から外せばうまくいきそうです。

proguard-rules.pro
-keep class com.example.sample.** { *; }
-keep class kotlin.SynchronizedLazyImpl { *; } # 追加

実行すると問題なく動作できました。

8
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5