SavedStateHandle and Bundle Under the Hood: How Android Saves State

A comprehensive article about how Android saves and restores state: from onSaveInstanceState and Bundle to modern architecture with SavedStateHandle and SavedStateRegistry. How everything is connected to the lifecycle of Activity, Fragment, and ViewModel, what roles ActivityThread, Instrumentation, and ActivityClientRecord play. A complete low-level data flow that demystifies all the magic moments.

30min readAndroid
Share:

Introduction

This is a continuation of the three previous articles.

  1. In the first one, we figured out where ViewModelStore is ultimately stored in the case of Activity,
  2. In the second — how this is arranged in Fragment,
  3. In the third, where ViewModels are stored when we use Compose (or even just View).

In this article, we’ll examine where SavedStateHandle is stored, check SavedStateHandle vs onSaveInstanceState vs ViewModel(ViewModelStore), understand the connection between SavedStateHandle and ViewModel. And we’ll learn the answer to the main question: where Bundle is stored. But, as always, we’ll start with the basics.

Basics

The article will not describe how to work with these APIs, but will tell you how they are arranged internally, so I will assume that you have already worked with them.

As always, let’s start with the basics — let’s give definitions for SavedStateHandle, onSaveInstanceState, and ViewModel:

ViewModel — a component of the MVVM architectural pattern, provided by Google as a primitive that allows surviving configuration changes. Configuration change — this is a state that causes Activity/Fragment to be recreated; it is precisely this state that ViewModel can survive. Unfortunately, this is where ViewModel’s responsibilities for storing data in the Android context end.

If the application process dies or is interrupted, ViewModel won’t cope; then the good old onSaveInstanceState/onRestoreInstanceState methods come to the rescue.

onSaveInstanceState/onRestoreInstanceState — lifecycle methods of Activity, Fragment, and even View (yes, View can also save state), which allow saving and restoring the temporary state of the user interface during configuration changes (for example, when rotating the screen) or when the activity is completely destroyed due to resource shortage. In onSaveInstanceState, data is saved in Bundle, which is automatically passed to onRestoreInstanceState when the activity is restored.

This is the basic mechanism for storing primitive types (and their arrays), Parcelable/Serializable, and a couple of other native Android types. These methods require explicitly specifying what exactly needs to be saved, and the logic is written inside Activity and Fragment. Most architectural patterns (MVI, MVVM) state that View (Fragment/Activity/Compose) should be as simple as possible and contain no logic other than displaying data, so direct use of these methods is now giving way to Saved State API, which integrates well with ViewModel, endowing it not only with the ability to “save” data from configuration changes, but also to save serializable data when the process is destroyed or stopped by the system.

Saved State API — a modern alternative to onSaveInstanceState/onRestoreInstanceState, providing more flexible state management, especially in conjunction with ViewModel.

SavedStateHandle — an object passed to the ViewModel constructor, which allows safely saving and restoring data even after process destruction. Unlike static onSaveInstanceState, SavedStateHandle also allows subscribing to Flow and LiveData of the data it stores and restores. It is automatically integrated with ViewModel and supports state saving during configuration changes, as well as complete destruction of the application process. An additional advantage is the ability to subscribe to changes in SavedStateHandle values and get reactive behavior directly in ViewModel.

By “process destruction or interruption” mentioned in the article, we mean a situation when the application is in the background and remains in the task stack. Usually this happens when the user minimizes the application without closing it. After some time of inactivity, the system may stop the process. This should not be confused with the case when the user manually closes the application — this is a different scenario.

onSaveInstanceState / onRestoreInstanceState

Let’s also refresh our memory about the onSaveInstanceState and onRestoreInstanceState methods:

class RestoreActivity : AppCompatActivity() {

    private var counter = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Restore value on recreation
        counter = savedInstanceState?.getInt("counter_key") ?: 0
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        // Restore value on recreation
        counter = savedInstanceState.getInt("counter_key")
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        // Save value
        outState.putInt("counter_key", counter)
        Log.d("RestoreActivity", "onSaveInstanceState: Counter saved = $counter")
    }
}

onSaveInstanceState — called to get the Activity’s state before it is destroyed, so it can be restored in the onCreate or onRestoreInstanceState methods. The Bundle filled in this method will be passed to both methods.

This method is called before the Activity can be destroyed, so that when it is recreated, it can restore its state. It should not be confused with lifecycle methods such as onPause (always called, called when Activity partially loses focus) or onStop (when Activity becomes invisible).

  • Example when onPause and onStop are called, but onSaveInstanceState is not: when returning from Activity B to Activity A. In this case, the state of B doesn’t need to be restored, so onSaveInstanceState for B is not called.
  • Another example: if Activity B is launched over Activity A, but A remains in memory, then onSaveInstanceState for A is also not called, since the Activity remains in memory and doesn’t need to save its state.

The default implementation of this method automatically saves most of the user interface state, calling onSaveInstanceState() on each View in the hierarchy that has an ID, and also saves the ID of the element that was in focus. Restoration of this data occurs in the standard implementation of onRestoreInstanceState(). If you override the method to save additional information, it’s recommended to call the default implementation through

super.onSaveInstanceState(outState)

— otherwise you’ll have to manually save the state of all Views.

If the method is called, this will happen after onStop for applications targeting platforms starting with Android P. For earlier Android versions, this method will be called before onStop, and there are no guarantees whether it will be called before or after onPause.

Documentation states:

If called, this method will occur after onStop for applications targeting platforms starting with android.os.Build.VERSION_CODES.P. For applications targeting earlier platform versions this method will occur before onStop and there are no guarantees about whether it will occur before or after onPause.

onRestoreInstanceState — this method is called after onStart, when the activity is re-initialized from a previously saved state passed in savedInstanceState. Most implementations use the onCreate method to restore state, but sometimes it’s convenient to do it here, after all initialization is complete, or so that subclasses can decide whether to use your default implementation. The standard implementation of this method restores the state of views (View) that was previously frozen by the onSaveInstanceState method. This method is called between onStart and onPostCreate. It triggers only when the activity is recreated; the method is not called if onStart was called for any other reason (for example, when transitioning from background to foreground).

Let’s temporarily forget about this example, we’ll encounter them again later in lower-level call chains.

Saved State Api

Starting with version 1.3.0-alpha02, androidx.savedstate:savedstate began supporting Kotlin Multiplatform. Now SavedState works not only on Android (Bundle), but also on iOS, JVM, Linux, and macOS Map<String, Any>, maintaining compatibility.

To understand how Saved State Api works, let’s rewrite the example above with onSaveInstanceState and onRestoreInstanceState using Saved State Api, doing exactly the same thing:

class RestoreActivity : AppCompatActivity() {

    private var counter = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Restore value on recreation
        counter = savedStateRegistry.consumeRestoredStateForKey("counter_key")?.getInt("counter", 0) ?: 0

        savedStateRegistry.registerSavedStateProvider(
            key = "counter_key",
            provider = object : SavedStateRegistry.SavedStateProvider {
                override fun saveState(): SavedState {
                    return SavedState(bundleOf("counter" to counter))
                }
            }
        )
    }
}

We call the registerSavedStateProvider method on the savedStateRegistry object, where we pass the key and an anonymous SavedStateRegistry.SavedStateProvider object that returns a Bundle wrapped in a SavedState object. Let’s now define what the SavedState type represents. If we look at the source code, specifically the expect logic, the type is described as follows:

androidx.savedstate.SavedState.kt:

/**
 * An opaque (empty) common type that holds saveable values to be saved and restored by native
 * platforms that have a concept of System-initiated Process Death.
 *
 * That means, the OS will give the chance for the process to keep the state of the application
 * (normally using a serialization mechanism), and allow the app to restore its state later. That is
 * commonly referred to as "state restoration".
 * ...
 */
public expect class SavedState

In the context of android, we’re interested in the actual implementation, so next is the android-specific actual:

androidx.savedstate.SavedState.android.kt:

public actual typealias SavedState = android.os.Bundle

As we can see, in Android there’s actually no type like SavedState, in the actual implementation it’s just a typealias that refers to the same good old native Bundle class, so always imagine that where SavedState is used — actually the Bundle class is used, so nothing prevents us from abandoning the extra wrapper and returning Bundle directly:

savedStateRegistry.registerSavedStateProvider(
    key = "counter_key",
    provider = object : SavedStateRegistry.SavedStateProvider {
        override fun saveState(): Bundle {
            return bundleOf("counter" to counter)
        }
    }
)

Now that we’ve figured this out, let’s go into the source code of the registerSavedStateProvider and consumeRestoredStateForKey methods. These methods are called on the savedStateRegistry variable, which has the type SavedStateRegistry. Let’s quickly learn the definition of this class:

SavedStateRegistry - manages saving and restoring saved state so that data is not lost when components are recreated. The implementation is bound to SavedStateRegistryImpl, which is responsible for the actual storage and restoration of data. Interface for connecting components that consume and contribute data to the saved state. The object has the same lifecycle as its owner (Activity or Fragment): when Activity or Fragment is recreated (for example, after process destruction or configuration change), a new instance of this object is created.

But where the savedStateRegistry variable comes from inside Activity we’ll look at later, for now it’s enough to know that Activity has it. Next, the source code of the registerSavedStateProvider and consumeRestoredStateForKey methods belonging to the SavedStateRegistry(expect) class:

androidx.savedstate.SavedStateRegistry.kt

public expect class SavedStateRegistry internal constructor(
    impl: SavedStateRegistryImpl,
) {

    /** This interface marks a component that contributes to saved state. */
    public fun interface SavedStateProvider {

        public fun saveState(): SavedState
    }

    ...
    public val isRestored: Boolean
    ...
    @MainThread
    public fun consumeRestoredStateForKey(key: String): SavedState?
    ...
    @MainThread
    public fun registerSavedStateProvider(key: String, provider: SavedStateProvider)
    ...
    public fun getSavedStateProvider(key: String): SavedStateProvider?
    ...
    @MainThread
    public fun unregisterSavedStateProvider(key: String)
}

As we can see, there are actually many methods in SavedStateRegistry. For our article, it’s enough to understand how the registerSavedStateProvider and consumeRestoredStateForKey methods work, but to have at least some understanding, let’s quickly go through each one:

  1. consumeRestoredStateForKey — extracts and removes from memory the SavedState(Bundle) that was registered using registerSavedStateProvider. When called again, it returns null.

  2. registerSavedStateProvider — registers a SavedStateProvider with the specified key. This provider will be used to save state when onSaveInstanceState is called.

  3. getSavedStateProvider — returns the registered SavedStateProvider by key or null if not found.

  4. unregisterSavedStateProvider — removes a previously registered SavedStateProvider from the registry by the passed key.

  5. SavedStateProvider — interface providing a SavedState(Bundle) object when saving state.

  6. isRestored — returns true if the state was restored after component creation.

The expect versions lack implementations — they only have method signatures. We also looked at the source code of the SavedStateProvider interface, which is a callback for getting the Bundle to be saved. To see the implementation of the registerSavedStateProvider method, you need to find the actual implementation, and then go to the actual implementation of SavedStateRegistry.

androidx.savedstate.SavedStateRegistry.android.kt:

public actual class SavedStateRegistry internal actual constructor(
    private val impl: SavedStateRegistryImpl,
) {

    @get:MainThread
    public actual val isRestored: Boolean
        get() = impl.isRestored

    @MainThread
    public actual fun consumeRestoredStateForKey(key: String): SavedState? =
        impl.consumeRestoredStateForKey(key)

    @MainThread
    public actual fun registerSavedStateProvider(key: String, provider: SavedStateProvider) {
        impl.registerSavedStateProvider(key, provider)
    }

    public actual fun getSavedStateProvider(key: String): SavedStateProvider? =
        impl.getSavedStateProvider(key)

    @MainThread
    public actual fun unregisterSavedStateProvider(key: String) {
        impl.unregisterSavedStateProvider(key)
    }

    public actual fun interface SavedStateProvider {
        public actual fun saveState(): SavedState
    }
    ...
}

