Slide 1

Slide 1 text

Serializable / Parcelableとの 上手な付き合い方 @kobaken

Slide 2

Slide 2 text

Index ● Serializable / Parcelableとは ● pixivコミックで問題になった事象 ● なぜ発生してしまったのか ● 今後の運用方針 ● まとめ 2

Slide 3

Slide 3 text

Serializable / Parcelable 3

Slide 4

Slide 4 text

4 https://developer.android.com/develop/ui/views/touch-and-input/stylus-input/ink-api-state-preservation

Slide 5

Slide 5 text

5 https://developer.android.com/develop/ui/views/touch-and-input/stylus-input/ink-api-state-preservation

Slide 6

Slide 6 text

6 🤔💭

Slide 7

Slide 7 text

Serializable / Parcelableとは ● オブジェクトを直列化するためのinterface ○ オブジェクトをバイト列に変換(シリアライズ)、復元(デシリアライズ)する処理 ● ActivityやFragment, Serviceなどのコンポーネント間のデータ受け渡し(画面遷移 など)や永続化などの際に利用されることが多い 7

Slide 8

Slide 8 text

Serializable 8 Java標準のシリアライズ機構 ※Kotlin Serializationの@Serializableではないです Pros ● Serializableと追加するだけなので実装が容易 ● Javaの実行環境であれば動作する Cons ● シリアライズ/デシリアライズの処理速度が 遅い ● メモリ消費量が大きい import java.io.Serializable data class Hoge( val fuga: String, val piyo: Int, ) : Serializable

Slide 9

Slide 9 text

Parcelable 9 AndroidのParcel機構 メモリ上で効率的にデータをやり取りするために設 計されている Pros ● シリアライズ/デシリアライズの処理速度が速い ● メモリ効率が良い Cons ● 実装がやや複雑で面倒 ● 永続化には向かない import android.os.Parcelable data class Hoge( val fuga: String, val piyo: Int, ) : Parcelable { constructor(parcel: Parcel): this( parcel.readString() ?: "", parcel.readInt(), ) override fun writeToParcel(dest: Parcel?, flags: Int) { if (dst != null) { dest.writeString(fuga) dest.writeInt(piyo) } } override fun describeContents() = 0 companion object CREATOR : Parcelable.Creator { override fun createFromParcel(parcel: Parcel) = Hoge(parcel) override fun newArray(size: Int): Array = arrayOfNulls(size) } }

Slide 10

Slide 10 text

Parcelable 10 AndroidのParcel機構 メモリ上で効率的にデータをやり取りするために設 計されている Pros ● シリアライズ/デシリアライズの処理速度が速い ● メモリ効率が良い Cons ● 実装がやや複雑で面倒 ● 永続化には向かない import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize data class Hoge( val fuga: String, val piyo: Int, ) : Parcelable kotlin-parcelize @Parcelizeで おk

Slide 11

Slide 11 text

問題になった事象 11

Slide 12

Slide 12 text

前提 ● pixivコミック Androidアプリでは画面遷移をActivity.startActivityで行っている ● マルチモジュール化を進行中 ○ 大半の実装がappモジュール内にまとまったままになっている 12

Slide 13

Slide 13 text

問題になった事象 ● 画面遷移時にBundleからデータを取得する箇所でクラッシュが発生💣💥 13

Slide 14

Slide 14 text

問題になった事象 ● 画面遷移時にBundleからデータを取得する箇所でクラッシュが発生💣💥 解決策 ● Serializableにロールバックした ○ 早急な対応のため、手数が少なく、動作の実績がある元実装に戻した 14

Slide 15

Slide 15 text

なぜ発生したのか ● 対応するclassをSerializable->Parcelableに置き換えていたが、Bundleから取り出 す処理をSerializable時代のままにしていた ● pixivコミック Androidチームでは変更箇所の動作確認をリリース前に行なっている ● 当時改修対象だった機能の動作も問題なかった ○ 影響範囲の調査不足(影響範囲が広い実装の変更だった) 15

Slide 16

Slide 16 text

今後の運用方針 16

Slide 17

Slide 17 text

