ViewModel в Activity под капотом: как она выживает при пересоздании
Что на самом деле происходит с ViewModel, когда Activity уничтожается и создаётся заново? Разбираем всю цепочку: ViewModelStore, NonConfigurationInstances, ActivityThread и ActivityClientRecord. Подробно показываем, где и как хранится состояние, и как Android магически восстанавливает всё при конфигурационных изменениях.
Введение
В статье не рассматривается работа с 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()
.
<activity
android:name="MainActivity"
android:configChanges="layoutDirection|touchscreen|density|orientation|keyboard|locale|keyboardHidden|navigation|screenLayout|mcc|mnc|fontScale|uiMode|screenSize|smallestScreenSize"
/>
Однако сейчас речь не об этом. Наша цель — разобраться, каким образом ViewModel
умудряется переживать все изменения конфигурации и сохранять своё состояние.
Объявление ViewModel
С появлением делегатов в Kotlin разработчики получили возможность значительно упростить создание и использование компонентов. Теперь объявление ViewModel
с использованием делегатов выглядит следующим образом:
class MainActivity : ComponentActivity() {
private val viewModel by viewModel<MyViewModel>()
}
Без делегатов создание объекта ViewModel, используя явный вызов ViewModelProvider выглядит следующий образом:
class MainActivity : ComponentActivity() {
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// В старых версиях ViewModelProvider был частью lifecycle-viewmodel
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
// После адаптации ViewModel под KMP и переноса ViewModelProvider в lifecycle-viewmodel-android
// можно и рекомендуется через перегруженный фабричный метод create:
viewModel = ViewModelProvider.create(owner = this).get(MyViewModel::class)
// Альтернативный способ создания ViewModel (эквивалентен предыдущему)
viewModel = ViewModelProvider.create(store = this.viewModelStore).get(MyViewModel::class)
}
}
Метод ViewModelProvider.create
имеет параметры со значениями по умолчанию, поэтому на уровне байткода компилятор создаст несколько перегруженных версий метода (overloads). Это позволяет вызывать его с разным количеством аргументов: только с store
, с store
и factory
, либо со всеми параметрами, включая extras
.
Jetpack ViewModel теперь поддерживает Kotlin Multiplatform (KMP), что позволяет использовать его не только на Android, но и на iOS, Desktop и Web. Это стало возможным благодаря разделению на два модуля:
lifecycle-viewmodel(expected): KMP-модуль без привязки к Android. lifecycle-viewmodel-android(actual): модуль для работы с ViewModelStoreOwner и ViewModelProvider на Android.
Начиная с версии 2.8.0-alpha03, артефакты lifecycle-* теперь официально поддерживают Kotlin Multiplatform! Это означает, что классы, такие как ViewModel, ViewModelStore, ViewModelStoreOwner и ViewModelProvider, теперь можно использовать в общем коде.
Далее в статье мы рассмотрим именно версию viewmodel:2.8.0+, если в версий на которой вы находитесь сейчас немного отличаются исходники, то не переживайте, c добавлением поддержки kmp немного поменяли внутренюю структуру , но реализация и внутренняя логика такая же что и до поддержки kmp
ViewModelStoreOwner ?
Как мы видим выше, мы вручную не создаём объект ViewModel, а только передаём тип его класса в ViewModelProvider, который самостоятельно занимается созданием экземпляра.
Обратите внимание, что мы также передаём в метод ViewModelProvider.create
параметр owner = this
. Если заглянуть в исходники метода create, можно заметить, что требуется тип owner: ViewModelStoreOwner:
public actual companion object {
@JvmStatic
@Suppress("MissingJvmstatic")
public actual fun create(
owner: ViewModelStoreOwner, // <- нас интересует этот тип
factory: Factory,
extras: CreationExtras,
): ViewModelProvider = ViewModelProvider(owner.viewModelStore, factory, extras)
}
Если интересно, почему метод create()
можно вызывать без передачи значений для параметров factory
и extras
(хоть они и обязательны):
ViewModelProvider.create(owner = this)
Это связано с тем, что код использует KMP (Kotlin Multiplatform). В expect-объявлении для create() уже заданы значения по умолчанию для factory и extras, поэтому передавать их явно необязательно.
public expect class ViewModelProvider {
....
public companion object {
public fun create(
owner: ViewModelStoreOwner,
factory: Factory = ViewModelProviders.getDefaultFactory(owner),
extras: CreationExtras = ViewModelProviders.getDefaultCreationExtras(owner),
): ViewModelProvider
}
....
}
Подробнее можно посмотреть в исходниках: ViewModelProvider.kt
Углубляемся в ViewModelStore / Owner
Получается что при вызове метода ViewModelProvider.create()
для параметра owner
мы передаем this
(само активити), и как можно догадаться, это означает, что activity реализует(наследуется) от интерфейса ViewModelStoreOwner
. Давайте взглянем на исходники этого интерфейса: ViewModelStoreOwner:
public interface [[[ViewModelStoreOwner |https://github.com/androidx/androidx/blob/androidx-main/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/ViewModelProvider.kt]]] {
/**
* The owned [ViewModelStore]
*/
public val viewModelStore: ViewModelStore
}
ViewModelStoreOwner
— это интерфейс с единственным полем, которое представляет собой ViewModelStore
(хранитель view models). От ViewModelStoreOwner
наследуются такие компоненты как: ComponentActivity, Fragment, * *NavBackStackEntry**.
Официальная документация гласит:
A scope that owns ViewModelStore. A responsibility of an implementation of this interface is to retain owned ViewModelStore during the configuration changes and call ViewModelStore. clear, when this scope is going to be destroyed.
Обязанности 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 по ключу:
private val map = mutableMapOf<String, ViewModel>()
По умолчанию в качестве ключа используется полное имя класса (включая его пакет). Этот ключ генерируется следующим образом в исходниках утилитного класса ViewModelProviders (не путать с ViewModelProvider):
private const val VIEW_MODEL_PROVIDER_DEFAULT_KEY: String = "androidx.lifecycle.ViewModelProvider.DefaultKey"
internal fun <T : ViewModel> getDefaultKey(modelClass: KClass<T>): String {
return "$VIEW_MODEL_PROVIDER_DEFAULT_KEY:$modelClass.canonicalName"
}
Таким образом, для 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()
:
public open class ViewModelStore {
private val map = mutableMapOf<String, ViewModel>()
...
/**
* Clears internal storage and notifies `ViewModel`s that they are no longer used.
*/
public fun clear() {
for (vm in map.values) {
vm.clear()
}
map.clear()
}
}
Давайте разберёмся, что здесь происходит. Когда наш ViewModelStoreOwner
(в лице ComponentActivity
или Fragment
) окончательно умирает (смерть не связана с пересозданием из-за изменения конфигураций), он вызывает метод clear()
у ViewModelStore
.
В методе clear()
цикл for
проходит по всем значениям (view models
), которые хранятся внутри внутреннего HashMap
, и вызывает у каждой ViewModel
внутренний метод clear()
. Этот метод, в свою очередь, инициирует вызов метода onCleared()
у нашей ViewModel
.
onCleared()
— это метод, который мы можем переопределить в своей ViewModel
, и он вызывается только в момент окончательного уничтожения ViewModel
, когда активити или фрагмент также окончательно завершают свою работу.
public actual abstract class ViewModel {
...
protected actual open fun onCleared() {} // <- метод onCleared, который можно переопределить
@MainThread
internal actual fun clear() {
impl?.clear()
onCleared() // <- вызов метода onCleared
}
}
Таким образом, метод clear()
гарантирует, что все ресурсы и фоновые задачи, связанные с ViewModel
, будут корректно освобождены перед уничтожением. Соответственно, сам метод viewModelStore.clear()
вызывается ViewModelStoreOwner
(в лице ComponentActivity
или Fragment
).
Давайте в качестве примера выберем ComponentActivity
, чтобы понять, как работает очистка.
Ниже приведён фрагмент кода из ComponentActivity
, который отслеживает её уничтожение и вызывает viewModelStore.clear()
:
@Suppress("LeakingThis")
lifecycle.addObserver(
LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) { // <- состояние ON_DESTROY является триггером
// Clear out the available context
contextAwareHelper.clearAvailableContext()
// And clear the ViewModelStore
if (!isChangingConfigurations) { // <- проверка на то можно ли очищать ViewModelStore
viewModelStore.clear() // <- очистка ViewModelStore
}
reportFullyDrawnExecutor.activityDestroyed()
}
}
)
В данном коде происходит добавление наблюдателя на жизненный цикл активности с использованием LifecycleEventObserver
. Когда активность достигает состояния ON_DESTROY
, запускается проверка, не происходит ли изменение конфигурации ( isChangingConfigurations
). Если активность действительно умирает окончательно (и не пересоздаётся), вызывается метод viewModelStore.clear()
, который очищает все связанные с активностью ViewModel
.
Мы видим, что проверка состояния ON_DESTROY
в сочетании с условием if (!isChangingConfigurations) позволяет убедиться в том, что причиной уничтожения не является изменение конфигурации. Только в этом случае очищается ViewModelStore и удаляются все экземпляры ViewModel, связанные с данной активностью.
В этой статье мы подробно разбираем внутренние методы класса
ComponentActivity
, начиная с версии **androidx.activity:activity:1.9.0-alpha01 **, когда он был переписан на Kotlin.Если у вас установлена более старая версия библиотеки, и вы видите реализацию на Java — не переживайте. Логика и основные методы остались прежними, поэтому все представленные концепции и объяснения будут актуальны.
ComponentActivity.onDestroy()
→getViewModelStore().clear()
→MyViewModel.onCleared()
Теперь мы разобрались с процессом очистки и уничтожения ViewModel
. Перейдём к следующему этапу — рассмотрим подробнее, как происходит создание объекта ViewModel
, когда мы передаём её в ViewModelProvider
:
ViewModelProvider.create(owner = this).get(MyViewModel::class)
Да, можно уточнить, что ViewModelProvider.create
— это функция с значениями по умолчанию. Например:
ViewModelProvider.create(owner = this).get(MyViewModel::class)
Ранее мы разобрали один из перегруженных методов ViewModelProvider.create
(функция с аргументами по умолчанию). Это фабричный метод, который принимает минимум ViewModelStore
или ViewModelStoreOwner
, создаёт объектViewModelProvider
и на этом завершает свою работу.
Теперь нас интересует следующий ключевой метод — get
, который принимает класс ViewModel
в качестве параметра. ViewModelProvider
делегирует свою работу классу ViewModelProviderImpl
:
public actual open class ViewModelProvider private constructor(
private val impl: ViewModelProviderImpl,
) {
...
@MainThread
public actual operator fun <T : ViewModel> get(modelClass: KClass<T>): T =
impl.getViewModel(modelClass) // <- вызов метода getViewModel, принадлежащий ViewModelProviderImpl
}
Разработчики Google вынесли общую логику создания ViewModel в отдельный объект ViewModelProviderImpl. Это позволило избежать дублирования кода на разных платформах в KMP. Причина в том, что expect-классы в Kotlin Multiplatform не могут содержать реализации методов по умолчанию. Если бы они могли, реализация находилась бы прямо внутри expect-версии ViewModelProvider, без необходимости выносить её в отдельный объект. Однако, из-за этого ограничения, была создана ViewModelProviderImpl, которая содержит общую логику создания ViewModel для всех платформ.
Оригинальный комментарий:
Kotlin Multiplatform does not support expect class with default implementation yet, so we extracted the common logic used by all platforms to this internal class.
Исходники метода getViewModel()
в ViewModelProviderImpl.kt
:
internal fun <T : ViewModel> getViewModel(
modelClass: KClass<T>,
key: String = ViewModelProviders.getDefaultKey(modelClass),
): T {
val viewModel = store[key] // 1. Достается viewmodel из ViewModelStore, если он существует
if (modelClass.isInstance(viewModel)) {
if (factory is ViewModelProvider.OnRequeryFactory) {
factory.onRequery(viewModel!!)
}
return viewModel as T
}
val extras = MutableCreationExtras(extras)
extras[ViewModelProviders.ViewModelKey] = key
// 2. Создается viewmodel и кладется в ViewModelStore
return createViewModel(factory, modelClass, extras).also { vm -> store.put(key, vm) }
}
При вызове ViewModelProvider.create()
под капотом вызывается метод getViewModel()
, который выполняет следующие шаги:
- Проверяет наличие объекта
ViewModel
вViewModelStore
по заданному ключу. Если объект уже существует, он возвращается. - Если объект не найден, создаётся новый экземпляр
ViewModel
, который затем кладётся вViewModelStore
для последующего использования.
Где ViewModelStore сохраняется?
Теперь, когда мы знаем полный процесс создания ViewModel
и её размещения в ViewModelStore
, возникает логичный вопрос: если все ViewModel
-и хранятся внутри ViewModelStore
, а сам ViewModelStore
находится в ComponentActivity
или Fragment
, которые реализуют интерфейс ViewModelStoreOwner
, то где и как хранится сам объект ViewModelStore
?
Для того чтобы найти ответ на вопрос о хранении ViewModelStore
, давайте посмотрим, как ComponentActivity
реализует интерфейс ViewModelStoreOwner
:
override val viewModelStore: ViewModelStore
get() {
checkNotNull(application) {
("Your activity is not yet attached to the " +
"Application instance. You can't request ViewModel before onCreate call.")
}
ensureViewModelStore()
return _viewModelStore!!
}
Мы видим, что вызывается метод ensureViewModelStore
, а затем возвращается поле _viewModelStore
.
// Lazily recreated from NonConfigurationInstances by val viewModelStore
private var _viewModelStore: ViewModelStore? = null
Поле _viewModelStore
не имеет значения по умолчанию, поэтому перед возвратом оно инициализируется внутри метода ensureViewModelStore
:
private fun ensureViewModelStore() {
if (_viewModelStore == null) {
// Извлекается ComponentActivity#NonConfigurationInstances из метода Activity#getLastNonConfigurationInstance()
val nc = lastNonConfigurationInstance as NonConfigurationInstances?
if (nc != null) {
// Восстанавливается ViewModelStore из NonConfigurationInstances
_viewModelStore = nc.viewModelStore
}
if (_viewModelStore == null) {
// Создается ViewModelStore если нет сохраненного внутри объекта NonConfigurationInstances
_viewModelStore = ViewModelStore()
}
}
}
Тут-то и начинается самое интересное. Если поле _viewModelStore
равно null
, сначала выполняется попытка получить его из метода getLastNonConfigurationInstance()
, который возвращает объект класса NonConfigurationInstances
.
Если ViewModelStore
отсутствует и там, это может означать одно из двух:
- Активность создаётся впервые и у неё ещё нет сохранённого
ViewModelStore
. - Система уничтожила процесс приложения (например, из-за нехватки памяти), а затем пользователь снова запустил приложение, из-за чего
ViewModelStore
не сохранился.
В любом из этих случаев создаётся новый экземпляр ViewModelStore
.
Самая неочевидная часть — это вызов метода getLastNonConfigurationInstance()
. Этот метод принадлежит классуActivity
, а класс NonConfigurationInstances
, у которого даже само название выглядит интригующе, объявлен вComponentActivity
:
internal class NonConfigurationInstances {
var custom: Any? = null
var viewModelStore: ViewModelStore? = null
}
Таким образом, объект 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
выглядят следующим образом:
@Suppress("deprecation")
final override fun onRetainNonConfigurationInstance(): Any? {
// Maintain backward compatibility.
val custom = onRetainCustomNonConfigurationInstance()
var viewModelStore = _viewModelStore
if (viewModelStore == null) {
// No one called getViewModelStore(), so see if there was an existing
// ViewModelStore from our last NonConfigurationInstance
val nc = lastNonConfigurationInstance as NonConfigurationInstances?
if (nc != null) {
viewModelStore = nc.viewModelStore
}
}
if (viewModelStore == null && custom == null) {
return null
}
val nci = NonConfigurationInstances()
nci.custom = custom
nci.viewModelStore = viewModelStore
return nci
}
Метод onRetainNonConfigurationInstance()
возвращает объект NonConfigurationInstances
, содержащий ссылку на ранее созданный ViewModelStore
.
Таким образом, при уничтожении активности (например, при повороте экрана) вызывается этот метод, и ViewModelStore
сохраняется в экземпляре NonConfigurationInstances
.
Когда активность пересоздаётся, объект NonConfigurationInstances
восстанавливается через вызов метода getLastNonConfigurationInstance()
, и из него извлекается сохранённый ViewModelStore
.
В методе onRetainNonConfigurationInstance
реализована логика получения уже существующего ViewModelStore
и объекта Custom
(если он есть). После получения этих объектов они кладутся в экземпляр класса NonConfigurationInstances
, который затем возвращается из метода.
Метод onRetainNonConfigurationInstance
создаёт объект класса NonConfigurationInstances
, помещает внутрь viewModelStore
и кастомный объект, а затем возвращает его. Возникает вопрос: кто именно вызывает этот метод?
Вызывающий метод внутри самого класса Activity(самый базовый Activity от которого наследуются все остальные):
NonConfigurationInstances retainNonConfigurationInstances() {
Object activity = onRetainNonConfigurationInstance(); // <- вызов onRetainNonConfigurationInstance()
//...code
NonConfigurationInstances nci = new NonConfigurationInstances();
nci.activity = activity; // <- присвоение извлеченного объекта из onRetainNonConfigurationInstance()
nci.children = children;
nci.fragments = fragments;
nci.loaders = loaders;
if (mVoiceInteractor != null) {
mVoiceInteractor.retainInstance();
nci.voiceInteractor = mVoiceInteractor;
}
return nci;
}
Как видно, сам класс Activity
вызывает метод onRetainNonConfigurationInstance
с которым мы ранее познакомились и сохраняет результат в полеactivity
класса NonConfigurationInstances
. При этом мы снова сталкиваемся с классом NonConfigurationInstances
, но на этот раз он объявлен в самой Activity
и имеет дополнительные поля:
static final class NonConfigurationInstances {
Object activity; // <- Здесь и будет храниться ComponentActivity.NonConfigurationInstances
HashMap<String, Object> children;
FragmentManagerNonConfig fragments;
ArrayMap<String, LoaderManager> loaders;
VoiceInteractor voiceInteractor;
}
Чтобы устранить путаницу:
- Объект
ViewModelStore
хранится внутриComponentActivity#NonConfigurationInstances
. - Сам объект
ComponentActivity#NonConfigurationInstances
хранится вActivity#NonConfigurationInstance
. - Это достигается через метод
retainNonConfigurationInstances()
классаActivity
.
Но кто же вызывает метод retainNonConfigurationInstances
() и где хранится конечный объект Activity#NonConfigurationInstance
, который содержит ViewModelStore
?
Ответ на этот вопрос кроется в классе ActivityThread
, который отвечает за управление жизненным циклом активностей и их взаимодействие с системой. Именно этот класс обрабатывает создание, уничтожение и повторное создание активности, а также отвечает за сохранение и восстановление данных при изменениях конфигурации.
Метод из ActivityThread
, который непосредственно вызывает Activity.retainNonConfigurationInstances()
, называется ActivityThread.performDestroyActivity()
.
Рассмотрим его исходники в классе ActivityThread
, далее исходники:
void performDestroyActivity(ActivityClientRecord r, boolean finishing,
boolean getNonConfigInstance, String reason) {
//...
if (getNonConfigInstance) {
try {
// Вызов Activity.retainNonConfigurationInstances()
// и сохранение в r.lastNonConfigurationInstances
r.lastNonConfigurationInstances = r.activity.retainNonConfigurationInstances();
} catch (Exception e) {
if (!mInstrumentation.onException(r.activity, e)) {
throw new RuntimeException("Unable to retain activity "
+ r.intent.getComponent().toShortString() + ": " + e.toString(), e);
}
}
}
//...
}
Чтобы найти исходники ActivityThread
, достаточно в Android Studio воспользоваться поиском по имени класса: ActivityThread
.
Или зайти в исходники Android по одной из ссылок:
После вызова метода retainNonConfigurationInstances()
результат сохраняется в поле lastNonConfigurationInstances
объекта ActivityClientRecord
:
r.lastNonConfigurationInstances = r.activity.retainNonConfigurationInstances();
Класс 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()
Важно понимать, что на более высоком уровне в этой цепочке стоят такие классы, как ClientTransactionItem, ClientTransaction и ClientLifecycleManager, а еще выше — сама система, которая управляет взаимодействием устройства с сенсорами и другими компонентами. Однако, углубляться дальше в эту цепочку мы не будем, так как всего через пару слоев окажемся на уровне межпроцессного взаимодействия (IPC) и работы системы с процессами.
На вершине вызовов находится метод ActivityTransactionItem.execute()
, который запускает цепочку: сначала вызывает getActivityClientRecord()
, а затем тот вызывает ClientTransactionHandler.getActivityClient()
.
public abstract class ActivityTransactionItem extends ClientTransactionItem {
@Override
public final void execute(ClientTransactionHandler client, IBinder token,
PendingTransactionActions pendingActions) {
final ActivityClientRecord r = getActivityClientRecord(client, token); // <- Вызов getActivityClientRecord
execute(client, r, pendingActions);
}
@NonNull
ActivityClientRecord getActivityClientRecord(
@NonNull ClientTransactionHandler client, IBinder token) {
final ActivityClientRecord r = client.getActivityClient(token); // <- получение клиент от ClientTransactionHandler(ActivityThread)
...
return r;
}
}
ClientTransactionHandler
— это абстрактный класс, и одна из его реализаций — класс ActivityThread
, с которым мы уже успели познакомиться.
public final class ActivityThread extends ClientTransactionHandler
implements ActivityThreadInternal {
...
@Override
public ActivityClientRecord getActivityClient(IBinder token) {
return mActivities.get(token); // <- Возвращает из Map ActivityClientRecord по ключу
}
...
}
От ActivityTransactionItem - наследуется класс ActivityRelaunchItem, который и запускает у ActivityThread метод handleRelaunchActivity
:
public class ActivityRelaunchItem extends ActivityTransactionItem {
@Override
public void execute(@NonNull ClientTransactionHandler client, @NonNull ActivityClientRecord r,
@NonNull PendingTransactionActions pendingActions) {
...
client.handleRelaunchActivity(mActivityClientRecord, pendingActions);
...
}
}
Все запущенные активности внутри нашего приложения хранятся в коллекций Map в объекте класса ActivityThread
:
/**
* Maps from activity token to local record of running activities in this process.
* ....
*/
@UnsupportedAppUsage
final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();
Таким образом, мы наконец выяснили, что наша 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
, это будет в рамках следующих статьей.
Обсуждение
Пока нет комментариев. Будьте первым, кто поделится своими мыслями!