The actual implementation of SavedStateRegistry delegates all calls of its methods to the ready implementation SavedStateRegistryImpl, so let’s look at SavedStateRegistryImpl:

internal class SavedStateRegistryImpl(
    private val owner: SavedStateRegistryOwner,
    internal val onAttach: () -> Unit = {},
) {

    private val keyToProviders = mutableMapOf<String, SavedStateProvider>()
    private var restoredState: SavedState? = null

    @MainThread
    fun consumeRestoredStateForKey(key: String): SavedState? {
        ...
        val state = restoredState ?: return null

        val consumed = state.read { if (contains(key)) getSavedState(key) else null }
        state.write { remove(key) }
        if (state.read { isEmpty() }) {
            restoredState = null
        }

        return consumed
    }

    @MainThread
    fun registerSavedStateProvider(key: String, provider: SavedStateProvider) {
        ..
        keyToProviders[key] = provider
        ...
    }
    ...
}

Main methods for saving, let’s just understand what’s happening here:

  1. consumeRestoredStateForKey - gets the value from restoredState(Bundle) by key, after getting the value, removes the value and key from restoredState(Bundle), restoredState is the most root Bundle that stores all other bundles inside itself
  2. registerSavedStateProvider - simply adds the SavedStateProvider object inside the keyToProviders map

These methods are very high-level and don’t reveal how the data is actually saved, so we need to dig deeper — inside the same SavedStateRegistryImpl class:

internal class SavedStateRegistryImpl(
    private val owner: SavedStateRegistryOwner,
    internal val onAttach: () -> Unit = {},
) {
    private val keyToProviders = mutableMapOf<String, SavedStateProvider>()
    private var restoredState: SavedState? = null

    @MainThread
    internal fun performRestore(savedState: SavedState?) {
        ...
        restoredState =
            savedState?.read {
                if (contains(SAVED_COMPONENTS_KEY)) getSavedState(SAVED_COMPONENTS_KEY) else null
            }
        isRestored = true
    }

    @MainThread
    internal fun performSave(outBundle: SavedState) {
        val inState = savedState {
            restoredState?.let { putAll(it) }
            synchronized(lock) {
                for ((key, provider) in keyToProviders) {
                    putSavedState(key, provider.saveState())
                }
            }
        }

        if (inState.read { !isEmpty() }) {
            outBundle.write { putSavedState(SAVED_COMPONENTS_KEY, inState) }
        }
    }

    private companion object {
        private const val SAVED_COMPONENTS_KEY =
            "androidx.lifecycle.BundlableSavedStateRegistry.key"
    }
}
  1. performSave — called when Activity or Fragment transitions to pause -> stop state, i.e., at the moment onSaveInstanceState is called. This method is responsible for saving the state of all SavedStateProviders registered through registerSavedStateProvider. Inside the method, an inState object of type SavedState (essentially, it’s the Bundle itself) is created. If there’s already data in restoredState, it’s added to inState. Then, in a synchronized block, all registered SavedStateProviders are iterated through, the saveState() method is called, and the results are saved in inState. At the end, if inState is not empty, its contents are written to the outBundle parameter under the key SAVED_COMPONENTS_KEY.

  2. performRestore — called when creating or restoring Activity or Fragment. This method simply reads from savedState the value by key SAVED_COMPONENTS_KEY, if it exists. The found value (nested SavedState) is saved in the restoredState variable, so that it can later be passed to the appropriate components.

So far we’ve seen how the saving and registration logic works, now it remains to understand who calls the performSave and performRestore methods and at what moment.

This logic is managed by SavedStateRegistryController. Since Saved State Api is also on KMP, it’s better to immediately look at the actual version:

public actual class SavedStateRegistryController private actual constructor(
    private val impl: SavedStateRegistryImpl,
) {

    public actual val savedStateRegistry: SavedStateRegistry = SavedStateRegistry(impl)

    @MainThread
    public actual fun performAttach() {
        impl.performAttach()
    }

    @MainThread
    public actual fun performRestore(savedState: SavedState?) {
        impl.performRestore(savedState)
    }

    @MainThread
    public actual fun performSave(outBundle: SavedState) {
        impl.performSave(outBundle)
    }

    public actual companion object {

        @JvmStatic
        public actual fun create(owner: SavedStateRegistryOwner): SavedStateRegistryController {
            val impl =
                SavedStateRegistryImpl(
                    owner = owner,
                    onAttach = { owner.lifecycle.addObserver(Recreator(owner)) },
                )
            return SavedStateRegistryController(impl)
        }
    }
}

And we see that calls to the SavedStateRegistryImpl.performSave and SavedStateRegistryImpl.performRestore methods are controlled by the same-named methods from SavedStateRegistryController.

We also see the create method, which creates SavedStateRegistryImpl, passes it to the SavedStateRegistryController constructor, and returns the SavedStateRegistryController itself.

Next, it remains only to understand where the SavedStateRegistryController methods themselves are called from. At the beginning of the article, we postponed the analysis of the source of the savedStateRegistry field in Activity. Now is the right time to figure it out.

Inside Activity, the savedStateRegistry field is available to us. This is possible because Activity implements the SavedStateRegistryOwner interface. If you look at the source code, you can see that ComponentActivity implements SavedStateRegistryOwner. Actually, ComponentActivity implements many interfaces, but below is a fragment with the other parents omitted:

open class ComponentActivity() : ..., SavedStateRegistryOwner, ... {

    private val savedStateRegistryController: SavedStateRegistryController =
        SavedStateRegistryController.create(this)

    final override val savedStateRegistry: SavedStateRegistry
        get() = savedStateRegistryController.savedStateRegistry
}

SavedStateRegistryOwner - is just an interface that stores SavedStateRegistry inside itself, it’s implemented by Activity, Fragment, and NavBackStackEntry, it looks like this:

public interface SavedStateRegistryOwner : androidx.lifecycle.LifecycleOwner {
    /** The [SavedStateRegistry] owned by this SavedStateRegistryOwner */
    public val savedStateRegistry: SavedStateRegistry
}

SavedStateRegistry is available in any component implementing the SavedStateRegistryOwner interface. This interface is possessed by:

  • ComponentActivity — this is the base class for all modern Activitys.

    open class ComponentActivity() : ..., SavedStateRegistryOwner, ... {
    
        private val savedStateRegistryController: SavedStateRegistryController =
            SavedStateRegistryController.create(this)
    
        final override val savedStateRegistry: SavedStateRegistry
            get() = savedStateRegistryController.savedStateRegistry
    }
  • Fragment — any Fragment also implements this interface.

    public class Fragment implements ...SavedStateRegistryOwner,...{
    
        SavedStateRegistryController mSavedStateRegistryController;
    
        @NonNull
        @Override
        public final SavedStateRegistry getSavedStateRegistry() {
            return mSavedStateRegistryController.getSavedStateRegistry();
        }
    }
  • NavBackStackEntry - navigation component from Jetpack Navigation

    public expect class NavBackStackEntry : ..., SavedStateRegistryOwner {
    
        override val savedStateRegistry: SavedStateRegistry
    
    }

We’ve figured out a big chain of calls, let’s look at it visually:

expect -> SavedStateRegistryController.performSave
  -> actual SavedStateRegistryController.performSave
  -> expect SavedStateRegistry
  -> actual SavedStateRegistry
  -> SavedStateRegistryImpl.performSave
  -> SavedStateProvider.saveState()
  -> // Bundle

We won’t dive into the workings of Fragment and NavBackStackEntry — let’s only figure out Activity. At this point, we understand that ultimately all calls go to SavedStateRegistryController. Let’s see how Activity interacts with it:

The performRestore method of SavedStateRegistryController, responsible for restoring data from Bundle, is called inside ComponentActivity.onCreate, and the performSave method, which saves data to Bundle, is called inside ComponentActivity.onSaveInstanceState.

open class ComponentActivity() : ..., SavedStateRegistryOwner, ... {

    override fun onCreate(savedInstanceState: Bundle?) {
        savedStateRegistryController.performRestore(savedInstanceState)
        super.onCreate(savedInstanceState)
        ...
    }

    @CallSuper
    override fun onSaveInstanceState(outState: Bundle) {
        ...
        super.onSaveInstanceState(outState)
        savedStateRegistryController.performSave(outState)
    }
}

Here is the very point where onSaveInstanceState / onRestoreInstanceState are combined with SavedStateRegistryController / SavedStateRegistry.

Now let’s switch to ViewModel and its SavedStateHandle to understand how it fits into all this logic. First, let’s declare a regular ViewModel, but pass SavedStateHandle in the constructor:

class MyViewModel(val savedStateHandle: SavedStateHandle) : ViewModel()

As mentioned at the beginning of the article, this is not a guide on how to use Saved State Api, but rather an answer to the question of how it works under the hood

Next, let’s try to initialize our ViewModel in Activity:

class MainActivity : ComponentActivity() {

    private lateinit var viewModel: MyViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewModel = ViewModelProvider.create(this).get(MyViewModel::class)
    }

}

At first glance, you might expect a crash when launching the application, because if a ViewModel takes any parameter as input, then you need a ViewModel factory, i.e., ViewModelProvider.Factory, where we manually have to somehow put the required parameter into the constructor. And in our example, the constructor is not empty, but if we run this code, there will be no crash or error, everything will launch and initialize properly. Why is that?

Developers at Google knew that it would often be necessary to pass SavedStateHandle to ViewModel, and so that developers wouldn’t have to create a factory for passing it every time - there is a ready-made factory that works under the hood, and there are also ready-made classes like:

AbstractSavedStateViewModelFactory - starting from lifecycle-viewmodel-savedstate-android-2.9.0 - declared deprecated SavedStateViewModelFactory - currently relevant for creating ViewModel with SavedStateHandle

Let’s now look at how this works at the Activity level. We’ve already looked at the ViewModelProvider/ViewModel logic in previous articles, now let’s just go through the topic that interests us. When we access ViewModelProvider.create:

public expect class ViewModelProvider {
    public companion object {
        ...
        public fun create(
            owner: ViewModelStoreOwner,
            factory: Factory = ViewModelProviders.getDefaultFactory(owner),
            extras: CreationExtras = ViewModelProviders.getDefaultCreationExtras(owner),
        ): ViewModelProvider

    }
}

We see that as factory there’s a call to the method ViewModelProviders.getDefaultFactory(owner), let’s look at its source code too:

internal object ViewModelProviders {
    internal fun getDefaultFactory(owner: ViewModelStoreOwner): ViewModelProvider.Factory =
        if (owner is HasDefaultViewModelProviderFactory) {
            owner.defaultViewModelProviderFactory
        } else {
            DefaultViewModelProviderFactory
        }
}

ViewModelProviders — this is a utility class, don’t confuse it with ViewModelProvider.

In this method, we’re interested in the check for is HasDefaultViewModelProviderFactory:

if (owner is HasDefaultViewModelProviderFactory) {
    owner.defaultViewModelProviderFactory
}

If owner (ViewModelStoreOwner, for example Activity or Fragment) implements the HasDefaultViewModelProviderFactory interface, then the defaultViewModelProviderFactory field is taken from it. The HasDefaultViewModelProviderFactory interface looks like this:

androidx.lifecycle.HasDefaultViewModelProviderFactory.android.kt

public interface HasDefaultViewModelProviderFactory {

    public val defaultViewModelProviderFactory: ViewModelProvider.Factory

    public val defaultViewModelCreationExtras: CreationExtras
        get() = CreationExtras.Empty
}

Implementation of the HasDefaultViewModelProviderFactory interface in Activity:

open class ComponentActivity() : ..., SavedStateRegistryOwner, HasDefaultViewModelProviderFactory, ... {
    ...
    override val defaultViewModelProviderFactory: ViewModelProvider.Factory by lazy {
        SavedStateViewModelFactory(application, this, if (intent != null) intent.extras else null)
    }

