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иonSaveInstanceStateintent— объект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, это будет в рамках следующих статьей.