Тема сериализации данных является базой для любого мобильного разработчика, поскольку используется для работы с сетью, файловой системой и коммуникацией между основными компонентами. Но есть в этом вопросе особенность, с которой я столкнулся впервые за 7 лет.
Именно об этом открытии и пойдет речь в статье — о смеси двух методов сериализации, а так же почему для решения этой задачи важно разбираться в работе разных видов classLoader. Информация из статьи поможет вам постепенно мигрировать на Parcelable в нужных местах, не переписывая сразу все классы на новую технологию.
Поскольку Parcelable не является общим механизмом сериализации (нельзя использовать для сохранения данных на диск или сетевых запросов), он не может полностью заменить Serializable, но остается более эффективным для Android среды. Два решения сериализации остаются в проекте с нами надолго, а это значит нам нужно уметь работать с ними правильно.
Точка отсчета
Статья родилась из периодически возникающего краша в системе логирования:
java.lang.RuntimeException: Unable to start activity java.io.NotSerializableException: android.content.Intent
Давайте разберемся, что же пошло не так. Рассмотрим упрощенный пример:
data class ScreenViewModel( val payload: Any ): Serializable
В рамках экрана payload использовался либо для навигации по deeplink (если передавали строку), либо для навигации в другую Activity , если передавался Intent . Может показаться, что проблемы нет, но давайте посмотрим на декларацию класса Intent.
public class Intent implements Parcelable, Cloneable
Здесь как раз и раскрывается причина краша приложения — Intent, в отличии от String, не является Serializable . В таком случае давайте напишем код, который позволит динамически выбирать тип сериализации, и использовать его, сохраняя возможность передачи в функциональность точно такого же Payload.
data class ScreenViewModel( val payload: Any ): Parcelable { public companion object CREATOR : Parcelable.Creator<ScreenViewModel> { override fun createFromParcel(parcel: Parcel): ScreenViewModel { return ScreenViewModel(parcel) } override fun newArray(size: Int): Array<ScreenViewModel?> { return arrayOfNulls(size) } } public constructor( parcel: Parcel ) : this( payload = parcel.readPayload(), ) override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writePayload(payload, flags) } override fun describeContents(): Int = 0 }
В шаблонном коде реализации Parcelable находятся два интересующих нас extension: readPayload и writePayload .
Для того, чтобы динамически понимать, какой способ сериализации нужен, достаточно сохранять идентификатор типа сериализации рядом с объектом:
private const val TYPE_NULL = 0 private const val TYPE_PARCELABLE = 1 private const val TYPE_SERIALIZABLE = 2 public fun Parcel.writePayload(payload: Any?, flags: Int) { when (payload) { is Parcelable -> { writeInt(TYPE_PARCELABLE) writeParcelable(payload, flags) } is Serializable -> { writeInt(TYPE_SERIALIZABLE) writeSerializable(payload) } null -> { writeInt(TYPE_NULL) } else -> { throw IllegalArgumentException("Unsupported type for payload") } } }
Таким образом на этапе чтения мы сможем понять, какой способ использовался при записи и использовать именно его
public fun Parcel.readPayload(): Any { return requireNotNull(readNullablePayload()) } public fun Parcel.readNullablePayload(): Any? { return when (readInt()) { TYPE_PARCELABLE -> readParcelable<Parcelable>() TYPE_SERIALIZABLE -> readSerializable<Serializable>() TYPE_NULL -> null else -> throw IllegalArgumentException("Unknown payload type") } }
Теперь нам остается реализовать методы readParcelable и readSerializable, после чего интеграция закончена. Но в этой простоте вызова системных методов по чтению Serializable и Parcelable всё равно остается проблема. Давайте рассмотрим код
public inline fun <reified T> Parcel.readParcelable(): T? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { readParcelable(T::class.java.classLoader, T::class.java) } else { readParcelable(T::class.java.classLoader) } }
На первый взгляд ничего необычного до момента, когда вы заходите прочитать class вашего приложения, но который на этапе исполнения кода будет являться обобщенным типом Parcelable, из-за чего в момент обращения к classLoader в Parcelable::class.java.classLoader система достанет java.lang.BootClassLoader, в котором естественно нет классов вашего приложения (системный classLoader виртуальной машины, где находятся системные классы и в том числе наш Parcelable), а следовательно десериализация будет невозможна.
Примечание. Подробнее про разные типы classLoaders можно почитать в официальной документации.
Одно из решений — при работе с обобщенным типом Parcelable работать не с его classLoader, а с classLoader вашего приложения (например, dalvik.system.PathClassLoader).
public inline fun <reified T> Parcel.readParcelable(): T? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (T::class != Parcelable::class) { readParcelable(T::class.java.classLoader, T::class.java) } else { readParcelable(Thread.currentThread().contextClassLoader, T::class.java) } } else { readParcelable(T::class.java.classLoader) } }
Аналогичный код будет и для Serializable.
public inline fun <reified T> Parcel.readSerializable(): T? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (T::class != Serializable::class) { readSerializable(T::class.java.classLoader, T::class.java) } else { readSerializable(Thread.currentThread().contextClassLoader, T::class.java) } } else { readSerializable() as T } }
Данные методы по чтению и зависи Serializable и Parcelable можно использовать и без payload, а для отдельный свойств, если вы наверняка знаете, являются ли они Serializable или Parcelable .
А можно организовать обратную вложенность?
Я рассмотрел пример, когда внутрь Parcelable может размещать Serializable объект. В моем случае, когда была потребность хранить в Payload объект Intent. Данное решения является единственным, посколько у меня нет возможности переписать исходники Android и сделать Intent поддерживающим Serializable, да и целесообразность данного решения остается сомнительной.
В общем случае реализовать обратную вложенность возможно — добавив интерфейс Serializable к вашему классу, либо переопределив хитрым способом сериализацию и десериализацию, например, через применение Externalizable (хотя ту же самую кастомную сериализацию можно сделать и с Serializable, но в Externalizable она выглядит элегантнее).
Рефлексия
Для меня всегда остается загадкой, как обыденные темы, спрашиваемые у младших разработчиков на интервью, могут так близко граничить с неочевидными особенностями SDK или требовать знаний работы системы глубже старшего разработчика.