    @get:CallSuper
    override val defaultViewModelCreationExtras: CreationExtras
        /**
         * {@inheritDoc}
         *
         * The extras of [getIntent] when this is first called will be used as the defaults to any
         * [androidx.lifecycle.SavedStateHandle] passed to a view model created using this extra.
         */
        get() {
            val extras = MutableCreationExtras()
            if (application != null) {
                extras[APPLICATION_KEY] = application
            }
            extras[SAVED_STATE_REGISTRY_OWNER_KEY] = this
            extras[VIEW_MODEL_STORE_OWNER_KEY] = this
            val intentExtras = intent?.extras
            if (intentExtras != null) {
                extras[DEFAULT_ARGS_KEY] = intentExtras
            }
            return extras
        }
    ...
}

Two very important things happen here:

  1. defaultViewModelProviderFactorySavedStateViewModelFactory is used as the default factory.
  2. defaultViewModelCreationExtrasSavedStateRegistryOwner is placed in CreationExtras under the key SAVED_STATE_REGISTRY_OWNER_KEY and ViewModelStoreOwner under the key VIEW_MODEL_STORE_OWNER_KEY.

This is the key part of how SavedStateHandle ultimately connects to ViewModel and to SavedStateRegistryOwner

To understand how SavedStateHandle is created and restored for ViewModel, let’s figure out what happens in SavedStateViewModelFactory

androidx.lifecycle.SavedStateViewModelFactory.android.kt:

public actual class SavedStateViewModelFactory :
    ViewModelProvider.OnRequeryFactory, ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
        ...
        return if (
            extras[SAVED_STATE_REGISTRY_OWNER_KEY] != null &&
            extras[VIEW_MODEL_STORE_OWNER_KEY] != null
        ) {
            ...
            newInstance(modelClass, constructor, extras.createSavedStateHandle())
            ...
        }
        ...
    }
}

internal fun <T : ViewModel?> newInstance(
    modelClass: Class<T>,
    constructor: Constructor<T>,
    vararg params: Any
): T {
    return try {
        constructor.newInstance(*params)
    }
    ...
}

The logic from the source code is shortened here to focus on the main point. Inside the factory’s create method, it checks whether extras contains fields with keys SAVED_STATE_REGISTRY_OWNER_KEY and VIEW_MODEL_STORE_OWNER_KEY. If they do — the newInstance method is called, which uses reflection to call the constructor and passes parameters, one of which is SavedStateHandle.

But we’re interested in another moment. Let’s pay attention to the createSavedStateHandle() call:

newInstance(modelClass, constructor, extras.createSavedStateHandle())

What happens inside createSavedStateHandle()? To understand how SavedStateHandle is created, we need to look at the source code of this method:

androidx.lifecycle.SavedStateHandleSupport.kt:

@MainThread
public fun CreationExtras.createSavedStateHandle(): SavedStateHandle {
    val savedStateRegistryOwner =
        this[SAVED_STATE_REGISTRY_OWNER_KEY]
            ?: throw IllegalArgumentException(
                "CreationExtras must have a value by `SAVED_STATE_REGISTRY_OWNER_KEY`"
            )
    val viewModelStateRegistryOwner =
        this[VIEW_MODEL_STORE_OWNER_KEY]
            ?: throw IllegalArgumentException(
                "CreationExtras must have a value by `VIEW_MODEL_STORE_OWNER_KEY`"
            )

    val defaultArgs = this[DEFAULT_ARGS_KEY]
    val key =
        this[VIEW_MODEL_KEY]
            ?: throw IllegalArgumentException(
                "CreationExtras must have a value by `VIEW_MODEL_KEY`"
            )
    return createSavedStateHandle(
        savedStateRegistryOwner,
        viewModelStateRegistryOwner,
        key,
        defaultArgs
    )
}

Here three key objects are extracted from CreationExtras:

  1. savedStateRegistryOwner — reference to SavedStateRegistry for state management.
  2. viewModelStateRegistryOwner — reference to ViewModelStore for lifecycle binding.
  3. defaultArgs — initial parameters, if they were passed.

All these dependencies are passed to another createSavedStateHandle method, which is responsible for creating or restoring SavedStateHandle for the given ViewModel.

androidx.lifecycle.SavedStateHandleSupport.kt:

private fun createSavedStateHandle(
    savedStateRegistryOwner: SavedStateRegistryOwner,
    viewModelStoreOwner: ViewModelStoreOwner,
    key: String,
    defaultArgs: SavedState?
): SavedStateHandle {
    val provider = savedStateRegistryOwner.savedStateHandlesProvider
    val viewModel = viewModelStoreOwner.savedStateHandlesVM
    return viewModel.handles[key]
        ?: SavedStateHandle.createHandle(provider.consumeRestoredStateForKey(key), defaultArgs)
            .also { viewModel.handles[key] = it }
}

Here it first looks for the needed SavedStateHandle inside SavedStateHandlesVM. If it’s not found — a new one is created, saved in SavedStateHandlesVM, and the createSavedStateHandle function returns control back to CreationExtras.createSavedStateHandle(), which we already saw. Ultimately, control returns to the factory, thus creating a SavedStateHandle for the specific ViewModel.

Also in this method we see calls like savedStateRegistryOwner.savedStateHandlesProvider and viewModelStoreOwner.savedStateHandlesVM.

Now let’s see how this relates to the provider. The code calls savedStateRegistryOwner.savedStateHandlesProvider. This is actually just an extension property that extracts an object (SavedStateProvider) from SavedStateRegistry.

This provider is responsible for access to all saved states (SavedStateHandle) bound to different ViewModel. Let’s move to the provider: savedStateHandlesProvider

androidx.lifecycle.SavedStateHandleSupport.kt:

internal val SavedStateRegistryOwner.savedStateHandlesProvider: SavedStateHandlesProvider
get() =
    savedStateRegistry.getSavedStateProvider(SAVED_STATE_KEY) as? SavedStateHandlesProvider
        ?: throw IllegalStateException(
            "enableSavedStateHandles() wasn't called " +
                    "prior to createSavedStateHandle() call"
        )

internal class SavedStateHandlesProvider(
    private val savedStateRegistry: SavedStateRegistry,
    viewModelStoreOwner: ViewModelStoreOwner
) : SavedStateRegistry.SavedStateProvider {
    private var restored = false
    private var restoredState: SavedState? = null

    private val viewModel by lazy { viewModelStoreOwner.savedStateHandlesVM }

    override fun saveState(): SavedState {
        return savedState {
            restoredState?.let { putAll(it) }
            viewModel.handles.forEach { (key, handle) ->
                val savedState = handle.savedStateProvider().saveState()
                if (savedState.read { !isEmpty() }) {
                    putSavedState(key, savedState)
                }
            }
            restored = false
        }
    }

    fun performRestore() {
        ...
    }

    fun consumeRestoredStateForKey(key: String): SavedState? {
        ...
    }
}

SavedStateHandlesProvider is an intermediary between SavedStateRegistry and SavedStateHandle, providing centralized saving and restoration of ViewModel states. In the saveState() method, all current states are collected from viewModel.handles, previously restored state is added if available, and the result is saved in SavedStateRegistry.

For selective restoration, the consumeRestoredStateForKey() method is used, allowing to get state by key without needing to load everything at once. Restoration and state preparation happen in performRestore().

Essentially, SavedStateHandlesProvider manages the lifecycle of all SavedStateHandle within the scope of a state owner, supporting lazy restoration logic and guaranteeing correct saving after process or configuration changes.

Interaction with SavedStateHandlesVM:

Now let’s move to how data is stored inside ViewModel. savedStateHandlesVM is an extension that creates or restores a SavedStateHandlesVM object, storing a Map from keys to SavedStateHandle:

internal val ViewModelStoreOwner.savedStateHandlesVM: SavedStateHandlesVM
get() =
    ViewModelProvider.create(
        owner = this,
        factory =
            object : ViewModelProvider.Factory {
                override fun <T : ViewModel> create(
                    modelClass: KClass<T>,
                    extras: CreationExtras
                ): T {
                    @Suppress("UNCHECKED_CAST") return SavedStateHandlesVM() as T
                }
            }
    )[VIEWMODEL_KEY, SavedStateHandlesVM::class]

internal class SavedStateHandlesVM : ViewModel() {
    val handles = mutableMapOf<String, SavedStateHandle>()
}

Here a SavedStateHandlesVM object is created, inside which a Map is maintained, linking keys with SavedStateHandle objects. SavedStateHandlesVM is needed to store and manage all SavedStateHandle of all ViewModel within one ViewModelStoreOwner and SavedStateRegistryOwner.

SavedStateHandlesProvider is a class implementing the SavedStateProvider interface. When SavedStateController calls performSave, it also addresses SavedStateHandlesProvider and calls its saveState method. Then it puts all existing SavedStateHandle in a SavedState object (Bundle) and returns it.

But for this whole process to work, it’s necessary to register SavedStateHandlesProvider in SavedStateRegistry, however so far in the code we haven’t encountered a block responsible for registering the provider, that is, calling the method: savedStateRegistry.registerSavedStateProvider(...)

Actually such logic exists, and it’s triggered inside ComponentActivity, Fragment and NavBackStackEntry, that is, in all SavedStateRegistryOwner. Let’s just look at how this is called in ComponentActivity:

open class ComponentActivity() : ..., SavedStateRegistryOwner, ... {

    init {
        ...
        enableSavedStateHandles()
        ...
    }
}

We see a call to some enableSavedStateHandles method — the name itself sounds enticing. Next — the source code of the enableSavedStateHandles method:

@MainThread
public fun <T> T.enableSavedStateHandles() where T : SavedStateRegistryOwner, T : ViewModelStoreOwner {
    ...
    if (savedStateRegistry.getSavedStateProvider(SAVED_STATE_KEY) == null) {
        val provider = SavedStateHandlesProvider(savedStateRegistry, this)
        savedStateRegistry.registerSavedStateProvider(SAVED_STATE_KEY, provider)
        lifecycle.addObserver(SavedStateHandleAttacher(provider))
    }
}

enableSavedStateHandles is a typed method that requires the calling scope to be both a SavedStateRegistryOwner and a ViewModelStoreOwner simultaneously. ComponentActivity / Fragment / NavBackStackEntry fit this perfectly — all three implement both interfaces.

Let’s briefly understand what happens in this method. First, a saved provider (SavedStateProvider) is requested from SavedStateRegistry by the key SAVED_STATE_KEY. This is the key for storing SavedStateHandlesProvider (which is also SavedStateProvider).

If nothing is found by the key, that is, null, this means the provider hasn’t been registered yet. Then a SavedStateHandlesProvider object (which is also SavedStateProvider) is created and registered in savedStateRegistry.

We’ve thoroughly examined how the SavedStateHandle mechanism is automatically created and connected to ViewModel. This is achieved through the built-in SavedStateViewModelFactory factory mechanism, which when creating ViewModel extracts necessary dependencies from the CreationExtras object. These dependencies include:

  1. SavedStateRegistryOwner — for managing state saving and restoration.
  2. ViewModelStoreOwner — for ViewModel lifecycle binding.
  3. DefaultArgs — initial parameters, if they were passed.

At the moment of ViewModel initialization, the SavedStateViewModelFactory factory through the createSavedStateHandle method forms a SavedStateHandle object. This object connects with SavedStateRegistry and registers in it through a special provider — SavedStateHandlesProvider(SavedStateProvider).

The provider registration mechanism is launched automatically when creating ComponentActivity, Fragment or NavBackStackEntry. This is ensured by calling the enableSavedStateHandles method, which registers the provider in SavedStateRegistry under the key SAVED_STATE_KEY. Later, when onSaveInstanceState is called, this provider saves all current states from SavedStateHandle, bound to ViewModel keys.

Thus, when a component is recreated (for example, when screen orientation changes or in case of Activity destruction and restoration), the restoration mechanism triggers automatically. SavedStateRegistry restores state from the provider, and SavedStateHandle reconnects with ViewModel, ensuring transparent work with saved data.

This allows us not to worry about manually passing saved state at each ViewModel recreation. The Android framework does this for us, using a powerful mechanism of factories and state stores, which makes SavedStateHandle a convenient and reliable tool for managing state inside ViewModel.

