ViewModel под капотом: как она выживает при пересоздании Activity
Введение
В статье не рассматривается работа с ViewModel, предполагается, что эта тема уже знакома. Основное внимание уделяется тому, как ViewModel переживает изменение конфигурации. Но для начала — небольшое введение в ViewModel.
ViewModel - компонент архитектурного паттерна MVVM, который был предоставлен Google как примитив позволяющий пережить изменение конфигураций. Изменение конфигураций в свою очередь - это состояние, заставляющая activity/fragment пересоздаваться, это именно то состояние которое может пережить ViewModel. Популярные конфигурации которые приводят к пересозданию Activity:
Изменение ориентаций экрана(screenOrientation): portrait/landscape
Изменение направления экрана(layoutDirection): rtl/ltr
Изменение языка приложения(locale)
Изменение размера шрифтов/соотношение экрана
Есть конечно способ сообщать системе о том что пересоздавать Activity при изменении конфигураций не нужно. Флаг android:configChanges
используется в AndroidManifest.xml
в теге <activity/>
, чтобы указать, какие изменения конфигурации система не должна пересоздавать Activity, а передавать управление методу Activity.onConfigurationChanged()
.
Однако сейчас речь не об этом. Наша цель — разобраться, каким образом ViewModel
умудряется переживать все изменения конфигурации и сохранять своё состояние.
Объявление ViewModel
С появлением делегатов в Kotlin разработчики получили возможность значительно упростить создание и использование компонентов. Теперь объявление ViewModel
с использованием делегатов выглядит следующим образом:
Без делегатов создание объекта ViewModel, используя явный вызов ViewModelProvider выглядит следующий образом:
Метод ViewModelProvider.create
имеет параметры со значениями по умолчанию, поэтому на уровне байткода компилятор создаст несколько перегруженных версий метода (overloads). Это позволяет вызывать его с разным количеством аргументов: только с store
, с store
и factory
, либо со всеми параметрами, включая extras
.
ViewModelStoreOwner ?
Как мы видим выше, мы вручную не создаём объект ViewModel, а только передаём тип его класса в ViewModelProvider, который самостоятельно занимается созданием экземпляра.
Обратите внимание, что мы также передаём в метод ViewModelProvider.create
параметр owner = this
. Если заглянуть в исходники метода create, можно заметить, что требуется тип owner: ViewModelStoreOwner:
Углубляемся в ViewModelStore / Owner
Получается что при вызове метода ViewModelProvider.create()
для параметра owner
мы передаем this
(само активити), и как можно догадаться, это означает, что activity реализует(наследуется) от интерфейса ViewModelStoreOwner
. Давайте взглянем на исходники этого интерфейса: ViewModelStoreOwner:
ViewModelStoreOwner
— это интерфейс с единственным полем, которое представляет собой ViewModelStore
(хранитель view models). От ViewModelStoreOwner
наследуются такие компоненты как: ComponentActivity, Fragment, * NavBackStackEntry*.
Официальная документация гласит:
Обязанности ViewModelStoreOwner:
Хранение ViewModelStore во время изменения конфигураций.
Очистка ViewModelStore при уничтожении ComponentActivity/Fragment — в состоянии
onDestroy()
. Удаляются все ViewModel-и которые ViewModelStore хранить в себе.
Мы определили, что ViewModelStoreOwner — это всего лишь интерфейс, не содержащий собственной логики. Его реализуют такие компоненты, как:
ComponentActivity
(и его наследники:FragmentActivity, AppCompatActivity
)Fragment
(и его производные:DialogFragment
,BottomSheetDialogFragment
,AppCompatDialogFragment
).NavBackStackEntry
- Класс из библиотеки Jetpack Navigation (он же androidx navigation)
Далее нас уже интересует сам ViewModelStore:
ViewModelStore — это класс, который внутри себя делегирует управление коллекцией Map (LinkedHashMap) для хранения ViewModel по ключу:
По умолчанию в качестве ключа используется полное имя класса (включая его пакет). Этот ключ генерируется следующим образом в исходниках утилитного класса ViewModelProviders (не путать с ViewModelProvider):
Таким образом, для MyViewModel ключ будет выглядеть так: androidx.lifecycle.ViewModelProvider.DefaultKey:com.example.MyViewModel
.
Поскольку ViewModelStore основан на Map
, он делегирует все основные операции, такие как put, get, keys и clear
, внутреннему Map (LinkedHashMap).
Соответственно, так как внутренняя реализация ViewModelStore
полагается на Map, он также делегирует свои методы put
, get
, key
, clear
внутреннему Map
(LinkedHashMap). Особого внимания заслуживает метод clear()
:
Давайте разберёмся, что здесь происходит. Когда наш ViewModelStoreOwner
(в лице ComponentActivity
или Fragment
) окончательно умирает (смерть не связана с пересозданием из-за изменения конфигураций), он вызывает метод clear()
у ViewModelStore
.
В методе clear()
цикл for
проходит по всем значениям (view models
), которые хранятся внутри внутреннего HashMap
, и вызывает у каждой ViewModel
внутренний метод clear()
. Этот метод, в свою очередь, инициирует вызов метода onCleared()
у нашей ViewModel
.
onCleared()
— это метод, который мы можем переопределить в своей ViewModel
, и он вызывается только в момент окончательного уничтожения ViewModel
, когда активити или фрагмент также окончательно завершают свою работу.
Таким образом, метод clear()
гарантирует, что все ресурсы и фоновые задачи, связанные с ViewModel
, будут корректно освобождены перед уничтожением. Соответственно, сам метод viewModelStore.clear()
вызывается ViewModelStoreOwner
(в лице ComponentActivity
или Fragment
).
Давайте в качестве примера выберем ComponentActivity
, чтобы понять, как работает очистка.
Ниже приведён фрагмент кода из ComponentActivity
, который отслеживает её уничтожение и вызывает viewModelStore.clear()
:
В данном коде происходит добавление наблюдателя на жизненный цикл активности с использованием LifecycleEventObserver
. Когда активность достигает состояния ON_DESTROY
, запускается проверка, не происходит ли изменение конфигурации (isChangingConfigurations
). Если активность действительно умирает окончательно (и не пересоздаётся), вызывается метод viewModelStore.clear()
, который очищает все связанные с активностью ViewModel
.
Мы видим, что проверка состояния ON_DESTROY
в сочетании с условием if (!isChangingConfigurations) позволяет убедиться в том, что причиной уничтожения не является изменение конфигурации. Только в этом случае очищается ViewModelStore и удаляются все экземпляры ViewModel, связанные с данной активностью.
Процесс очистки ViewModel при уничтожении активности:
Уничтожение Activity (не связано с изменением конфигураций)
ComponentActivity.onDestroy()
-> Очистка ViewModelStoregetViewModelStore().clear()
-> Оповещение ViewModelMyViewModel.onCleared()
Теперь мы разобрались с процессом очистки и уничтожения ViewModel
.
Перейдём к следующему этапу — рассмотрим подробнее, как происходит создание объекта ViewModel
, когда мы передаём её в ViewModelProvider
:
Да, можно уточнить, что ViewModelProvider.create
— это функция с значениями по умолчанию. Например:
Ранее мы разобрали один из перегруженных методов ViewModelProvider.create
(функция с аргументами по умолчанию). Это фабричный метод, который принимает минимум ViewModelStore
или ViewModelStoreOwner
, создаёт объектViewModelProvider
и на этом завершает свою работу.
Теперь нас интересует следующий ключевой метод — get
, который принимает класс ViewModel
в качестве параметра. ViewModelProvider
делегирует свою работу классу ViewModelProviderImpl
:
Исходники метода getViewModel()
в ViewModelProviderImpl.kt
:
При вызове ViewModelProvider.create()
под капотом вызывается метод getViewModel()
, который выполняет следующие шаги:
Проверяет наличие объекта
ViewModel
вViewModelStore
по заданному ключу. Если объект уже существует, он возвращается.Если объект не найден, создаётся новый экземпляр
ViewModel
, который затем кладётся вViewModelStore
для последующего использования.
Где ViewModelStore сохраняется?
Теперь, когда мы знаем полный процесс создания ViewModel
и её размещения в ViewModelStore
, возникает логичный вопрос: если все ViewModel
-и хранятся внутри ViewModelStore
, а сам ViewModelStore
находится в ComponentActivity
или Fragment
, которые реализуют интерфейс ViewModelStoreOwner
, то где и как хранится сам объект ViewModelStore
?
Для того чтобы найти ответ на вопрос о хранении ViewModelStore
, давайте посмотрим, как ComponentActivity
реализует интерфейс ViewModelStoreOwner
:
Мы видим, что вызывается метод ensureViewModelStore
, а затем возвращается поле _viewModelStore
.
Поле _viewModelStore
не имеет значения по умолчанию, поэтому перед возвратом оно инициализируется внутри метода ensureViewModelStore
:
Тут-то и начинается самое интересное. Если поле _viewModelStore
равно null
, сначала выполняется попытка получить его из метода getLastNonConfigurationInstance()
, который возвращает объект класса NonConfigurationInstances
.
Если ViewModelStore
отсутствует и там, это может означать одно из двух:
Активность создаётся впервые и у неё ещё нет сохранённого
ViewModelStore
.Система уничтожила процесс приложения (например, из-за нехватки памяти), а затем пользователь снова запустил приложение, из-за чего
ViewModelStore
не сохранился.
В любом из этих случаев создаётся новый экземпляр ViewModelStore
.
Самая неочевидная часть — это вызов метода getLastNonConfigurationInstance()
. Этот метод принадлежит классуActivity
, а класс NonConfigurationInstances
, у которого даже само название выглядит интригующе, объявлен вComponentActivity
:
Таким образом, объект NonConfigurationInstances
используется для хранения ViewModelStore
при изменении конфигурации активности. Это позволяет сохранить состояние ViewModel
и восстановить его после пересоздания активности.
Переменная custom
по умолчанию имеет значение null
и фактически не используется, поскольку ViewModelStore
более гибко выполняет всю работу по сохранению состояний для переживания изменений конфигураций. Тем не менее, переменную custom
можно задействовать, переопределив такие функции, как onRetainCustomNonConfigurationInstance
и getLastCustomNonConfigurationInstance
. До появления ViewModel
многие разработчики активно использовали(в 2012) именно её для сохранения данных при пересоздании активности когда менялась конфигурация.
Переменная viewModelStore
имеет тип ViewModelStore
и хранит ссылку на наш объект ViewModelStore
. Значение в эту переменную NonConfigurationInstances#viewModelStore
присваивается при вызове методаonRetainNonConfigurationInstance
, а извлекается при вызове getLastNonConfigurationInstance
(с этим методом мы уже столкнулись выше в методе ensureViewModelStore
).
Разобравшись с классом NonConfigurationInstances
, давайте выясним, где создаётся объект этого класса и каким образом в поле viewModelStore
присваивается значение.
Для этого обратимся к методам onRetainNonConfigurationInstance
и getLastNonConfigurationInstance
, которые присутствуют в Activity
и ComponentActivity
.
Исходники метода в ComponentActivity
выглядят следующим образом:
Метод onRetainNonConfigurationInstance()
возвращает объект NonConfigurationInstances
, содержащий ссылку на ранее созданный ViewModelStore
.
Таким образом, при уничтожении активности (например, при повороте экрана) вызывается этот метод, и ViewModelStore
сохраняется в экземпляре NonConfigurationInstances
.
Когда активность пересоздаётся, объект NonConfigurationInstances
восстанавливается через вызов метода getLastNonConfigurationInstance()
, и из него извлекается сохранённый ViewModelStore
.
В методе onRetainNonConfigurationInstance
реализована логика получения уже существующего ViewModelStore
и объекта Custom
(если он есть). После получения этих объектов они кладутся в экземпляр класса NonConfigurationInstances
, который затем возвращается из метода.
Метод onRetainNonConfigurationInstance
создаёт объект класса NonConfigurationInstances
, помещает внутрь viewModelStore
и кастомный объект, а затем возвращает его. Возникает вопрос: кто именно вызывает этот метод?
Вызывающий метод внутри самого класса Activity(самый базовый Activity от которого наследуются все остальные):
Как видно, сам класс Activity
вызывает метод onRetainNonConfigurationInstance
с которым мы ранее познакомились и сохраняет результат в полеactivity
класса NonConfigurationInstances
. При этом мы снова сталкиваемся с классом NonConfigurationInstances
, но на этот раз он объявлен в самой Activity
и имеет дополнительные поля:
Чтобы устранить путаницу:
Объект
ViewModelStore
хранится внутриComponentActivity#NonConfigurationInstances
.Сам объект
ComponentActivity#NonConfigurationInstances
хранится вActivity#NonConfigurationInstance
.Это достигается через метод
retainNonConfigurationInstances()
классаActivity
.
Но кто же вызывает метод retainNonConfigurationInstances
() и где хранится конечный объект Activity#NonConfigurationInstance
, который содержит ViewModelStore
?
Ответ на этот вопрос кроется в классе ActivityThread
, который отвечает за управление жизненным циклом активностей и их взаимодействие с системой. Именно этот класс обрабатывает создание, уничтожение и повторное создание активности, а также отвечает за сохранение и восстановление данных при изменениях конфигурации.
Метод из ActivityThread
, который непосредственно вызывает Activity.retainNonConfigurationInstances()
, называется ActivityThread.performDestroyActivity()
.
Рассмотрим его исходники в классе ActivityThread
, далее исходники:
После вызова метода retainNonConfigurationInstances()
результат сохраняется в поле lastNonConfigurationInstances
объекта ActivityClientRecord
:
Класс ActivityClientRecord
представляет собой запись активности и используется для хранения всей информации, связанной с реальным экземпляром активности.
Это своего рода структура данных для ведения учета активности в процессе выполнения приложения.
Основные поля класса ActivityClientRecord
:
lastNonConfigurationInstances
— объектActivity#NonConfigurationInstance
, в котором хранитсяComponentActivity#NonConfigurationInstances
в котором хранитсяViewModelStore
.state
— объектBundle
, содержащий сохраненное состояние активности. Да, да, это тот самый Bundle который мы получаем в методеonCreate
,onRestoreInstanceState
иonSaveInstanceState
intent
— объектIntent
, представляющий намерение запуска активности.window
— объектWindow
, связанный с активностью.activity
— сам объектActivity
.parent
— родительская активность (если есть).createdConfig
— объектConfiguration
, содержащий настройки, примененные при создании активности.overrideConfig
— объектConfiguration
, содержащий текущие настройки активности.
В рамках данной статьи нас интересует только поле lastNonConfigurationInstances
, так как именно оно связано с хранением и восстановлением ViewModelStore
.
Теперь давайте разберемся, как вызывается метод performDestroyActivity()
в рамках системного вызова.
Последовательность вызовов:
ActivityTransactionItem.execute()
ActivityRelaunchItem.execute()
ActivityThread.handleRelaunchActivity()
ActivityThread.handleRelaunchActivityInner()
ActivityThread.handleDestroyActivity()
ActivityThread.performDestroyActivity()
На вершине вызовов находится метод ActivityTransactionItem.execute()
, который запускает цепочку: сначала вызывает getActivityClientRecord()
, а затем тот вызывает ClientTransactionHandler.getActivityClient()
.
ClientTransactionHandler
— это абстрактный класс, и одна из его реализаций — класс ActivityThread
, с которым мы уже успели познакомиться.
От ActivityTransactionItem - наследуется класс ActivityRelaunchItem, который и запускает у ActivityThread метод handleRelaunchActivity
:
Все запущенные активности внутри нашего приложения хранятся в коллекций Map в объекте класса ActivityThread
:
Таким образом, мы наконец выяснили, что наша ViewModel
фактически хранится в объекте ActivityThread
, который является синглтоном. Благодаря этому ViewModel
не уничтожается при изменении конфигурации.
Важно: Экземпляр ActivityThread
является синглтоном и существует на протяжении всего жизненного цикла процесса приложения. В методе handleBindApplication()
внутри ActivityThread
создается объект Application
, который также живет до завершения процесса. Это означает, что ActivityThread
и Application
связаны общим жизненным циклом, за исключением того, что ActivityThread
появляется раньше — еще до создания Application
— и управляет его инициализацией.
Восстановление ViewModelStore
Исходя из того, что мы обнаружили ранее, цепочка хранения ViewModel
выглядит следующим образом:
ViewModel
хранится внутриViewModelStore
.ViewModelStore
хранится вComponentActivity#NonConfigurationInstances
.ComponentActivity#NonConfigurationInstances
хранится вActivity#NonConfigurationInstance
.Activity#NonConfigurationInstance
хранится вActivityClientRecord
.ActivityClientRecord
хранится вActivityThread
.
При повторном создании Activity
вызывается его метод attach()
, одним из параметров которого является Activity#NonConfigurationInstances
. Этот объект извлекается из связанного с Activity
объекта ActivityClientRecord
.
Когда у Activity
меняется конфигурация, система сразу же перезапускает её, чтобы применить новые параметры. В этот момент ActivityThread.java
мгновенно извлекает ViewModelStore
, который хранится в ComponentActivity#NonConfigurationInstances
. Этот объект, в свою очередь, находится внутри Activity#NonConfigurationInstances
.
Далее Activity#NonConfigurationInstances
сохраняется в ActivityClientRecord
, связанном с пересоздаваемой Activity
. Внутри ActivityClientRecord
есть специальное поле lastNonConfigurationInstances
, куда и помещается этот объект. Сам ActivityClientRecord
хранится в Map-коллекции внутри ActivityThread.java
, который является синглтоном в рамках процесса приложения и способен переживать изменения конфигурации.
После этого ActivityThread пересоздаёт Activity, применяя новые параметры конфигурации. При создании он передаёт в неё все сохранённые данные, включая NonConfigurationInstances
, который, в конечном итоге, содержит ViewModelStore
. А ViewModelStore, в свою очередь, хранит нашу ViewModel
Диаграмма вызовов при сохранении и восстановлении ViewModelStore
Диаграмма ниже иллюстрирует цепочку вызовов. Ради упрощения некоторые детали опущены, а избыточные абстракции убраны:
Итоги
В этой статье мы не касались работы ViewModel как таковой — фокус был исключительно на том, почему она не умирает при пересоздании Activity, и за счёт чего это вообще возможно.
Мы проследили всю цепочку: ViewModel
→ ViewModelStore
→ ComponentActivity#NonConfigurationInstances
→ Activity#NonConfigurationInstances
→ ActivityClientRecord
→ ActivityThread
. Именно в этой глубокой вложенности и заключается ответ: ViewModel выживает, потому что сохраняется не в Activity напрямую, а в объекте, который система сама передаёт новой Activity при конфигурационных изменениях.
Сам ViewModelStore
создаётся либо с нуля, либо восстанавливается через getLastNonConfigurationInstance()
. Он очищается только в onDestroy()
, если isChangingConfigurations == false
, — то есть если Activity действительно умирает, а не пересоздаётся.
Под капотом всё это обеспечивается ActivityThread
, который сохраняет NonConfigurationInstances
в ActivityClientRecord
, а потом передаёт в метод attach()
при создании новой Activity. ActivityThread
— синглтон, живущий столько же, сколько и процесс, и именно он является опорной точкой, через которую проходит вся цепочка восстановления.
ViewModel выживает не потому, что её кто-то “сохраняет” — а потому, что никто её не убивает. Пока жив ActivityThread
, жив и ViewModelStore
.
Позже мы снова вернемся к ActivityThread
и ActivityClientRecord
, это будет в рамках следующих статьей.