ViewModel в Activity под капотом: как она выживает при пересоздании

Что на самом деле происходит с ViewModel, когда Activity уничтожается и создаётся заново? Разбираем всю цепочку: ViewModelStore, NonConfigurationInstances, ActivityThread и ActivityClientRecord. Подробно показываем, где и как хранится состояние, и как Android магически восстанавливает всё при конфигурационных изменениях.

20мин чтенияAndroid
Share:

Введение

В статье не рассматривается работа с ViewModel, предполагается, что эта тема уже знакома. Основное внимание уделяется тому, как ViewModel переживает изменение конфигурации. Но для начала — небольшое введение в ViewModel.

ViewModel - компонент архитектурного паттерна MVVM, который был предоставлен Google как примитив позволяющий пережить изменение конфигураций. Изменение конфигураций в свою очередь - это состояние, заставляющая activity/fragment пересоздаваться, это именно то состояние которое может пережить ViewModel. Популярные конфигурации которые приводят к пересозданию Activity:

  1. Изменение ориентаций экрана(screenOrientation): portrait/landscape
  2. Изменение направления экрана(layoutDirection): rtl/ltr
  3. Изменение языка приложения(locale)
  4. Изменение размера шрифтов/соотношение экрана

Есть конечно способ сообщать системе о том что пересоздавать 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:

  1. Хранение ViewModelStore во время изменения конфигураций.
  2. Очистка 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 — не переживайте. Логика и основные методы остались прежними, поэтому все представленные концепции и объяснения будут актуальны.

Процесс очистки ViewModel при уничтожении активности:
Уничтожение Activity (не связано с изменением конфигураций) ComponentActivity.onDestroy()
Очистка ViewModelStore getViewModelStore().clear()
Оповещение ViewModel 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(), который выполняет следующие шаги:

  1. Проверяет наличие объекта ViewModel в ViewModelStore по заданному ключу. Если объект уже существует, он возвращается.
  2. Если объект не найден, создаётся новый экземпляр 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 отсутствует и там, это может означать одно из двух:

  1. Активность создаётся впервые и у неё ещё нет сохранённого ViewModelStore.
  2. Система уничтожила процесс приложения (например, из-за нехватки памяти), а затем пользователь снова запустил приложение, из-за чего 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() в рамках системного вызова.

Последовательность вызовов:

  1. ActivityTransactionItem.execute()
  2. ActivityRelaunchItem.execute()
  3. ActivityThread.handleRelaunchActivity()
  4. ActivityThread.handleRelaunchActivityInner()
  5. ActivityThread.handleDestroyActivity()
  6. 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 выглядит следующим образом:

  1. ViewModel хранится внутри ViewModelStore.
  2. ViewModelStore хранится в ComponentActivity#NonConfigurationInstances.
  3. ComponentActivity#NonConfigurationInstances хранится в Activity#NonConfigurationInstance.
  4. Activity#NonConfigurationInstance хранится в ActivityClientRecord.
  5. 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, и за счёт чего это вообще возможно.

Мы проследили всю цепочку: ViewModelViewModelStoreComponentActivity#NonConfigurationInstancesActivity#NonConfigurationInstancesActivityClientRecordActivityThread. Именно в этой глубокой вложенности и заключается ответ: ViewModel выживает, потому что сохраняется не в Activity напрямую, а в объекте, который система сама передаёт новой Activity при конфигурационных изменениях.

Сам ViewModelStore создаётся либо с нуля, либо восстанавливается через getLastNonConfigurationInstance(). Он очищается только в onDestroy(), если isChangingConfigurations == false, — то есть если Activity действительно умирает, а не пересоздаётся.

Под капотом всё это обеспечивается ActivityThread, который сохраняет NonConfigurationInstances в ActivityClientRecord, а потом передаёт в метод attach() при создании новой Activity. ActivityThread — синглтон, живущий столько же, сколько и процесс, и именно он является опорной точкой, через которую проходит вся цепочка восстановления.

ViewModel выживает не потому, что её кто-то “сохраняет” — а потому, что никто её не убивает. Пока жив ActivityThread, жив и ViewModelStore.

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

Обсуждение

Комментарии