At this point we understand how SavedStateHandle works in conjunction with ViewModel and how it ultimately connects to SavedStateRegistry. Also before this we learned how SavedStateRegistry and SavedStateRegistryController work, and saw their connection to onSaveInstanceState and onRestoreInstanceState methods.

It turned out that both Saved State API and the ancient onSaveInstanceState / onRestoreInstanceState methods ultimately work through one and the same path. Let’s return to the point where they meet. Next is code we’ve already seen:

open class ComponentActivity() : ..., SavedStateRegistryOwner, ... {

    override fun onCreate(savedInstanceState: Bundle?) {
        savedStateRegistryController.performRestore(savedInstanceState)
        super.onCreate(savedInstanceState)
        ...
    }

    @CallSuper
    override fun onSaveInstanceState(outState: Bundle) {
        ...
        super.onSaveInstanceState(outState)
        savedStateRegistryController.performSave(outState)
    }
}

That is, in standard practice when using the state saving mechanism, two methods are applied:

  • onCreate — receives a savedInstanceState parameter of type Bundle as input. It’s in this method that saved values are read.
  • onSaveInstanceState — receives an outState parameter of type Bundle as input. Values that should be saved are written to this parameter.

Let’s figure out how this whole construction works: how values saved in outState of the onSaveInstanceState method survive configuration changes and even process death, and how this saved data returns back to onCreate.

Let’s look at the implementation of the onSaveInstanceState method in super, that is, in the Activity class itself:

public class Activity extends ContextThemeWrapper ...{

final void performSaveInstanceState(@NonNull Bundle outState) {
       ...
    onSaveInstanceState(outState);
       ...
}

protected void onSaveInstanceState(@NonNull Bundle outState) {
       ...
}
}

Everything that happens inside this method doesn’t concern us right now. The main thing is that onSaveInstanceState calls another final method — performSaveInstanceState.

Now let’s understand who calls performSaveInstanceState. This call is initiated by the Instrumentation class:

android.app.Instrumentation.java:


@android.ravenwood.annotation.RavenwoodKeepPartialClass
public class Instrumentation {
   ...

    public void callActivityOnSaveInstanceState(@NonNull Activity activity,
                                                @NonNull Bundle outState) {
        activity.performSaveInstanceState(outState);
    }
   ...
}
Official documentation states the following about this class:

Base class for implementing application instrumentation code. When running with instrumentation turned on, this class will be instantiated for you before any of the application code, allowing you to monitor all of the interaction the system has with the application. An Instrumentation implementation is described to the system through an AndroidManifest.xml’stag.

Now we need to understand who calls Instrumentation.callActivityOnSaveInstanceState? And here we encounter ActivityThread:

public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal {
    ...

    private void callActivityOnSaveInstanceState(ActivityClientRecord r) {
        r.state = new Bundle();
        r.state.setAllowFds(false);
        if (r.isPersistable()) {
            r.persistentState = new PersistableBundle();
            mInstrumentation.callActivityOnSaveInstanceState(
                    r.activity, r.state,
                    r.persistentState
            );
        } else {
            mInstrumentation.callActivityOnSaveInstanceState(r.activity, r.state);
        }
    }
   ...
}

What’s happening here? callActivityOnSaveInstanceState takes a parameter r of type ActivityClientRecord as input. This class has a field state, which is a Bundle. It’s assigned a new Bundle object.

We’ve already encountered the ActivityClientRecord class when we were looking at ViewModelStore. ActivityClientRecord represents an activity record and is used to store all information related to the actual activity instance. This is a kind of data structure for tracking activity during application execution.

Main fields of the ActivityClientRecord class:

  • stateBundle object containing saved activity state. Yes, yes, this is the same Bundle we get in onCreate, onRestoreInstanceState and onSaveInstanceState methods
  • lastNonConfigurationInstancesActivity#NonConfigurationInstance object, which stores ComponentActivity#NonConfigurationInstances which stores ViewModelStore.
  • intentIntent object representing the activity launch intent.
  • windowWindow object associated with the activity.
  • activity — the Activity object itself.
  • parent — parent activity (if any).
  • createdConfigConfiguration object containing settings applied when creating the activity.
  • overrideConfigConfiguration object containing current activity settings.

Let’s not get distracted for now, and find out who calls callActivityOnSaveInstanceState:

public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal {

    private void callActivityOnStop(ActivityClientRecord r, boolean saveState, String reason) {
        final boolean shouldSaveState = saveState && !r.activity.mFinished && r.state == null
                && !r.isPreHoneycomb();
        final boolean isPreP = r.isPreP();
        if (shouldSaveState && isPreP) {
            callActivityOnSaveInstanceState(r);
        }
        ...
    }

    private Bundle performPauseActivity(ActivityClientRecord r, boolean finished, String reason,
                                        PendingTransactionActions pendingActions) {
       ...
        final boolean shouldSaveState = !r.activity.mFinished && r.isPreHoneycomb();
        if (shouldSaveState) {
            callActivityOnSaveInstanceState(r);
        }
       ...
    }
}

The callActivityOnStop method determines whether activity state should be saved before stopping. It checks the saveState flag, the activity shouldn’t be finished (!mFinished), the state (r.state) should not be saved yet, and the version should be before Honeycomb (!isPreHoneycomb()). If all conditions are met and the version is before Android P (isPreP()), callActivityOnSaveInstanceState is called to create and fill the Bundle

The performPauseActivity method checks whether state should be saved before pausing. Here the conditions are simplified: the activity shouldn’t be finished, version — before Honeycomb. If yes, then callActivityOnSaveInstanceState is called again to form the Bundle.

public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal {

    private void performStopActivityInner(ActivityClientRecord r, StopInfo info,
                                          boolean saveState, boolean finalStateRequest, String reason) {
      ...
        callActivityOnStop(r, saveState, reason);
    }

    private void handleRelaunchActivityInner(@NonNull ActivityClientRecord r,...) {
       ...
        if (!r.stopped) {
            callActivityOnStop(r, true /* saveState */, reason);
        }
       ...
    }
}

performStopActivityInner is used for complete activity stopping. Inside, callActivityOnStop is immediately called, which checks and, if needed, initiates state saving. This guarantees that activity state gets into the Bundle before the activity is stopped and destroyed.

In handleRelaunchActivityInner, callActivityOnStop is called if the activity is not stopped yet (!r.stopped). This is important when recreating activity (for example, when configuration changes) to save state before recreation.

public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal {
    @Override
    public void handleRelaunchActivity(@NonNull ActivityClientRecord tmp,
                                       @NonNull PendingTransactionActions pendingActions) {
      ...
        handleRelaunchActivityInner(r, tmp.pendingResults, tmp.pendingIntents,
                pendingActions, tmp.startsNotResumed, tmp.overrideConfig, tmp.mActivityWindowInfo,
                "handleRelaunchActivity");
    }


    @Override
    public void handleStopActivity(ActivityClientRecord r,
                                   PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
      ...
        performStopActivityInner(r, stopInfo, true /* saveState */, finalStateRequest,
                reason);
      ...
    }
}

handleRelaunchActivity is an external method that calls handleRelaunchActivityInner. Used for handling complete activity recreation. All checks and state saving logic are already inside handleRelaunchActivityInner.

handleStopActivity calls performStopActivityInner, passing the saveState = true flag there to forcibly save state before final stopping. This is used, for example, when closing the application or when the activity is unloaded by the system.

Subsequent calls to performStopActivity and handleRelaunchActivity methods lead to ActivityRelaunchItem.execute() and StopActivityItem.execute() classes. The performStopActivity method is called from StopActivityItem.execute(), and handleRelaunchActivity — from ActivityRelaunchItem.execute().

public class StopActivityItem extends ActivityLifecycleItem {
    @Override
    public void execute(@NonNull ClientTransactionHandler client, @NonNull ActivityClientRecord r,
                        @NonNull PendingTransactionActions pendingActions) {
        client.handleStopActivity(r, pendingActions,
                true /* finalStateRequest */, "STOP_ACTIVITY_ITEM");
        Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
    }
}

In the StopActivityItem.execute method we see a call to client.handleStopActivity. Since client is ClientTransactionHandler, and ActivityThread inherits from it, actually ActivityThread.handleStopActivity is called here.

public class ActivityRelaunchItem extends ActivityTransactionItem {
    @Override
    public void execute(@NonNull ClientTransactionHandler client, @NonNull ActivityClientRecord r,
                        @NonNull PendingTransactionActions pendingActions) {
        client.handleRelaunchActivity(mActivityClientRecord, pendingActions);
    }
}

In the ActivityRelaunchItem.execute method we see a call to client.handleRelaunchActivity. By the same logic, actually ActivityThread.handleRelaunchActivity is called.

At this point we’ve traced the following call chain:

StopActivityItem.executeActivityThread.handleStopActivityActivityThread.performStopActivityInnerActivityThread.callActivityOnStopActivityThread.callActivityOnSaveInstanceStateInstrumentation.callActivityOnSaveInstanceStateActivity.performSaveInstanceStateActivity.onSaveInstanceState.

This is the key chain that ensures saving of Activity state during configuration changes or its termination. Note that the call to callActivityOnSaveInstanceState from Instrumentation is exactly the point where the system passes control back to Activity, calling the performSaveInstanceState method, which initiates saving all data to the Bundle object.

In parallel, in case of configuration change or activity recreation, another chain is launched:

ActivityRelaunchItem.executeActivityThread.handleRelaunchActivityActivityThread.handleRelaunchActivityInnerActivityThread.callActivityOnStopActivityThread.callActivityOnSaveInstanceStateInstrumentation.callActivityOnSaveInstanceStateActivity.performSaveInstanceStateActivity.onSaveInstanceState.

These two chains work independently, but converge in the callActivityOnStop method, which guarantees data saving to Bundle before Activity is stopped or recreated.

Further, the formed Bundle object containing the Activity state is saved in the ActivityClientRecord object. This object represents a data structure storing all necessary information about Activity during its lifecycle. It’s in the state field of this class that the system saves the passed Bundle to restore its state when the activity is recreated. ActivityClientRecord exists in the process of all chain calls, before Activity transitions to STOP state. Inside the ActivityThread.callActivityOnSaveInstanceState method, the ActivityClientRecord.state field is assigned a new Bundle, into which activities and fragments put everything needed — from View hierarchy state to any data the developer decided to save.

Thus, we see that this chain is launched not from the Activity itself, but from Android’s internal logic through ActivityThread. This once again confirms that all lifecycles are managed by the system through a unified client-server transaction mechanism, and ActivityThread performs the role of a mediator coordinating calls between Activity and the system.

An important point here is where ActivityClientRecord comes from and how its internal Bundle survives process death. In the case of saving between PAUSE/STOP we saw where a clean Bundle is created, into which data can be saved. There are no special secrets here. But how this saved Bundle inside ActivityClientRecord survives system death and then returns to Activity.onCreate, we don’t know yet. The next chapter will reveal this moment.

onCreate Call Chain

Let’s start our movement from the very bottom — from the onCreate method. As can be seen from the code, its call happens inside the performCreate method, which, in turn, is called from the callActivityOnCreate method of the Instrumentation class.

public class Activity extends ContextThemeWrapper ...{

public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
    onCreate(savedInstanceState);
}

@MainThread
@CallSuper
protected void onCreate(@Nullable Bundle savedInstanceState) {
            ...
}

final void performCreate(Bundle icicle) {
    performCreate(icicle, null);
}

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
final void performCreate(Bundle icicle, PersistableBundle persistentState) {
            ...
    if (persistentState != null) {
        onCreate(icicle, persistentState);
    } else {
        onCreate(icicle);
    }
            ...
}
}

The performCreate method is a link between the onCreate call logic and lower-level system components. The performCreate call itself is made in the Instrumentation class:

public class Instrumentation {
    ...

    public void callActivityOnCreate(Activity activity, Bundle icicle) {
        ...
        activity.performCreate(icicle);
        ...
    }
}

The Instrumentation class manages the Activity lifecycle and calls performCreate, passing it a Bundle object for state restoration. Now let’s go higher. Who calls callActivityOnCreate? This is handled by the performLaunchActivity method in the ActivityThread class:

public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal {

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ...
        if (r.isPersistable()) {
            mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
        } else {
            mInstrumentation.callActivityOnCreate(activity, r.state);
        }
        ...
    }
}

Here we see that depending on the activity state (whether it’s saved in PersistentState), callActivityOnCreate is called with different numbers of parameters, but always through Instrumentation.

Further, this performLaunchActivity method is called from the handleLaunchActivity method of the same class:

public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal {

    @Override
    public Activity handleLaunchActivity(ActivityClientRecord r, ...) {
    ...
        final Activity a = performLaunchActivity(r, customIntent);
    ...
    }
}

Activity Restart on Relaunch (e.g., on Screen Rotation)

When Activity is recreated, for example, on screen rotation, the handleRelaunchActivity method is triggered:

public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal {

    @Override
    public void handleRelaunchActivity(@NonNull ActivityClientRecord tmp,
                                       @NonNull PendingTransactionActions pendingActions) {
        ...
        handleRelaunchActivityInner(r, tmp.pendingResults, tmp.pendingIntents,
                pendingActions, tmp.startsNotResumed, tmp.overrideConfig, tmp.mActivityWindowInfo,
                "handleRelaunchActivity");
    }

    private void handleRelaunchActivityInner(@NonNull ActivityClientRecord r,...) {
    ....
        handleLaunchActivity(r, pendingActions, mLastReportedDeviceId, customIntent);
    }
}

The call to the handleRelaunchActivity method is initiated by the command/transaction class ActivityRelaunchItem, which acts as a marker to perform restart with state preservation:

public class ActivityRelaunchItem extends ActivityTransactionItem {

    @Override
    public void execute(@NonNull ClientTransactionHandler client, @NonNull ActivityClientRecord r,
                        @NonNull PendingTransactionActions pendingActions) {
        ...
        client.handleRelaunchActivity(mActivityClientRecord, pendingActions);
        ...
    }
}

This command initiates the following chain of calls:

ActivityRelaunchItem.executehandleRelaunchActivityhandleRelaunchActivityInnerhandleLaunchActivityperformLaunchActivitycallActivityOnCreateperformCreateonCreate.

Creating Activity after process destruction or on first launch

In case the process was destroyed or this is the first launch of Activity, a different command is used — LaunchActivityItem. It launches a similar, but separate chain of calls:

public class LaunchActivityItem extends ClientTransactionItem {

    @Nullable
    private final Bundle mState;

    @Nullable
    private final PersistableBundle mPersistentState;

    public LaunchActivityItem(
            // other parameters
            @Nullable Bundle state,
            @Nullable PersistableBundle persistentState,
            // other parameters
    ) {
        this(
                // passed arguments before
                state != null ? new Bundle(state) : null,
                persistentState != null ? new PersistableBundle(persistentState) : null,
                // remaining arguments
        );
    ...
    }


    @Override
    public void execute(@NonNull ClientTransactionHandler client,
                        @NonNull PendingTransactionActions pendingActions) {
        ...
        ActivityClientRecord r = new ActivityClientRecord(...,mState, mPersistentState, ...);
        client.handleLaunchActivity(r, pendingActions, mDeviceId, null /* customIntent */);
        ...
    }
}

The chain looks like this: LaunchActivityItem.executeActivityThread.handleLaunchActivityActivityThread.performLaunchActivityActivityThread.callActivityOnCreateActivity.performCreateActivity.onCreate.

It’s important to remember before going higher up, you need to understand that LaunchActivityItem is a transaction that accepts Bundle and PersistableBundle in its constructor (we won’t consider the latter). The LaunchActivityItem class inherits from ClientTransactionItem.

ClientTransactionItem is an abstract base class from which all transactions related to Activity lifecycle inherit. It includes LaunchActivityItem, ActivityRelaunchItem, ResumeActivityItem (the latter are not direct, but transitive inheritors) and other elements involved in managing Activity state.

We saw that the creation of ActivityClientRecord occurs in LaunchActivityItem.execute, but it uses ready data that was passed to it in the constructor during creation.

Our goal next is to figure out two points:

  1. Who creates LaunchActivityItem and passes the Bundle to it, which is exactly what survives process death or stopping.
  2. Who calls the execute method on LaunchActivityItem and launches the chain of calls described above: LaunchActivityItem.executehandleLaunchActivityperformLaunchActivitycallActivityOnCreateperformCreateonCreate.

So let’s continue, above the LaunchActivityItem.execute call, there’s the TransactionExecutor class

public class TransactionExecutor {

    private final ClientTransactionHandler mTransactionHandler;

    public TransactionExecutor(@NonNull ClientTransactionHandler clientTransactionHandler) {
        mTransactionHandler = clientTransactionHandler;
    }

    public void execute(@NonNull ClientTransaction transaction) {
        ...
        executeTransactionItems(transaction);
        ...
    }

    public void executeTransactionItems(@NonNull ClientTransaction transaction) {
        final List<ClientTransactionItem> items = transaction.getTransactionItems();
        final int size = items.size();
        for (int i = 0; i < size; i++) {
            final ClientTransactionItem item = items.get(i);
            if (item.isActivityLifecycleItem()) {
                executeLifecycleItem(transaction, (ActivityLifecycleItem) item);
            } else {
                executeNonLifecycleItem(transaction, item,
                        shouldExcludeLastLifecycleState(items, i));
            }
        }
    }

    private void executeLifecycleItem(@NonNull ClientTransaction transaction,
                                      @NonNull ActivityLifecycleItem lifecycleItem) {
        ...
        lifecycleItem.execute(mTransactionHandler, mPendingActions);
                ...
    }

    private void executeNonLifecycleItem(@NonNull ClientTransaction transaction,
                                         @NonNull ClientTransactionItem item, boolean shouldExcludeLastLifecycleState) {
        ...
        item.execute(mTransactionHandler, mPendingActions);
        ...
    }
}

TransactionExecutor is exactly the class that works with all transactions, i.e., with ClientTransactionItem, and ClientTransaction - which is an array or queue that stores ClientTransactionItems.

The TransactionExecutor constructor takes ClientTransactionHandler as input, if you remember, ActivityThread implements the abstract ClientTransactionHandler class, so actually the ActivityThread arrives in the TransactionExecutor constructor.

TransactionExecutor has an execute method that calls another method executeTransactionItems, executeTransactionItems - in turn iterates through all elements inside the transaction queue, i.e., in ClientTransaction, and ultimately determines which method to call, executeNonLifecycleItem or executeLifecycleItem.

The difference between these methods is that executeLifecycleItem is called for transactions representing activity lifecycle stages — such as ResumeActivityItem, PauseActivityItem, StopActivityItem, DestroyActivityItem. These elements are responsible for transitions between states of an already existing Activity. Their purpose is to call the corresponding callbacks (onPause, onStop, and so on) on the activity object that has already been created and exists in memory.

On the other hand, executeNonLifecycleItem is used to execute transactions that are not related to the lifecycle. The main representative is LaunchActivityItem, which is responsible for creating Activity from scratch. This can happen either during the first launch of Activity, or after the system has destroyed the process and is now restoring it. Inside executeNonLifecycleItem, item.execute(...) is called, which, in the case of LaunchActivityItem, initiates the full creation chain: from ActivityClientRecord to calling onCreate.

Inside LaunchActivityItem, in the executeNonLifecycleItem method, we see that the item (instance of ClientTransactionItem) has its execute method called, which is passed ClientTransactionHandler and PendingTransactionActions. Actually at this moment the execute method of LaunchActivityItem is called. Don’t forget that LaunchActivityItem inherits from ClientTransactionItem.

Now let’s figure out who calls the execute method on TransactionExecutor. This is done by the inner class H, which is a Handler:

public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal {

    final H mH = new H();
    private final TransactionExecutor mTransactionExecutor = new TransactionExecutor(this);

    class H extends Handler {

        public void handleMessage(Message msg) {
            switch (msg.what) {
                ...
                case EXECUTE_TRANSACTION:
                    final ClientTransaction transaction = (ClientTransaction) msg.obj;
                    final ClientTransactionListenerController controller = ClientTransactionListenerController.getInstance();
                    controller.onClientTransactionStarted();
                    try {
                        mTransactionExecutor.execute(transaction);
                    } finally {
                        controller.onClientTransactionFinished();
                    }
                    ...
                ...
            }
        }
    }
}

Let’s recall that ClientTransactionHandler is an abstract class from which ActivityThread inherits. Next we see that an H object is created, as well as TransactionExecutor, to which this is passed as an argument, i.e., ActivityThread, implementing ClientTransactionHandler.

Now let’s pay attention to the handleMessage implementation inside the H class: when a message with type EXECUTE_TRANSACTION arrives, ClientTransaction is extracted from the Message object, containing a list (List) of transactions. Then the execute method is called on TransactionExecutor, which launches the transaction execution.

The handleMessage method of class H itself calls methods from the ActivityThread class:

public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal {

    final H mH = new H();

    void sendMessage(int what, Object obj) {
        sendMessage(what, obj, 0, 0, false);
    }

    private void sendMessage(int what, Object obj, int arg1) {
        sendMessage(what, obj, arg1, 0, false);
    }

    private void sendMessage(int what, Object obj, int arg1, int arg2) {
        sendMessage(what, obj, arg1, arg2, false);
    }

    private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {
        ...
        mH.sendMessage(msg);
    }
}

We see that the last sendMessage method calls the sendMessage method on class H, since class H inherits from the Handler class, it has a sendMessage method and calls the handleMessage method. We need to understand who calls sendMessage on ActivityThread.

public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal {

    void sendMessage(int what, Object obj) {
        sendMessage(what, obj, 0, 0, false);
    }

    private class ApplicationThread extends IApplicationThread.Stub {

        @Override
        public void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
            ActivityThread.this.scheduleTransaction(transaction);
        }
    }
}

This is done by ApplicationThread. How does calling ActivityThread.scheduleTransaction method call ActivityThread.sendMessage?

The thing is that ActivityThread inherits from ClientTransactionHandler, and ClientTransactionHandler looks like this:

public abstract class ClientTransactionHandler {

    void scheduleTransaction(ClientTransaction transaction) {
        transaction.preExecute(this);
        sendMessage(ActivityThread.H.EXECUTE_TRANSACTION, transaction);
    }

    abstract void sendMessage(int what, Object obj);

}

It turns out that the scheduleTransaction method is called on ApplicationThread, it calls the scheduleTransaction method on ActivityThread that it inherited from ClientTransactionHandler, inside the scheduleTransaction method in ClientTransactionHandler we see that it calls the sendMessage method with two parameters, ActivityThread just overrides this method, and then the call goes to H.sendMessage.

ApplicationThread is a Proxy that implements the AIDL interface, this class is responsible for many schedulings, for example services, receiver or binding Application. Also note that it implements IApplicationThread.Stub, i.e., actually the AIDL interface IApplicationThread itself.

Next, let’s understand where the call to ApplicationThread.scheduleTransaction method comes from, and voila, this is handled by the class:

public class ClientTransaction implements Parcelable, ObjectPoolItem {

    private IApplicationThread mClient;

    public void schedule() throws RemoteException {
        mClient.scheduleTransaction(this);
    }
}

It calls ApplicationThread.scheduleTransaction passing itself, thereby scheduling itself and its internal transactions for execution. IApplicationThread is the ActivityThread.ApplicationThread class. Next, let’s trace the call to ClientTransaction.schedule() method. Meet another class:

class ClientLifecycleManager {

    void scheduleTransactionItems(@NonNull IApplicationThread client,
                                  boolean shouldDispatchImmediately,
                                  @NonNull ClientTransactionItem... items) throws RemoteException {
        ...
        final ClientTransaction clientTransaction = getOrCreatePendingTransaction(client);

        final int size = items.length;
        for (int i = 0; i < size; i++) {
            clientTransaction.addTransactionItem(items[i]);
        }

        onClientTransactionItemScheduled(clientTransaction, shouldDispatchImmediately);
    }

    private void onClientTransactionItemScheduled(
            @NonNull ClientTransaction clientTransaction,
            boolean shouldDispatchImmediately) throws RemoteException {
        ...
        scheduleTransaction(clientTransaction);
    }


    void scheduleTransaction(@NonNull ClientTransaction transaction) throws RemoteException {
        ...
        transaction.schedule();
        ...
    }
}

Inside it, the scheduleTransactionItems method is defined, which takes IApplicationThread and an array of ClientTransactionItem. This method creates or gets a transaction through getOrCreatePendingTransaction, adds all ClientTransactionItem to it (for example, LaunchActivityItem, ResumeActivityItem, PauseActivityItem, etc.), after which it passes it to the onClientTransactionItemScheduled method, where scheduleTransaction is called.

After that, control passes to the scheduleTransaction method, inside which transaction.schedule() is called. And as we already know, the schedule method calls ApplicationThread.scheduleTransaction, i.e., actually we return back to the AIDL call, from which everything starts.

Thus, ClientLifecycleManager collects the transaction, fills it with the necessary ClientTransactionItem, and sends it for execution. This is the class that forms the chain of actions and delegates execution to the low-level layer through AIDL.

ClientLifecycleManager.scheduleTransactionItems - the method call is handled by a very important class ActivityTaskSupervisor

public class ActivityTaskSupervisor implements RecentTasks.Callbacks {
    ...
    final ActivityTaskManagerService mService;
    ...

    boolean realStartActivityLocked(ActivityRecord r, WindowProcessController proc,
                                    boolean andResume, boolean checkConfig) throws RemoteException {


        // Create activity launch transaction.
        final LaunchActivityItem launchActivityItem = new LaunchActivityItem(r.token,
                ...,r.getSavedState(), r.getPersistentSavedState(), ...,
       );
        ...
        mService.getLifecycleManager().scheduleTransactionItems(
                proc.getThread(),
                // Immediately dispatch the transaction, so that if it fails, the server can
                // restart the process and retry now.
                true /* shouldDispatchImmediately */,
                launchActivityItem, lifecycleItem);
        ...
        return true;
    }
    ...
}

We see very key points:

  1. In the realStartActivityLocked method, an ActivityRecord class object is passed as input, which stores values - r.getSavedState()(Bundle) and r.getPersistentSavedState(PersistentBundle) and other important values and information about the activity
  2. Finally we see the creation of the LaunchActivityItem transaction with passing all necessary arguments, including Bundle
  3. We see that the getLifecycleManager() method is called on the ActivityTaskManagerService class which returns a ClientLifecycleManager object and calls the scheduleTransactionItems method on it that we already saw, passing LaunchActivityItem

Let’s make sure that the getLifecycleManager method in ActivityTaskManagerService actually returns ClientLifecycleManager:

public class ActivityTaskManagerService extends IActivityTaskManager.Stub {

    ClientLifecycleManager getLifecycleManager() {
        return mLifecycleManager;
    }
}

Confirmed, excellent, let’s continue and trace the call to the realStartActivityLocked method of the ActivityTaskSupervisor class

class RootWindowContainer extends WindowContainer<DisplayContent> implements DisplayManager.DisplayListener {

    ActivityTaskSupervisor mTaskSupervisor;
    ActivityTaskManagerService mService;

    boolean attachApplication(WindowProcessController app) throws RemoteException {
        final ArrayList<ActivityRecord> activities = mService.mStartingProcessActivities;
        for (int i = activities.size() - 1; i >= 0; i--) {
            final ActivityRecord r = activities.get(i);
            ...
            if (mTaskSupervisor.realStartActivityLocked(r, app, canResume,
                    true /* checkConfig */)) {
                hasActivityStarted = true;
            }
            ...
            return hasActivityStarted;
        }
    }
}
RootWindowContainer...

RootWindowContainer is a central component in Android’s window management system, which contains the entire hierarchy of windows on all displays. It manages DisplayContent instances, coordinates layout, input, focus, animations, transitions, split-screen, picture-in-picture and any changes related to screen configuration. Everything that should appear, disappear, be recalculated or animated - first goes through it. This is the entry point for all window transactions, including starting and finishing activities.

It’s so cool that it can stop activity restart if it feels that the layout is still “in progress”. It doesn’t need confirmation from WindowManagerService to show Window and work with content.

RootWindowContainer was previously called RootActivityContainer

We see that the call to ActivityTaskSupervisor.realStartActivityLocked method occurs in the RootWindowContainer class, which in the attachApplication method, gets a list of ActivityRecord from ActivityTaskManagerService, and in a loop calls the ActivityTaskSupervisor.realStartActivityLocked method for all of them.

Next, we return again to ActivityTaskManagerService, because it is the one that calls the attachApplication method on RootWindowContainer and passes it

public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
   ...

    /** The starting activities which are waiting for their processes to attach. */
    final ArrayList<ActivityRecord> mStartingProcessActivities = new ArrayList<>();
    RootWindowContainer mRootWindowContainer;

    @HotPath(caller = HotPath.PROCESS_CHANGE)
    @Override
    public boolean attachApplication(WindowProcessController wpc) throws RemoteException {
        ...
        return mRootWindowContainer.attachApplication(wpc);
    }

    void startProcessAsync(ActivityRecord activity, boolean knownToBeDead, boolean isTop,
                           String hostingType) {
         ...
        mStartingProcessActivities.add(activity);
         ...
    }


    ClientLifecycleManager getLifecycleManager() {
        return mLifecycleManager;
    }
   ...
}

We see that it stores a list of ActivityRecord in the mStartingProcessActivities field — a call we already saw in RootWindowContainer.attachApplication.

Next, we see that it also has a reference to RootWindowContainer, and in the ActivityTaskManagerService.attachApplication method, the RootWindowContainer.attachApplication method is called.

The startProcessAsync method is also very important; it adds new ActivityRecord instances to the mStartingProcessActivities list, each of which internally stores a Bundle. We will analyze this part later as well.

Above ActivityTaskManagerService, there is the ActivityManagerService class, which calls attachApplication on ActivityTaskManagerService:

public class ActivityManagerService extends IActivityManager.Stub {

    public ActivityTaskManagerInternal mAtmInternal;
    final PidMap mPidsSelfLocked = new PidMap();

    @GuardedBy("this")
    private void attachApplicationLocked(@NonNull IApplicationThread thread,
                                         int pid, int callingUid, long startSeq) {
        ...
        finishAttachApplicationInner(startSeq, callingUid, pid);
        ...
    }

    private void finishAttachApplicationInner(long startSeq, int uid, int pid) {
        ...
        final ProcessRecord app;
        app = mPidsSelfLocked.get(pid);
        ...

        didSomething = mAtmInternal.attachApplication(app.getWindowProcessController());
        ...
    }
}

In the finishAttachApplicationInner method, we see a call to the attachApplication method on mAtmInternal. ActivityTaskManagerInternal is an abstract AIDL interface for ActivityTaskManagerService, so in fact, this is a call to ActivityTaskManagerService.attachApplication().

The finishAttachApplicationInner method itself is called from attachApplicationLocked, which also retrieves the process from mPidsSelfLocked using the pid as a key (i.e., the process ID).

The ActivityManagerService class itself is a singleton within the entire Android system. It contains a PidMap structure that stores ProcessRecord instances keyed by pid. So, when we call mPidsSelfLocked.get(pid), it queries PidMap:

public class ActivityManagerService extends IActivityManager.Stub {

    final PidMap mPidsSelfLocked = new PidMap();

    ...

    static final class PidMap {
        private final SparseArray<ProcessRecord> mPidMap = new SparseArray<>();

        ProcessRecord get(int pid) {
            return mPidMap.get(pid);
        }
        ...

        void doAddInternal(int pid, ProcessRecord app) {
            mPidMap.put(pid, app);
        }
       ...
    }

    public void setSystemProcess() {
      ...
        ProcessRecord app = mProcessList.newProcessRecordLocked(info, info.processName,
                false,
                0,
                false,
                0,
                null,
                new HostingRecord(HostingRecord.HOSTING_TYPE_SYSTEM));
            ...
        addPidLocked(app);
            ...
    }

    void addPidLocked(ProcessRecord app) {
        final int pid = app.getPid();
        synchronized (mPidsSelfLocked) {
            mPidsSelfLocked.doAddInternal(pid, app);
        }
      ...
    }
}

We see the PidMap structure, which internally stores a list of records for application processes.

We also see two methods: setSystemProcess creates a new ProcessRecord and calls addPidLocked, which puts the ProcessRecord into mPidsSelfLocked. The setSystemProcess method is called from SystemServer (also known as system_service). Below is a brief call stack:

1. Bootloader → Kernel (Linux Kernel)
2. init process (first userspace process)
   ├─ Launch zygote (via app_process)
   │   ├─ ZygoteInit (singleton, prepares environment for Java processes)
   │   │   ├─ fork() → Creates SystemServer
   │   │   └─ fork() → Creates applications
   └─ SystemServer (singleton, starts all system services)
       ├─ RuntimeInit (initializes environment for SystemServer)
       └─ ActivityManagerService (singleton, including setSystemProcess())

We don’t need to go above ActivityManagerService, as there is no Bundle stored there. Most of these components are singletons for the entire system and have no direct relation to a specific application.

At this point, a lot should already be clear. We have examined a very long call flow. One moment we slightly skipped is the exact point where ActivityRecord instances are created. Earlier, we already saw that ActivityRecord instances are retrieved from the mStartingProcessActivities field in ActivityTaskManagerService:

class RootWindowContainer extends WindowContainer<DisplayContent> implements DisplayManager.DisplayListener {

    ActivityTaskSupervisor mTaskSupervisor;
    ActivityTaskManagerService mService;

    boolean attachApplication(WindowProcessController app) throws RemoteException {
        final ArrayList<ActivityRecord> activities = mService.mStartingProcessActivities;
        for (int i = activities.size() - 1; i >= 0; i--) {
            final ActivityRecord r = activities.get(i);
            ...
            if (mTaskSupervisor.realStartActivityLocked(r, app, canResume,
                    true /* checkConfig */)) {
                hasActivityStarted = true;
            }
            ...
        }
    }
}

In ActivityTaskManagerService, this looks as we already saw. The mStartingProcessActivities field is a collection that stores ActivityRecord instances, and there is one method that adds an ActivityRecord to it — startProcessAsync:

public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
    ...

    /** The starting activities which are waiting for their processes to attach. */
    final ArrayList<ActivityRecord> mStartingProcessActivities = new ArrayList<>();
    RootWindowContainer mRootWindowContainer;

    void startProcessAsync(ActivityRecord activity, boolean knownToBeDead, boolean isTop,
                           String hostingType) {
        ...
        mStartingProcessActivities.add(activity);
        ...
    }
    ...
}

The next chapter of the article will focus on this point: how ActivityRecord instances are created and who actually places them into mStartingProcessActivities in ActivityTaskManagerService.

Process recreation with Bundle preservation

ActivityManagerService.startActivity()
  → ActivityTaskManagerService.startActivityAsUser()
    → ActivityStartController.obtainStarter()
      → ActivityStarter.execute()
        → executeRequest():
          1. Creating ActivityRecord (new object)
          2. startActivityUnchecked()
             → startActivityInner()
               → setInitialState(r) // save ActivityRecord in mStartActivity
               → RootWindowContainer.resumeFocusedTasksTopActivities(mStartActivity)
                 → Task.resumeTopActivityUncheckedLocked()
                   → ActivityTaskSupervisor.startSpecificActivity(r)
                     → (if process is not running)
                        → ActivityTaskManagerService.startProcessAsync(r)
                          → mStartingProcessActivities.add(r) // final point

ActivityRecord (with Bundle) can survive process death or interruption. This implies a situation where an application goes to background and is saved in the task stack (Recents), the system kills the process after some time. When the user returns, the system calls the startActivityFromRecents method to restore the task (Task) and start the process. Each task usually corresponds to one root Activity, but inside it can store child Activities that are also linked to components.

