When you tap an app icon, Android doesn’t just “open the app.” It asks the system to start a process, load your code into it, and hand control to a component. Most lifecycle and state bugs become obvious once you can picture this layer.

What a process actually is

Each Android app runs in its own Linux process with a unique user ID. By default, all components of an app — activities, services, broadcast receivers — run in a single process on the main thread (also called the UI thread).

The process is created lazily: it doesn’t exist until something needs it, such as the user launching your launcher activity or the system delivering a broadcast.

// Every component you declare runs in the app's default process
// unless you opt into a separate one via android:process.
<service
    android:name=".SyncService"
    android:process=":sync" />

The leading colon (:sync) creates a private process named com.example.app:sync. This isolates heavy or crash-prone work, at the cost of extra memory and inter-process communication overhead.

The process lifecycle

Android ranks every running process by importance. When memory runs low, the system kills the least important processes first. From most to least important:

  1. Foreground — has an activity the user is interacting with, or a foreground service.
  2. Visible — has a visible (but not focused) activity.
  3. Service — running a started background service.
  4. Cached — no active components; kept around only to speed up relaunch.

Your app doesn’t get a callback when its process is killed in the background. The process simply disappears. This is why you can never rely on onDestroy() being called.

What survives a process death

When the system kills a cached process and the user returns, Android creates a new process and tries to restore the user’s place. Knowing what is and isn’t preserved is critical:

  • Saved instance state (onSaveInstanceState) is restored — small UI state only.
  • ViewModel is not restored; it lives only as long as the process.
  • SavedStateHandle bridges the two: it survives process death like saved state.
  • Static fields and singletons are reinitialized from scratch.
class SearchViewModel(
    private val state: SavedStateHandle
) : ViewModel() {
    // Survives process death because it's backed by saved state.
    var query: String
        get() = state["query"] ?: ""
        set(value) { state["query"] = value }
}

Testing process death

The easiest way to reproduce a killed process during development:

  1. Open your app, navigate somewhere with state.
  2. Press Home.
  3. In Android Studio, click Terminate Application (the red stop button), or run adb shell am kill com.example.app.
  4. Reopen the app from Recents.

If state is gone or the app crashes, you have a restoration bug.

Key takeaways

  • A process is created on demand and can be killed at any time in the background.
  • Process importance — not your code — decides what gets killed first.
  • Persist anything the user would be upset to lose using SavedStateHandle or disk.
  • Always test the “killed and restored” path; it’s where real-world crashes hide.