Activity Scheduling and Lifecycle: A Deep Dive into AOSP Source Code
When you call startActivity(), what happens is far more complex than "reflectively instantiating an Activity and invoking onCreate()."
An Activity is fundamentally the result of coordination between the App process and the system_server process. Understanding the lifecycle means understanding how the core system service (ATMS) manages your App's memory state, UI visibility, and input focus across process boundaries.
This article dives into AOSP source code to trace the complete cross-process pipeline from a startActivity() call to the onCreate() callback.
1. Design Philosophy: Inversion of Control
Why doesn't Android expose a main() function and let you drive rendering and input with a while(true) loop?
Memory and focus resources on a mobile OS are highly constrained. The system must retain the authority to forcibly schedule — killing background processes to reclaim memory at any time, or stripping foreground focus when a phone call arrives. To achieve this level of control, Android uses an Inversion of Control (IoC) architecture: App code implements callbacks, and the system decides when to invoke them.
onCreate, onResume, and similar methods are not invoked by you — they are commands issued by the system according to its own state machine.
2. Macro Architecture: Cross-Process Dispatch Overview
Before stepping through source code, let's map out the division of responsibilities between system_server and the App process:
┌──────────────────────────────────────────────────────────────────────────┐
│ System Process (system_server) │
└──────────────────────────────────────────────────────────────────────────┘
[ActivityTaskManagerService] (ATMS: global scheduler for all Activities)
│
│ (1. Delegates to ActivityStarter)
▼
[ActivityStarter] (Resolves Manifest, handles LaunchMode, creates ActivityRecord)
│
│ (2. Triggers focus switch and stack operations)
▼
[RootWindowContainer] (Manages all display containers)
│
│ (3. Schedules task stacks)
▼
[Task] (Activity task stack)
│
│ (4. System holds ActivityRecord, not the Activity instance itself)
▼
[ActivityRecord] (System-side state record for an Activity; holds IApplicationThread Binder ref)
│
│ (5. Sends ClientTransaction cross-process via IApplicationThread)
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈│┈┈┈┈┈┈┈┈ Binder IPC ┈┈┈┈┈┈┈┈┈┈┈┈┈┈
│
┌─────────────────│────────────────────────────────────────────────────────┐
│ ▼ Application Process (App Process) │
└─────────────────│────────────────────────────────────────────────────────┘
[ApplicationThread] (Binder server-side stub for IApplicationThread; receives system commands)
│
│ (6. Switches to main thread via Handler)
▼
[ActivityThread] (Main thread message loop; dispatches performLaunchActivity etc.)
│
│ (7. Delegates instantiation to Instrumentation)
▼
[Instrumentation] (Handles Activity object creation and lifecycle dispatch; also the test injection point)
│
│ (8. Reflectively invokes no-arg constructor via ClassLoader.loadClass().newInstance())
▼
[Activity] (Activity instance; not yet attached — cannot access Context)
│
│ (9. Calls attach() to establish context association)
▼
[ContextImpl] & [PhoneWindow] (Grants resource access and window drawing capabilities)
Key design point: The system holds ActivityRecord, not the Activity instance. ActivityRecord communicates with the App process via the IApplicationThread Binder interface. Communication between system and App is bidirectional Binder calls.
3. Full-Pipeline Source Code Walkthrough
Phase 1: App Process Initiates a Cross-Process Request
Everything starts from the App side and eventually crosses the Binder boundary to the system process.
Call chain:
Activity.startActivity(Intent)Activity.startActivityForResult()Instrumentation.execStartActivity(): Acquires the ATMS Binder ProxyActivityTaskManager.getService().startActivity(...): Cross-process invocation
Source code:
// frameworks/base/core/java/android/app/Instrumentation.java
public ActivityResult execStartActivity(...) {
try {
// Cross-process call to ATMS via Binder
int result = ActivityTaskManager.getService().startActivity(...);
// If the Activity is not declared in Manifest, ATMS returns an error code
// This throws ActivityNotFoundException
checkStartActivityResult(result, intent);
} catch (RemoteException e) { ... }
}
Why this design: Every component launch must go through the system service for permission validation, memory management, and focus arbitration. Apps cannot bypass the system to create visible UI components on their own.
Phase 2: system_server Executes Scheduling Logic
After the request reaches system_server, ATMS delegates the actual work to ActivityStarter.
Call chain:
ATMS.startActivity(): Entry pointActivityStarter.execute(): Queries PMS to validate Manifest; createsActivityRecordActivityStarter.startActivityUnchecked(): HandlesLaunchModeandFlags; determines target task stackRootWindowContainer.resumeFocusedTasksTopActivities(): Triggers the foreground resume flowTask.resumeTopActivityUncheckedLocked(): Finds an Activity in Resumed state already at the top of the stack
Source code:
// frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java
int execute() {
// Query PackageManagerService to verify the Activity is declared in Manifest
ActivityInfo aInfo = mService.resolveActivityInfoForIntent(...);
// Create ActivityRecord on the system side — the Activity class in the App process
// is not loaded yet at this point
ActivityRecord r = new ActivityRecord(mService, ...);
// Decide stack position based on LaunchMode and Task Flags
return startActivityUnchecked(r, ...);
}
// frameworks/base/services/core/java/com/android/server/wm/Task.java
boolean resumeTopActivityInnerLocked(...) {
// The previous Activity is still in Resumed state at the top of the stack
if (mResumedActivity != null) {
// The previous Activity must complete onPause before the new one can start
// This sends PauseActivityItem to the App process
startPausingLocked(userLeaving, false /* uiSleeping */, top);
// Critical point: after sending the Pause instruction, this method returns immediately
// The new Activity's startup is suspended until the App process reports back via activityPaused()
return true;
}
}
Why this design: On a single-screen device, only one Activity can be in the Resumed state at a time. The system must wait for the previous Activity to complete onPause and report back before launching the new one, preventing two Activities from simultaneously holding input focus and rendering resources.
Phase 3: Foreground Activity Executes onPause and Reports Back
The system sends PauseActivityItem to the App process via Binder.
Call chain:
Task.startPausingLocked()→ cross-process callApplicationThread.scheduleTransaction(): Binder server-side receives the instructionActivityThread.H: Handler switches to the main threadTransactionExecutor.execute(): Parses and executes the transactionActivityThread.handlePauseActivity()Activity.performPause()→onPause(): Foreground Activity executes pauseActivityTaskManagerService.activityPaused(): App proactively notifies system — releases the wait
Source code:
// frameworks/base/core/java/android/app/ActivityThread.java
@Override
public void handlePauseActivity(...) {
// Triggers Activity.onPause()
performPauseActivity(r, finished, reason, pendingActions, ...);
// After onPause() completes, must actively notify ATMS
// This unblocks the suspended state in system_server from Phase 2
ActivityTaskManager.getService().activityPaused(r.token);
}
Critical performance pitfall: Doing disk I/O or slow synchronous operations inside onPause() directly delays the activityPaused() callback. While ATMS is waiting for this callback, the entire new Activity startup pipeline is blocked. This is why the documentation strictly requires onPause() to complete quickly.
Phase 4: ATMS Resumes and Sends the Launch Transaction
After receiving activityPaused(), ATMS resumes execution and sends a transaction to the App process to start the new Activity.
Call chain:
ActivityStackSupervisor.realStartActivityLocked(): Enters the actual launch logicClientLifecycleManager.scheduleTransaction(): Packages aClientTransactionand sends it cross-process
Source code:
// frameworks/base/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
void realStartActivityLocked(ActivityRecord r, WindowProcessController proc, ...) {
// Package an atomic transaction (ClientTransaction)
final ClientTransaction clientTransaction = ClientTransaction.obtain(
proc.getThread(), r.appToken);
// Instruction 1: LaunchActivityItem — create the Activity instance and execute onCreate
clientTransaction.addCallback(LaunchActivityItem.obtain(...));
// Instruction 2: ResumeActivityItem — declares the final target state must reach onResume
// Missing intermediate steps (onStart) are filled in automatically by TransactionExecutor
clientTransaction.setLifecycleStateRequest(ResumeActivityItem.obtain(...));
// Send the entire transaction cross-process via Binder
mService.getLifecycleManager().scheduleTransaction(clientTransaction);
}
Why a transaction package instead of multiple Binder calls: Before Android 8, each lifecycle callback was a separate Binder call. Under extreme conditions (e.g., CPU preemption causing packets to arrive out of order), this caused lifecycle state corruption. ClientTransaction wraps a complete lifecycle transition as an atomic package executed sequentially by TransactionExecutor on the App side, fundamentally eliminating ordering issues.
4. Inside the App Process: From Unboxing to onCreate()
Once LaunchActivityItem reaches ActivityThread in the App process, performLaunchActivity() handles the actual Activity creation in three steps:
① Reflective Instantiation
// frameworks/base/core/java/android/app/ActivityThread.java
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
// Reflectively invokes the no-arg constructor via ClassLoader
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
}
Note: At this point, the Activity instance has no Context and no Window. Calling any method that touches resources or UI will crash.
② attach(): Establishing Context and Window
// Create a dedicated ContextImpl instance for this Activity
ContextImpl appContext = createBaseContextForActivity(r);
// attach() is the key method that gives the Activity its full capabilities
activity.attach(appContext, this, getInstrumentation(), r.token, ...);
attach() does three things internally:
- Inject Context: Sets
ContextImplas themBaseof theContextWrapperbase class. From this point, the Activity can access resources, start components, etc. - Create PhoneWindow:
mWindow = new PhoneWindow(...)— the prerequisite forsetContentView()to work. - Store IBinder Token:
r.tokenis the unique identity credential issued by the system. All communication between the Activity and the system is associated through this token.
③ callActivityOnCreate(): Trigger onCreate
// The Activity now has a complete runtime environment; safe to trigger onCreate
mInstrumentation.callActivityOnCreate(activity, r.state);
When you call setContentView() inside onCreate(), PhoneWindow is already initialized, allowing the View hierarchy to be properly constructed.
5. Automatic Lifecycle Path Completion
You may notice that realStartActivityLocked() only sets LaunchActivityItem (corresponding to onCreate) and the final target ResumeActivityItem (corresponding to onResume). How does onStart get triggered?
This is handled by the path-completion logic in TransactionExecutor:
// frameworks/base/core/java/android/app/servertransaction/TransactionExecutor.java
private void executeLifecycleState(ClientTransaction transaction) {
// Get the target state (ResumeActivityItem corresponds to ON_RESUME)
final ActivityLifecycleItem lifecycleItem = transaction.getLifecycleStateRequest();
// Query the Activity's current state (just ran onCreate, so currently ON_CREATE)
int currentState = r.getLifecycleState();
// Target state (ON_RESUME)
int targetState = lifecycleItem.getTargetState();
// Calculate the intermediate states needed to get from current to target
// Result: [ON_START, ON_RESUME]
final IntArray lifecycleItemRequest = mHelper.getLifecyclePath(
currentState, targetState, excludeLastState);
// Execute each intermediate state's callback in order
performLifecycleSequence(r, lifecycleItemRequest);
}
Inside performLifecycleSequence(), the path array is iterated, sequentially triggering handleStartActivity() (onStart) and handleResumeActivity() (onResume).
Problem this design solves: Cross-process communication carries a risk of packet loss. If each lifecycle callback required a separate system-issued packet, losing one would leave an Activity stuck in an intermediate state. Declarative target state + App-side automatic path completion makes lifecycle transitions idempotent: as long as the final target state is correct, the intermediate path is always executed completely.
6. LMK and the Reliability Boundary of onDestroy
A common mistake is placing important data persistence logic in onDestroy(), assuming it will always be called when an Activity is destroyed.
The reality is: the system does not guarantee that onDestroy() will ever execute.
onDestroy() is only reliably called in two scenarios:
- Code explicitly calls
finish() - The system recreates the Activity due to a configuration change (e.g., screen rotation)
Under memory pressure, the situation is completely different. When an App process's oom_adj value is lowered by the system due to background status, and a foreground process needs memory, the Linux kernel's LMK (Low Memory Killer) sends SIGKILL (kill -9) to terminate the process.
SIGKILL cannot be intercepted. It forcibly terminates the process at the kernel level. The ART VM has no opportunity to execute any Java code — including onDestroy(), ShutdownHook, or any other cleanup logic. All in-memory state is instantly gone.
Practical conclusions:
- Data that needs to be persisted must be saved in
onPause()oronStop(), notonDestroy(). - UI state (scroll position, input content, etc.) should use
ViewModel+onSaveInstanceState()so it can be restored when the process is recreated. - Async tasks (network requests, database operations) should be cancelled or have their state persisted in
onStop(), not relied upon to clean up inonDestroy().
Understanding LMK's mechanism is what allows you to correctly diagnose state-loss bugs when an App is force-killed, rather than chasing the wrong callback.