public class ActivityManagerService extends IActivityManager.Stub {

    @Override
    public final int startActivityFromRecents(int taskId, Bundle bOptions) {
        return mActivityTaskManager.startActivityFromRecents(taskId, bOptions);
    }

}

The startActivityFromRecents method inside ActivityManagerService directly delegates the call to ActivityTaskManagerService. It doesn’t do anything by itself, just passes control further.

public class ActivityTaskManagerService extends IActivityTaskManager.Stub {

    ActivityTaskSupervisor mTaskSupervisor;

    @Override
    public final int startActivityFromRecents(int taskId, Bundle bOptions) {
        ...
        return mTaskSupervisor.startActivityFromRecents(callingPid, callingUid, taskId, safeOptions);
    }
}

In ActivityTaskManagerService.startActivityFromRecents preparation happens: PID, UID are extracted, safe launch options (SafeActivityOptions) are formed. Then the method immediately passes execution to ActivityTaskSupervisor, where the main logic for task processing occurs.

public class ActivityTaskSupervisor implements RecentTasks.Callbacks {

    final ActivityTaskManagerService mService;
    RootWindowContainer mRootWindowContainer;

    int startActivityFromRecents(int callingPid, int callingUid, int taskId,
                                 SafeActivityOptions options) {
        final Task task;

        task = mRootWindowContainer.anyTaskForId(taskId, MATCH_ATTACHED_TASK_OR_RECENT_TASKS_AND_RESTORE, activityOptions, ON_TOP);

        if (!mService.mAmInternal.shouldConfirmCredentials(task.mUserId) && task.getRootActivity() != null) {
            final ActivityRecord targetActivity = task.getTopNonFinishingActivity();
         ...
            mService.moveTaskToFrontLocked(...);
         ...
            return ActivityManager.START_TASK_TO_FRONT;
        }
    }

}

Inside `startActivityFromRecents` in `ActivityTaskSupervisor`, the real parsing begins: first, the needed task is searched through
`mRootWindowContainer.anyTaskForId(...)`, where various flags are passed (for example, `MATCH_ATTACHED_TASK_OR_RECENT_TASKS_AND_RESTORE`),
to restore the task from the recent tasks list.
Then it's checked whether user credentials need to be confirmed (for example, if profile protection mode is enabled). After that, it checks
whether the task has a root Activity (`getRootActivity()`), and extracts the top non-finishing Activity through `getTopNonFinishingActivity()`.

If all conditions are met, `moveTaskToFrontLocked(...)` is called in `ActivityTaskManagerService`, which is responsible for moving the task to
the foreground and further launching. All this is needed to correctly restore the application state from the task stack without
the need to fully recreate the Activity from scratch.

