В большинстве случаев сериализация в Андроиде не нужна

от автора

TL;DR: В большинстве приложений имеет смысл принять явное осознанное архитектурное решение, что в случае смерти процесса приложение просто перезапускается с нуля, не пытаясь восстанавливать состояние. И в этом случае Serializable, Parcelable и прочие Bundle не нужны.

Если хотя бы одна активность приложения находится между onStart() и onStop(), то гарантируется, что активность, а следовательно, и процесс, в котором активность живёт, находятся в безопасности. В остальных случаях операционная система может в любой момент убить процесс приложения.

Мне не приходилось реализовывать прозрачную (то есть чтобы было незаметно для пользователя) обработку смерти процесса в реальном приложении. Но вроде бы это вполне реализуемо, набросал рабочий пример: https://github.com/mychka/resurrection
Идея состоит в том, чтобы в каждой активности в onSaveInstanceState() сохранять всё состояние приложения, а в onCreate(), если процесс был убит, восстанавливать:

abstract class BaseActivity : AppCompatActivity() {      override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)          ResurrectionApp.ensureState(savedInstanceState)     }      override fun onSaveInstanceState(outState: Bundle) {         super.onSaveInstanceState(outState)          ResurrectionApp.STATE.save(outState)     } }

Для того, чтобы сэмулировать убийство процесса, можно свернуть приложение и использовать команду

adb shell am kill org.resurrection

Если решаем обрабатывать смерть процесса, то можно отметить следующие накладные расходы.

  1. Усложнение кода.

    • Раньше можно было в любом месте воткнуть статическое поле и хранить в нём состояние. Теперь так не получится, нужно аккуратно следить за состоянием приложения, чтобы не забыть что-нибудь сохранить. Но в чём-то это даже плюс: дисциплинирует, может положительно повлиять на архитектуру, помочь избежать утечки памяти.
    • Всё состояние должно быть честно Serializable/Parcelable.
    • Восстановление состояния — это не только десериализация, но и приведение к консистентному виду. Например, до смерти процесса был флажок loading == true и запущенный поток. Так как после смерти процесса поток умер, нужно либо этот поток перезапустить, либо сбросить loading в false. То же самое с открытыми TCP-соединениями, которые после смерти процесса закрываются.
    • Нужно следить за кодом, который вызывается до Activity#onCreate() — например, за статикой и инициализацией полей — так как в этот момент глобальное состояние может быть ешё не восстановлено.

    В целом, мне кажется, описанные выше проблемы вполне решаемы так, чтобы полностью оградить основной код. Нужно немного дисциплины, а современные инструменты позволят всё спрятать в базовые классы и аннотации, в крайнем случае помогут рефлексия, манипуляция байт-кодом и кодогенерация.

  2. Приходится в каждом Activity#onSaveInstanceState() сохранять полностью всё состояние приложения. (Наверное, если Activity#isChangingConfigurations == true, то можно сэкономить, не сохранять.) Но не думаю, что это может сказаться на производительности, так как более-менее современные смартфоны достаточно мощные.

  3. Случай смерти процесса нужно тестировать. Иначе нет смысла вкладываться в пункт 1, а в итоге всё равно иметь неработающую фичу. В случае большого приложения с сотней активностей/фрагментов тестирование может вылиться в копеечку.

  4. Безопасность. Я в эту тему не углублялся, но, наверное, если чувствительная информация хранится только в оперативной памяти, то украсть её сложнее, чем если она в случае смерти процесса дампится куда-то на жёсткий диск.
    На практике, думаю, никакой разницы нет: если смогли украсть данные из дампа, то и без дампа украдут. Непреодолимой трудностью может оказаться убедить в этом надзорные органы, когда речь идёт, например, о мобильном банкинге.

Отдельного исследования заслуживает вопрос о том, насколько вообще вероятно убийство процесса приложения в реальной жизни. Как вариант, чтобы снизить вероятность, можно запускать foreground сервис. Но не думаю, что это правильно.

В любом случае, мне кажется, можно сделать следующий вывод. Прозрачно обработать смерть процесса технически возможно, но в большинстве приложений это нецелесообразно, и имеет смысл сознательно и явно принять решение не поддерживать прозрачную обработку смерти процесса.