今後の運用方針 ● 新規実装ではParcelableを使用する ○ @Parcelizeを活用する ● 役割を明確にするため、画面遷移に利用するデータはドメインモデルとは別に定義 ○ 便宜上、ナビゲーションモデルと呼ぶ ○ ナビゲーションモデルはnavigationモジュールに配置する ○ ナビゲーションモデル<-->ドメインモデルの変換ロジックもnavigationモジュールに配置する 17

Slide 18

Slide 18 text

Parcelableに変えていく理由 ● IntentやBundleのListの扱いがSerializableよりParcelableが型安全 ● Serializableよりパフォーマンス面に優れているParcelableを利用 18

Slide 19

Slide 19 text

Listの扱いが型安全 19 ● Listの型を指定して型安全に取得可能 ● Serializableの場合は自分でキャスト data class Hoge( val fuga: String, val piyo: Int, ) : Parcelable // IntentからHogeのArrayListを取得 IntentCompat.getParcelableArrayListExtra( /* in = */ intent, /* key = */ “hoge”, /* clazz = */ Hoge::class.java ) // SerializableはList専用の口がないのでキャストする IntentCompat.getSerializableExtra( /* in = */ data, /* key = */ “hoge”, /* clazz = */ ArrayList::class.java ) as? ArrayList

Slide 20

Slide 20 text

Serializable vs Parcelable Serizliable ● Java ● 実装が楽 ● 処理速度が遅め ● メモリ消費大 20 Parcelable ● Android固有 ● 実装が面倒(プラグインで自動生成が可能) ● 処理速度が速め ● メモリ消費小

Slide 21

Slide 21 text

Serializableと比較してParcelableがパフォーマンス優勢な理由 ● リフレクションしていない ○ writeToParcelで明示的に書き出す対象を指定するので解析の手間がなく、実行時の処理が速い ● writeString, readString などの専用メソッドにより効率化 ○ 余計なGCのオーバーヘッドを削減 ● Parcelの内部実装で最適化が行われている ○ メモリマッピングなど ● オブジェクトの循環参照を回避している ○ 循環参照が発生する可能性がある場合、IDを利用して既にデシリアライズされたものを再利用したり、呼 び出された時に参照を解決したりして解決できる 21

Slide 22

Slide 22 text

モデルの相互変換 22 ● それぞれのプロパティに1対1対応す るシンプルな変換 ○ 遷移元->toNavigationModel ○ 遷移先->toModel ● kotlin-fill-classというプラグインを 活用すると効率UP ○ https://plugins.jetbrains.com/plugin/1094 2-kotlin-fill-class // core/data/model/~/Hoge.kt data class Hoge( val fuga: String, val piyo: Int, ) // core/navigation/~/model/Hoge.kt data class Hoge( val fuga: String, val piyo: Int, ) : Parcelable // core/navigation/~/model/converter/DoM2NaviM.kt fun Hoge.toNavigationModel() = hoge.sample.core.navigation.model.Hoge( fuga = fuga, piyo = piyo, ) // core/navigation/~/model/converter/NaviM2DoM.kt fun Hoge.toModel() = hoge.sample.core.data.model.Hoge( fuga = fuga, piyo = piyo, )

Slide 23

Slide 23 text

まとめ 23

Slide 24

Slide 24 text

まとめ ● ActivityやFragment間のデータ受け渡しはSerializableよりParcelableが優秀 ○ シリアライズの速度やメモリ効率もParcelableに軍配があがる ○ ただし、永続化に使うのは避けること ● @Parcelizeを活用すべし ○ kotlin-parcelizeを導入しよう ○ ボイラープレートを自動生成してくれて便利 ● 役割ごとに型を分けて影響範囲を明確にする ○ 今回は画面遷移時に引き渡されるパラメータとしてナビゲーションモデルを定義した ○ ナビゲーションモデルはBundleに格納・取出することに集中する ○ 遷移先でドメインモデルとして振る舞えるように相互変換を実装する 24

Slide 25

Slide 25 text

参考文献 ● https://developer.android.com/kotlin/parcelize?hl=ja#skip_properties_from _serialization ● https://developer.android.com/privacy-and-security/risks/unsafe-deserializ ation ● https://outcomeschool.com/blog/parcelable-vs-serializable ● https://mixi-inc.github.io/AndroidTraining/fundamentals/2.07.serialize-and- collection-and-perpetuation.html#直列化 ● Gemini Advanced 2.0 Flash 25