【android】まれにレビューで指摘するParcelableについて
時差ボケからの早起きになってしまい、朝の時間に余裕ができたので小ネタを書いてみます。
Parcelableに関する全く同じ指摘をいくつかのプロジェクトでやってきました。
自分が見てきたプロジェクトでは、下記の条件を満たす場合はほぼ100%の確率で1回以上指摘しています。
- kotlinを使っている
- Parcelableをライブラリとか使わずに自分らで頑張ってる
この記事の結論はライブラリ使えってことではなく、ここを気をつけようって話になります。
テストには出ないエクササイズ
data class TestData( val num: Int, val str: String = "test" ) : Parcelable { constructor(source: Parcel) : this( source.readInt() + source.readInt() // ← ここ注目 ) override fun describeContents() = 0 override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { writeInt(num) writeString(str) } companion object { @JvmField val CREATOR: Parcelable.Creator<TestData> = object : Parcelable.Creator<TestData> { override fun createFromParcel(source: Parcel): TestData = TestData(source) override fun newArray(size: Int): Array<TestData?> = arrayOfNulls(size) } } }
こんなクラスがあるとします。 ここ注目 という箇所に注目ください。
この時に TestData(4, "hi")
でインスタンスを作りIntentにputして遷移先でgetした時、num
とstr
の値はどうなるでしょうか??
プロダクションコードでこんなのを書いてきたら困惑ものですが、遊びと思って考えてくださいませ。
ちなみに、ここで「なーんだ」とわかる人はこの先を読む必要がありません。
回答は最後にします。
よく指摘するケース
さて上記の回答&解説をする前に、よく指摘するケースも見ておきます。
data class TestData( val num1: Int?, val num2: Int?, val str: String ) : Parcelable { constructor(source: Parcel) : this( source.readInt(), source.readInt(), source.readString() ) override fun describeContents() = 0 override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { num1?.let { writeInt(it) } num2?.let { writeInt(it) } writeString(str) } companion object { @JvmField val CREATOR: Parcelable.Creator<TestData> = object : Parcelable.Creator<TestData> { override fun createFromParcel(source: Parcel): TestData = TestData(source) override fun newArray(size: Int): Array<TestData?> = arrayOfNulls(size) } } }
このコードには致命的な問題があります。わかりますでしょうか?
java.lang.IllegalStateException
が発生したりします。
解説
イメージの話です。Parcel
には直列的に値が積まれていきます。
TestData(1, 2, "aaa")
の時、 Parcel
には 1, 2, 3, a, a, a
という風に値が積まれていきます。 3
は文字列のlengthですね。
この Parcel
から取得する時も直列的に取得していきます。
- 1回目の
readInt()
でnum1
に1
が入る - 2回目の
readInt()
でnum2
に2
が入る。 readString()
で 初めの3, a, a, a
を取得。サイズを取得してその後ろのサイズ分getする感じです。これがstr
に入る。
この場合はちゃんと動きますね。
では TestData(null, 2, "aaa")
の時を考えましょう。
num1?.let { writeInt(it) } num2?.let { writeInt(it) } writeString(str)
となっているので、初回の writeInt
は走りません。そのため Parcel
には 2, 3, a, a, a
という値が積まれます。
これをgetするとどうなるでしょうか?
- 1回目の
readInt()
でnum1
に2
が入る。(ここですでに間違ってますね) - 2回目の
readInt()
でnum2
に3
が入る。 (これも正しくない) - 3回目の
readString()
時にサイズが取れず、 IllegalExceptionが発生する (😇)
こうなります。ダメダメです。
では、これを修正してます。
override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { writeInt(num1 ?: 0) writeInt(num2 ?: 0) writeString(str) }
で、いいじゃんと思う方がいるかもしれませんが、これも厳密には間違っています。
TestData(null, null, "aaa")
が TestData(0, 0, "aaa")
となってしまうからです。
修正方法は単純でnull
かどうかのフラグもParcel
に詰めてやります。(他にやり方あったら教えてください)
: constructor(source: Parcel) : this( if (source.readInt() == 1) source.readInt() else null, if (source.readInt() == 1) source.readInt() else null, source.readString() ) override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { writeInt(if (num1 != null) 1 else 0) if (num1 != null) { writeInt(num1) } writeInt(if (num2 != null) 1 else 0) if (num2 != null) { writeInt(num2) } writeString(str) } :
こうすることで取得する時に対象がずれることがなくなります。
一件落着。
"テストには出ないエクササイズ" の回答&解説
回答は num=6, str="test"
となります。
ここまでくれば解説もいらないと思います。
TestData(4, "hi")
となっている時、 4, 2, h, i
と積まれています。
data class TestData( val num: Int, val str: String = "test" ) : Parcelable { constructor(source: Parcel) : this( source.readInt() + source.readInt() // ← ここ注目 ) : }
pos=0の4 と pos=1の2を足したものが num
、 str
はデフォルト値となるので、TestData(4+2, "test")
となるわけです。
結論
自分でParcelable
を実装する時は値が積まれていくのをイメージすべし!
ちなみに、javaのときは int
はプリミティブで null
になることはなかったし、 Integer
使ってたとしてもその場合は writeValue
/ readValue(class loader)
使えば null
時もいい感じに動くので、こういうミスに遭遇する人は少なかったんでしょうね。
追記
修正コードについて、Int?
に関しては writeValue
を使うことでも問題なく動きます!なので、この方法でも良いです。
ただ個人的には、雑にやってるとランタイムにjava.lang.RuntimeException: Parcel: unable to marshal value
で落ちるのでなるべく使わないほうがいいのかなという考えです。
くろかわさんから質問いただきました!
自分が勘違いしているのかもしれませんが、修正版で writeValue を使わないのって、なぜなんでしょうか?
— Hiroshi Kurokawa (@hydrakecat) 2018年5月15日
酒の席では思い出せなかった回答。
あ、これ使わない理由一つあります!(酔っ払って思い出せてなかった😂)
— 有象無象 (@kgmyshin) 2018年5月15日
writeValueは引数がAnyになっちゃってRuntimeで落ちる可能性がでてくるので、なるべく使いたくないなと思って避けてた次第です!