Итак, допустим, мы решаем не заморачиваться с обработкой смерти процесса. Если процесс убивают, и пользователь возвращается в приложение, то нас устраивает перезапуск с нуля. В этом случае возникает трудность: Андроид пытается восстанавливать стек активностей, что может приводить к непредсказуемым последствиям.

В качестве иллюстрации я создал https://github.com/mychka/life-from-scratch
Закомментируем код BaseActivity и запустим приложение. Открывается LoginActivity. Нажимаем кнопку "NEXT". Поверх открывается DashboardActivity. Сворачиваем приложение. Для эмуляции убийства процесса вызываем

adb shell am kill org.lifefromscratch

Возвращаемся в приложение. Приложение крашится, так как DashboardActivity обращается к полю LoginActivity#loginShownAt, которое в случае смерти процесса оказывается непроинициализированным.

Красивого и универсального решения проблемы я не знаю. Предлагаю в статическом блоке базового класса активностей делать проверку, не было ли перезапуска приложения. Если обнаруживаем перезапуск процесса, то отправляем интент на перезапуск приложения с нуля и самоубиваемся:

abstract class BaseActivity : AppCompatActivity() {      companion object {         init {             if (!appStartedNormally) {                 APP.startActivity(                     APP.getPackageManager().getLaunchIntentForPackage(                         APP.getPackageName()                     )                 );                 System.exit(0)             }         }     } }

Решение кривое. Но оно вроде бы достаточно надёжное, проверено годами в серьёзном интернет-банкинге.

Теперь пришла пора пожинать плоды. Коль скоро мы всегда остаёмся в рамках одного процесса, то и заморачиваться с сериализацией нет резона. Создаём класс

class BinderReference<T>(val value: T?) : Binder()

И гоняем через Parcel любые объекты по ссылке. Например,

class MyNonSerializableData(val os: OutputStream)  val parcel: Parcel = Parcel.obtain() val obj = MyNonSerializableData(ByteArrayOutputStream()) parcel.writeStrongBinder(BinderReference(obj)) parcel.setDataPosition(0) val obj2 = (parcel.readStrongBinder() as BinderReference<*>).value assert(obj === obj2)

Темы использования android.os.Binder в качестве транспорта объектов я касался в статье https://habr.com/ru/post/274635/

Такой подход имеет большой потенциал. Можно красиво обернуть и использовать во многих местах. Например, для передачи произвольных аргументов во фрагменты. Добавим несколько утилит:

const val DEFAULT_BUNDLE_KEY = "com.example.DEFAULT_BUNDLE_KEY.cr5?Yq+&Jr@rnH5j"  val Any?.bundle: Bundle?     get() = if (this == null) null else Bundle().also { it.putBinder(DEFAULT_BUNDLE_KEY, BinderReference(this)) }  inline fun <reified T> Bundle?.value(): T =     this?.getBinder(DEFAULT_BUNDLE_KEY)?.let {         if (it is BinderReference<*>) it.value as T else null     } as T  inline fun <reified Arg> Fragment.defaultArg() = lazy<Arg>(LazyThreadSafetyMode.NONE) {     arguments.value() }

И наслаждаемся комфортом. Запуск фрагмента:

findNavController(R.id.nav_host_fragment).navigate(     R.id.bbbFragment,     MyNonSerializableData(ByteArrayOutputStream()).bundle )

Во фрагменте добавляем поле

val data: MyNonSerializableData by defaultArg()

Другой пример — androidx.lifecycle.ViewModel. Этот класс бесполезен чуть менее, чем полностью, так как не переживает destroy активности, а обрабатывает только configuration change, являясь обёрткой над https://developer.android.com/reference/androidx/activity/ComponentActivity.html#onRetainCustomNonConfigurationInstance()
Допустим, что одна активность приложения открывается поверх другой, или пользователь кратковременно переключается на другое приложение, и операционная система решает уничтожить активность. В этом случае ViewModel умирает.

Используя BinderReference, несложно сделать аналог androidx.lifecycle.ViewModel, основывающийся на обычном механизме сохранения состояния onSaveInstanceState(outState)/onCreate(savedInstanceState). Такому view model не страшно уничтожение активности.

ссылка на оригинал статьи https://habr.com/ru/post/489746/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *