Let’s take a more in-depth look at processes, a concept you’ve almost certainly encountered before. Processes exist on desktop operating systems as well as on mobile devices, and since Android is built on Linux, it naturally adopts Linux’s process model. At a fundamental level, every piece of running code must execute within a process.
Read the previous article in this series here.
A process is essentially an isolated container for a program. Each process manages its own memory space, which is used to allocate variables and objects. This memory space is protected so that other processes cannot overwrite it, ensuring isolation and stability across the system.
In addition to memory, a process owns certain system resources and—most importantly—contains at least one thread responsible for executing code. On Android, this primary thread is known as the main thread. While a process can host multiple threads, it must have at least one; without a thread, no code can be executed.
What Makes Up a Process?
If we examine a process more closely, it consists of several core components:
- Application Code
This is the actual Android code that needs to be executed.
- Data Segment
The data segment stores global and static variables. These variables exist for the entire lifetime of the process, which is why they are stored separately from temporary data.
- Heap Storage
The heap is used for dynamically allocated memory. Objects that outlive a single function call—such as objects shared across multiple methods—are stored here. For example, a global variable like counter would be allocated on the heap.
class MainActivity : ComponentActivity() {
private var counter = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val string = helloWorld()
setContent {
AndroidInternalsTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
- Stack
The stack stores data related to function calls, including local variables, function parameters, and return addresses. Variables declared inside a function are allocated on the stack and are removed once the function finishes execution.
private fun helloWorld(): String {
var count = 0
return "Hello world!"
}
- CPU Pointers
CPU pointers reference the next instruction to be executed. One of the most important of these is the program counter, which tells the CPU which instruction should run next.
- Resource Handles
Resource handles represent access points to system resources such as files, network sockets, or hardware components. For example, open file descriptors, Bluetooth connections, or web sockets are all managed through resource handles.
This model describes how processes work in a pure Linux environment. Android builds on top of this foundation and introduces additional constraints and abstractions. Android aims to exercise more control than a traditional Linux system, particularly to manage limited resources on mobile devices.
Because apps run inside processes, Android can monitor how much CPU time a process consumes, how much memory it uses, and how much battery it drains. The system uses this information to decide whether a process should remain alive. If a process is consuming excessive resources while providing little user value, Android may terminate it to preserve system health.
Processes vs. Components on Android
On Android, it’s important to distinguish between a process and a component.
When you tap an app icon, the app is launched inside a process. In a simple app, that process might initially host only a single MainActivity. However, activities are just one type of Android component. Other component types include services, broadcast receivers, and content providers.
Multiple components can run inside the same process. Just because an app contains several activities or services does not mean each one runs in its own process by default.
That said, developers can explicitly configure certain components to run in separate processes. This is done in the AndroidManifest.xml file using the android:process=":process_name" attribute. Running components in separate processes is useful when isolation or dedicated resources are required.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
...>
<activity
android:name=".MainActivity"
android:exported="true"
android:process=":" //launch new process for this activity
...>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Inspecting and Managing Processes with ADB
You can inspect all running processes on an Android device or emulator using the Android Debug Bridge (ADB), which provides a command-line interface to connected devices.
To list all active processes, run:
adb shell ps -A
This command outputs every running process, including your app’s process. You can identify your app by its package name in the NAME column, along with additional information such as the process ID (PID) and the user under which it is running.
To simulate a process being killed—a useful technique for testing lifecycle handling and resilience—you can force-stop your app with the following command:
adb shell am force-stop <your_app_package_name>
Understanding how processes work and testing how your app behaves when they are terminated, is essential for building robust Android applications that can handle crashes, background kills, and memory pressure gracefully.
In the next article, we’ll look at the zygote process, followed by process lifecycle and importance hierarchy.
