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.
Introduction
This is a continuation of the three previous articles.
- In the first one, we figured out where
ViewModelStore
is ultimately stored in the case ofActivity
, - In the second — how this is arranged in
Fragment
, - In the third, where
ViewModel
s are stored when we use Compose (or even justView
).
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
andonStop
are called, butonSaveInstanceState
is not: when returning from Activity B to Activity A. In this case, the state of B doesn’t need to be restored, soonSaveInstanceState
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 View
s.
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
.
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:
consumeRestoredStateForKey — extracts and removes from memory the
SavedState
(Bundle) that was registered usingregisterSavedStateProvider
. When called again, it returnsnull
.registerSavedStateProvider — registers a
SavedStateProvider
with the specified key. This provider will be used to save state whenonSaveInstanceState
is called.getSavedStateProvider — returns the registered
SavedStateProvider
by key ornull
if not found.unregisterSavedStateProvider — removes a previously registered
SavedStateProvider
from the registry by the passed key.SavedStateProvider — interface providing a
SavedState
(Bundle) object when saving state.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:
consumeRestoredStateForKey
- gets the value fromrestoredState
(Bundle) by key, after getting the value, removes the value and key fromrestoredState
(Bundle),restoredState
is the most rootBundle
that stores all other bundles inside itselfregisterSavedStateProvider
- simply adds theSavedStateProvider
object inside thekeyToProviders
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"
}
}
performSave
— called whenActivity
orFragment
transitions topause
->stop
state, i.e., at the momentonSaveInstanceState
is called. This method is responsible for saving the state of allSavedStateProvider
s registered throughregisterSavedStateProvider
. Inside the method, aninState
object of type SavedState (essentially, it’s theBundle
itself) is created. If there’s already data in restoredState, it’s added toinState
. Then, in a synchronized block, all registeredSavedStateProvider
s are iterated through, thesaveState()
method is called, and the results are saved ininState
. At the end, ifinState
is not empty, its contents are written to theoutBundle
parameter under the keySAVED_COMPONENTS_KEY
.performRestore
— called when creating or restoringActivity
orFragment
. This method simply reads fromsavedState
the value by keySAVED_COMPONENTS_KEY
, if it exists. The found value (nestedSavedState
) is saved in therestoredState
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 modernActivity
s.open class ComponentActivity() : ..., SavedStateRegistryOwner, ... { private val savedStateRegistryController: SavedStateRegistryController = SavedStateRegistryController.create(this) final override val savedStateRegistry: SavedStateRegistry get() = savedStateRegistryController.savedStateRegistry }
Fragment
— anyFragment
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 Navigationpublic 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:
defaultViewModelProviderFactory
—SavedStateViewModelFactory
is used as the default factory.defaultViewModelCreationExtras
—SavedStateRegistryOwner
is placed inCreationExtras
under the keySAVED_STATE_REGISTRY_OWNER_KEY
andViewModelStoreOwner
under the keyVIEW_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:
- savedStateRegistryOwner — reference to SavedStateRegistry for state management.
- viewModelStateRegistryOwner — reference to ViewModelStore for lifecycle binding.
- 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:
- SavedStateRegistryOwner — for managing state saving and restoration.
- ViewModelStoreOwner — for ViewModel lifecycle binding.
- 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 asavedInstanceState
parameter of typeBundle
as input. It’s in this method that saved values are read.onSaveInstanceState
— receives anoutState
parameter of typeBundle
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);
}
...
}
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’s
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:
state
—Bundle
object containing saved activity state. Yes, yes, this is the same Bundle we get inonCreate
,onRestoreInstanceState
andonSaveInstanceState
methodslastNonConfigurationInstances
—Activity#NonConfigurationInstance
object, which storesComponentActivity#NonConfigurationInstances
which storesViewModelStore
.intent
—Intent
object representing the activity launch intent.window
—Window
object associated with the activity.activity
— theActivity
object itself.parent
— parent activity (if any).createdConfig
—Configuration
object containing settings applied when creating the activity.overrideConfig
—Configuration
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.execute
→ ActivityThread.handleStopActivity
→ ActivityThread.performStopActivityInner
→ ActivityThread.callActivityOnStop
→ ActivityThread.callActivityOnSaveInstanceState
→ Instrumentation.callActivityOnSaveInstanceState
→ Activity.performSaveInstanceState
→ Activity.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.execute
→ ActivityThread.handleRelaunchActivity
→ ActivityThread.handleRelaunchActivityInner
→ ActivityThread.callActivityOnStop
→ ActivityThread.callActivityOnSaveInstanceState
→ Instrumentation.callActivityOnSaveInstanceState
→ Activity.performSaveInstanceState
→ Activity.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.execute
→ handleRelaunchActivity
→ handleRelaunchActivityInner
→ handleLaunchActivity
→ performLaunchActivity
→ callActivityOnCreate
→ performCreate
→ onCreate
.
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.execute
→ ActivityThread.handleLaunchActivity
→ ActivityThread.performLaunchActivity
→ ActivityThread.callActivityOnCreate
→ Activity.performCreate
→ Activity.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:
- Who creates
LaunchActivityItem
and passes theBundle
to it, which is exactly what survives process death or stopping. - Who calls the
execute
method onLaunchActivityItem
and launches the chain of calls described above:LaunchActivityItem.execute
→handleLaunchActivity
→performLaunchActivity
→callActivityOnCreate
→performCreate
→onCreate
.
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:
- 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
- Finally we see the creation of the
LaunchActivityItem
transaction with passing all necessary arguments, including Bundle - We see that the
getLifecycleManager()
method is called on the ActivityTaskManagerService class which returns aClientLifecycleManager
object and calls the scheduleTransactionItems method on it that we already saw, passingLaunchActivityItem
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
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).
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
No comments yet. Be the first to share your thoughts!