```java
public class ActivityTaskManagerService extends IActivityTaskManager.Stub {

    void moveTaskToFrontLocked(@Nullable IApplicationThread appThread,
                               @Nullable String callingPackage, int taskId, ...) {

        final Task task = mRootWindowContainer.anyTaskForId(taskId);
        ...
        mTaskSupervisor.findTaskToMoveToFront(task, flags, ...);
    }

}

The moveTaskToFrontLocked method, after verification, passes control to findTaskToMoveToFront. Here the task is not just found, but actually moved to the foreground. At the beginning, the root container of the task is extracted through getRootTask(). If the task hasn’t been “reparented” yet, moveHomeRootTaskToFrontIfNeeded is called to bring the home task to the front if necessary (for example, if the application hasn’t been launched for a long time).

Then through getTopNonFinishingActivity() the top non-finishing ActivityRecord (Activity) in the task is extracted. Then currentRootTask.moveTaskToFront is called, where the task itself, animation options and other parameters are passed.

public class ActivityTaskSupervisor implements RecentTasks.Callbacks {

    void findTaskToMoveToFront(Task task, int flags, ActivityOptions options, String reason,
                               boolean forceNonResizeable) {
        Task currentRootTask = task.getRootTask();

        if (!reparented) {
            moveHomeRootTaskToFrontIfNeeded(flags, currentRootTask.getDisplayArea(), reason);
        }

        final ActivityRecord r = task.getTopNonFinishingActivity();
        currentRootTask.moveTaskToFront(task, false /* noAnimation */, options,
                r == null ? null : r.appTimeTracker, reason);
        ...
    }

}

In the moveTaskToFront method inside the Task class, we see the final step — calling mRootWindowContainer.resumeFocusedTasksTopActivities(). This call is responsible for launching or resuming the top activity at the window container (WindowContainer) level, making it active and rendering it.

class Task extends TaskFragment {

    final void moveTaskToFront(Task tr, boolean noAnimation, ActivityOptions options,
                               AppTimeTracker timeTracker, boolean deferResume, String reason) {
        ...
        mRootWindowContainer.resumeFocusedTasksTopActivities();
    }

}

The resumeFocusedTasksTopActivities method in RootWindowContainer goes through all displays to determine which task should be launched or resumed. For each display, forAllRootTasks is called, inside which the top activity is taken ( topRunningActivity). If it’s already in the RESUMED state, then the application transition is simply executed (executeAppTransition). Otherwise, the activity is activated through makeActiveIfNeeded.

If no suitable activity is found on the display, resumeTopActivityUncheckedLocked is called for the focused task. And if there are no focused tasks at all, the system launches the home Activity through resumeHomeActivity.

class RootWindowContainer extends WindowContainer<DisplayContent>
        implements DisplayManager.DisplayListener {

    boolean resumeFocusedTasksTopActivities(
            Task targetRootTask, ActivityRecord target, ActivityOptions targetOptions,
            boolean deferPause) {

        for (int displayNdx = getChildCount() - 1; displayNdx >= 0; --displayNdx) {
            final DisplayContent display = getChildAt(displayNdx);
            final boolean curResult = result;
            boolean[] resumedOnDisplay = new boolean[1];
            final ActivityRecord topOfDisplay = display.topRunningActivity();
            display.forAllRootTasks(rootTask -> {
                final ActivityRecord topRunningActivity = rootTask.topRunningActivity();
                if (!rootTask.isFocusableAndVisible() || topRunningActivity == null) {
                    return;
                }
                if (rootTask == targetRootTask) {
                    resumedOnDisplay[0] |= curResult;
                    return;
                }
                if (topRunningActivity.isState(RESUMED) && topRunningActivity == topOfDisplay) {
                    rootTask.executeAppTransition(targetOptions);
                } else {
                    resumedOnDisplay[0] |= topRunningActivity.makeActiveIfNeeded(target);
                }
            });
            result |= resumedOnDisplay[0];
            if (!resumedOnDisplay[0]) {

                final Task focusedRoot = display.getFocusedRootTask();
                if (focusedRoot != null) {
                    result |= focusedRoot.resumeTopActivityUncheckedLocked(
                            target, targetOptions, false /* skipPause */);
                } else if (targetRootTask == null) {
                    result |= resumeHomeActivity(null /* prev */, "no-focusable-task",
                            display.getDefaultTaskDisplayArea());
                }
            }
        }

        return result;
    }

}

Thus, when the user returns to the application from Recents, the system step by step raises the task from the stack, prepares the root Activity and brings it to the RESUMED state. All this happens sequentially: from searching for the task in the stack — to the final call of makeActiveIfNeeded, which essentially completes the restoration process.

After the window container selects the task for resumption, control passes to the resumeTopActivityUncheckedLocked method inside the Task class. Here the internal resumeTopActivityInnerLocked method is called, which finally determines which Activity needs to be launched.

class Task extends TaskFragment {

    @GuardedBy("mService")
    boolean resumeTopActivityUncheckedLocked(ActivityRecord prev, ActivityOptions options,
                                             boolean deferPause) {
        someActivityResumed = resumeTopActivityInnerLocked(prev, options, deferPause);
    }

    @GuardedBy("mService")
    private boolean resumeTopActivityInnerLocked(ActivityRecord prev, ActivityOptions options,
                                                 boolean deferPause) {
        final TaskFragment topFragment = topActivity.getTaskFragment();
        resumed[0] = topFragment.resumeTopActivity(prev, options, deferPause);
    }

}

In the resumeTopActivityInnerLocked method, the task fragment (TaskFragment) to which the top Activity is bound is extracted. This is where the concrete preparation for launching the application component begins.

Then resumeTopActivity is called in TaskFragment. Here the search for the top activity (topRunningActivity) occurs and the startSpecificActivity method is launched. Essentially, startSpecificActivity is the last point inside the system core where the decision is made: launch a new process for the activity or use an already existing one.

class TaskFragment extends WindowContainer<WindowContainer> {

    final boolean resumeTopActivity(ActivityRecord prev, ActivityOptions options,
                                    boolean skipPause) {
        ActivityRecord next = topRunningActivity(true /* focusableOnly */);
        mTaskSupervisor.startSpecificActivity(next, true, false);
        ...
        return true;
        ...
    }

}

Then the startSpecificActivity method inside ActivityTaskSupervisor. Here the process state is analyzed: if the process already exists and is bound, then the activity will be launched immediately. If the process is absent or was terminated by the system, then startProcessAsync is called to create a new process for this activity.

public class ActivityTaskSupervisor implements RecentTasks.Callbacks {
    ...
    final ActivityTaskManagerService mService;

    void startSpecificActivity(ActivityRecord r, boolean andResume, boolean checkConfig) {
        ...
        mService.startProcessAsync(r, knownToBeDead, isTop,
                isTop ? HostingRecord.HOSTING_TYPE_TOP_ACTIVITY
                        : HostingRecord.HOSTING_TYPE_ACTIVITY);
    }
}

In the startProcessAsync method, the activity is added to the mStartingProcessActivities list. This is a kind of “launch queue” where the system places activities while waiting for the process to be created and bound for them.

public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
    ...

    final ArrayList<ActivityRecord> mStartingProcessActivities = new ArrayList<>();
    RootWindowContainer mRootWindowContainer;

    void startProcessAsync(ActivityRecord activity, boolean knownToBeDead, boolean isTop,
                           String hostingType) {
        ...
        mStartingProcessActivities.add(activity);
        ...
    }
    ...
}

Thus, when we reach the final stage, an important question arises: where is the ActivityRecord ultimately stored and how are the connections between key entities — DisplayContent, WindowContainer, Task (and TaskFragment) — organized? This will help to finally understand how exactly the system manages the state and “life” of Activities on the System Server side.

General structure of the hierarchy Android manages activities and windows in the form of a hierarchical tree of containers, where each container is implemented through the base WindowContainer class. The entire structure starts with the root container RootWindowContainer, inside which DisplayContent is created for each physical or virtual display.

DisplayContent DisplayContent represents a separate physical or virtual display. It is a direct descendant of RootWindowContainer and inside itself stores so-called DisplayAreas, in which different types of windows are segmented (for example, application area, system overlay area, etc.). Inside DisplayContent is TaskDisplayArea, which is responsible for placing user tasks (Tasks).

TaskDisplayArea TaskDisplayArea is a display area where tasks (Task) are added. In most cases, if there’s no multi-window or special modes, a single DefaultTaskDisplayArea is used, where all application tasks are placed. In the hierarchy, the path looks like: DisplayContent → TaskDisplayArea → Task.

Task Task (essentially, “task stack”) groups one or more activities that the user perceives as one application in the Recents list. In Android Task inherits from TaskFragment, making it a container capable of containing child WindowContainer. Usually inside the task, ActivityRecord objects are placed, each representing a specific activity. In more complex cases, for example in split-screen, Task can contain other tasks or TaskFragments. However, in the standard scenario (single screen without split), the task contains a list of ActivityRecords directly.

Here’s the key point: Task is the direct parent for ActivityRecord. This means that all states and context of a specific Activity are stored inside its ActivityRecord, which in turn is always located inside a task. Thus, when the user returns to the application through Recents, the system restores the task, and along with it all nested ActivityRecords.

TaskFragment TaskFragment is a base class used to create sub-containers inside a task. In normal scenarios, we don’t see it directly because we work with Task, which is already an extension of TaskFragment. In some modes (for example, Activity Embedding), separate TaskFragments can be created to split the screen between multiple activities. But if there are no such scenarios, Task itself contains ActivityRecords, and additional TaskFragments are not used.

ActivityRecord ActivityRecord represents a specific instance of Activity in the system. It inherits from WindowToken, which in turn is a child class of WindowContainer. Thus, ActivityRecord is simultaneously both a container for activity windows and a token that WindowManager uses to manage windows. Usually inside ActivityRecord there is one main WindowState (application window), as well as any child windows (for example, dialogs).

The path in the hierarchy looks like this: RootWindowContainer → DisplayContent → TaskDisplayArea → Task → ActivityRecord → WindowState.

This means that ActivityRecord always lives inside a task and never exists by itself or in a global list. That’s exactly why when returning from Recents, the task is first raised as a whole (Task), and then inside it the needed activities are activated ( ActivityRecord).

Such a tree of containers allows the Android system to centrally manage the entire hierarchy of windows and tasks. For example, when changing configuration or unloading a process, the activity state remains “bound” to its ActivityRecord, which lives inside Task. When the task returns to the screen, all tree objects are sequentially restored, and the Activity gets its data back through Bundle associated with its ActivityRecord.

Let’s make a brief summary

  • DisplayContent — top container for the display, includes TaskDisplayArea.
  • TaskDisplayArea — display area for tasks.
  • Task — container grouping one or more ActivityRecords.
  • TaskFragment — intermediate container, used in embedding or split, usually not needed in basic scenario.
  • ActivityRecord — container and token for a specific Activity, always located inside Task.
  • WindowState — child windows of Activity, live inside ActivityRecord.

Thus, the question “where is ActivityRecord stored” can be clearly answered: inside Task, as a child element in the container tree.

This architecture makes task behavior predictable and allows the system to save, pause and restore activities without disrupting the overall application structure in memory. That’s exactly why the user always sees a “complete” task in Recents, and not separate activities.

For a more visual understanding of the hierarchy, you can look at the diagram below, which excellently illustrates the container tree in Android WindowManager (starting from Android 12).

Android WindowManager Hierarchy

Diagram taken from sobyte.net — Android 12 WMS Hierarchy to illustrate the WindowManager hierarchy.

Where and when ActivityRecord is created for the first time

After we’ve figured out where exactly ActivityRecord is stored in the container hierarchy, the next important question arises: when and how does this object actually appear in the system?

All previous chapters showed us how the system manages already existing ActivityRecord — how they are restored from the task stack ( Recents), how they transition between states, how their states are saved. But where does the first instance of ActivityRecord come from when an Activity is launched for the first time, for example, at the very first launch of an application or when starting a new Activity through an intent?

This very moment — the creation of ActivityRecord — can be considered the entry point of the activity into “life” on the system server side. At this stage, the main structure is created, to which everything will be bound in the future: both windows (WindowState), and states (Bundle), and bindings to the task (Task).

Then the system begins to “unfold” the process through the call chain, starting from the top level — ActivityManagerService. When an application or another system component calls startActivity(...), this command first gets into the public API of ActivityManagerService, and from there it paves the way down through the system server layers, where all the objects needed for startup are prepared.

Here’s what this call chain looks like at the first levels:

public class ActivityManagerService extends IActivityManager.Stub,...{

@Override
public int startActivityWithFeature(IApplicationThread caller, String callingPackage,...) {
    return mActivityTaskManager.startActivity(caller, callingPackage, callingFeatureId, intent,...);
}

}

Here ActivityManagerService just redirects the call to ActivityTaskManagerService, where more detailed work with user profiles, intent flags and other checks begins.

public class ActivityTaskManagerService extends IActivityTaskManager.Stub {

    @Override
    public final int startActivity(IApplicationThread caller, String callingPackage, ...) {
        return startActivityAsUser(caller, callingPackage, callingFeatureId, intent, ...);
    }

    private int startActivityAsUser(IApplicationThread caller, String callingPackage, ...) {

        return getActivityStartController().obtainStarter(intent, "startActivityAsUser")
              ...
              .execute();
    }

    ActivityStartController getActivityStartController() {
        return mActivityStartController;
    }

}

In the startActivityAsUser method, we already see a call to ActivityStartController, which manages the process of creating and configuring activity startup. The obtainStarter method returns an ActivityStarter object, which can be called the real “conductor” of the launch. It collects all parameters, checks whether a new task (Task) is needed or an existing one can be used, checks the configuration and finally prepares the ActivityRecord.

public class ActivityStartController {

    ActivityStarter obtainStarter(Intent intent, String reason) {
        return mFactory.obtain().setIntent(intent).setReason(reason);
    }
}

After we get ActivityStarter through obtainStarter, this is exactly where the creation of a new ActivityRecord object happens. ActivityStarter forms all key startup parameters: intent, flags, target Task, window configuration, and also decides whether to create a new task or use an existing one.

The created ActivityRecord is linked to the task, added to the container hierarchy and becomes part of the overall RootWindowContainer structure. After creation, ActivityRecord is stored in the container tree until the activity is finished or removed by the system.


class ActivityStarter {

    private final ActivityTaskManagerService mService;
    private final RootWindowContainer mRootWindowContainer;
    ActivityRecord mStartActivity;

    int execute() {
        ...
        res = executeRequest(mRequest);
        ...
    }

    private int executeRequest(Request request) {
        final ActivityRecord r = new ActivityRecord.Builder(mService)
                 ... // parameters through builder
                .build();

        mLastStartActivityResult = startActivityUnchecked(r, ...);
        ...
    }

    private int startActivityUnchecked(final ActivityRecord r, ...) {
        ...
        result = startActivityInner(r, ...);
        ...
    }

    int startActivityInner(final ActivityRecord r, ...) {
        setInitialState(r, ...);

        mRootWindowContainer.resumeFocusedTasksTopActivities(
                mTargetRootTask, mStartActivity, mOptions, mTransientLaunch);
    }

    private void setInitialState(ActivityRecord r, ...) {
        ...
        mStartActivity = r;
        ...
    }
}

In the executeRequest method, an ActivityRecord object is created through the builder. After initialization, it’s passed to startActivityUnchecked, and then to startActivityInner, where the setInitialState method is called. Here the object is saved in mStartActivity — this is a reference to the current activity that will be launched.

Then the activity is prepared for launch through calling resumeFocusedTasksTopActivities in RootWindowContainer.

class RootWindowContainer extends WindowContainer<DisplayContent>
        implements DisplayManager.DisplayListener {

    boolean resumeFocusedTasksTopActivities(
            Task targetRootTask, ActivityRecord target, ActivityOptions targetOptions,
            boolean deferPause) {

        for (int displayNdx = getChildCount() - 1; displayNdx >= 0; --displayNdx) {
            final DisplayContent display = getChildAt(displayNdx);
            final boolean curResult = result;
            boolean[] resumedOnDisplay = new boolean[1];
            final ActivityRecord topOfDisplay = display.topRunningActivity();
            display.forAllRootTasks(rootTask -> {
                final ActivityRecord topRunningActivity = rootTask.topRunningActivity();
                if (!rootTask.isFocusableAndVisible() || topRunningActivity == null) {
                    return;
                }
                if (rootTask == targetRootTask) {
                    resumedOnDisplay[0] |= curResult;
                    return;
                }
                if (topRunningActivity.isState(RESUMED) && topRunningActivity == topOfDisplay) {
                    rootTask.executeAppTransition(targetOptions);
                } else {
                    resumedOnDisplay[0] |= topRunningActivity.makeActiveIfNeeded(target);
                }
            });
            result |= resumedOnDisplay[0];
            if (!resumedOnDisplay[0]) {
                final Task focusedRoot = display.getFocusedRootTask();
                if (focusedRoot != null) {
                    result |= focusedRoot.resumeTopActivityUncheckedLocked(
                            target, targetOptions, false /* skipPause */);
                } else if (targetRootTask == null) {
                    result |= resumeHomeActivity(null /* prev */, "no-focusable-task",
                            display.getDefaultTaskDisplayArea());
                }
            }
        }

        return result;
    }
}

In the resumeFocusedTasksTopActivities method, all displays and root tasks are traversed. For each task, the top activity is selected, its state and activation possibility are checked. If the task contains the target activity (target), it’s activated by calling resumeTopActivityUncheckedLocked.

Thus, after creating ActivityRecord, the system fully prepares the task and activates the top activity, transitioning it to the RESUMED state. Excellent, let’s continue in exactly the same technical, “steady” style, considering that we’ve already analyzed these methods in detail earlier.

After the window container selects the task for resumption, control passes to the resumeTopActivityUncheckedLocked method inside the Task class. We’ve already encountered this method before — it’s responsible for selecting and final preparation of the top activity inside the task before launch. Inside it, resumeTopActivityInnerLocked is called, which in turn extracts the needed TaskFragment.

class Task extends TaskFragment {

    @GuardedBy("mService")
    boolean resumeTopActivityUncheckedLocked(ActivityRecord prev, ActivityOptions options,
                                             boolean deferPause) {
        someActivityResumed = resumeTopActivityInnerLocked(prev, options, deferPause);
    }

    @GuardedBy("mService")
    private boolean resumeTopActivityInnerLocked(ActivityRecord prev, ActivityOptions options,
                                                 boolean deferPause) {
        final TaskFragment topFragment = topActivity.getTaskFragment();
        resumed[0] = topFragment.resumeTopActivity(prev, options, deferPause);
    }

}

As we remember, in the resumeTopActivityInnerLocked method, the top task fragment (the TaskFragment object) is extracted, which contains the activity ready for launch.

Then resumeTopActivity is called in TaskFragment. This method searches for the top activity in the container (topRunningActivity) and initiates a call to startSpecificActivity. Here the decision is made whether to start a new process or use an already existing one.

class TaskFragment extends WindowContainer<WindowContainer> {

    final boolean resumeTopActivity(ActivityRecord prev, ActivityOptions options,
                                    boolean skipPause) {
        ActivityRecord next = topRunningActivity(true /* focusableOnly */);
        mTaskSupervisor.startSpecificActivity(next, true, false);
        ...
        return true;
        ...
    }

}

We’ve already seen the startSpecificActivity method inside ActivityTaskSupervisor in previous chapters. It checks whether a process for the current activity already exists. If the process is alive and the activity is bound, then the system continues its launch directly. If the process is absent or was unloaded by the system, the startProcessAsync method is called, which is responsible for asynchronous start of a new process.

public class ActivityTaskSupervisor implements RecentTasks.Callbacks {
    ...
    final ActivityTaskManagerService mService;

    void startSpecificActivity(ActivityRecord r, boolean andResume, boolean checkConfig) {
        ...
        mService.startProcessAsync(r, knownToBeDead, isTop,
                isTop ? HostingRecord.HOSTING_TYPE_TOP_ACTIVITY
                        : HostingRecord.HOSTING_TYPE_ACTIVITY);
    }
}

Inside startProcessAsync, as we’ve already analyzed in detail, the activity is added to the mStartingProcessActivities list. This is a queue for those activities that are waiting for the process to be created and bound by the system. Such a queue allows the system to control the launch order and manage resources without losing states.

public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
    ...

    final ArrayList<ActivityRecord> mStartingProcessActivities = new ArrayList<>();
    RootWindowContainer mRootWindowContainer;

    void startProcessAsync(ActivityRecord activity, boolean knownToBeDead, boolean isTop,
                           String hostingType) {
        ...
        mStartingProcessActivities.add(activity);
        ...
    }
    ...
}

Thus, this entire chain of methods that we’ve already encountered earlier closes exactly here: from the call from window containers to the final decision about creating a new process or continuing in the current one. As a result, ActivityRecord is created, saved and activated, and it becomes the key link between the system and the user interface. What happens after calling this method and the subsequent processing logic we’ve already analyzed in detail in previous chapters.

On this note, that’s all — this is the end of the article.

Discussion

Comments