Mobile Computing · Mobile Platforms

Lifecycle Callbacks and the Process Importance Ranking

Why an app receives stopped and destroyed notifications in a fixed order, how the platform derives that order from a ranked list of processes, and which transitions are not reversible.

Lifecycle callbacks are usually taught as a sequence: creation, start, resume, pause, stop, destroy. The sequence is real, but it is not the mechanism. The callbacks are notifications of decisions that a system service has already made about the process, and the decision is driven by a ranking, not by a timer. Once the ranking is understood, the order of callbacks, the cases where a callback is skipped, and the cases where none of them arrive at all become predictable.

The ranking is the primary object

The activity manager maintains, for every process it has started, a record of what that process is currently doing on behalf of the user. Each record carries an importance level and a rank within the level. The levels, from most to least protected, are roughly these:

LevelConditionReclaim consequence
ForegroundHosts the activity the user is interacting with, or a service the platform treats as foreground workNot a candidate under normal pressure
VisibleHosts a visible but unfocused activity, or a service the user is actively aware ofReclaimed only under severe pressure
Perceptible backgroundHosts an activity the user can still perceive indirectly, or a service that is doing work on the user’s behalfCandidate after visible processes
CachedHosts no visible activity and no active serviceFirst to be reclaimed, oldest first

The ordering is not advisory. It is the input to the memory reclaim logic described in Memory Pressure, Reclaim Order, and the Kill Decision, and it is the input to lifecycle dispatch. An app does not decide when it is stopped; the service decides that the app’s process is no longer visible, and then tells it.

Two properties of this design are worth noting. First, importance is a property of the process, not of a component. An app with two activities in separate tasks has one process and one importance level, computed from the most important component it hosts. Second, importance is recomputed on every relevant event, not polled. A transition in the window manager, a service starting or stopping, a broadcast being delivered, or a binder call arriving all trigger recomputation.

The ranking is also what the platform uses when it must decide whether an app is permitted to perform an operation that requires the user’s attention. A service that would otherwise need to track activities can ask for the importance of the caller’s process instead, and refuse without showing any interface when the answer indicates the caller is not visible.

// Illustrative: a service gating an operation on caller importance.
int importance = activityManager.getUidImportance(callerUid);
if (importance > RunningAppProcessInfo.IMPORTANCE_VISIBLE) {
    // Caller has no visible component; refuse rather than show a dialog.
    return RESULT_DENIED_BACKGROUND;
}

How a transition becomes a callback

The service does not call into the app synchronously from the thread that made the decision. It enqueues a message to the app’s main thread and waits for the app to acknowledge, with a timeout. The app’s main thread runs a loop that takes messages from a queue and dispatches them; lifecycle callbacks are ordinary messages in that queue, which has three consequences.

Callbacks are serialised. Two transitions cannot interleave, even if the decisions that caused them were made concurrently by different system threads. The app therefore observes a total order over state changes, which is what makes the state machine model valid at all.

Callbacks are delayed by whatever the main thread is doing. A callback that blocks the main thread for a second delays every subsequent lifecycle notification by at least that second, and if the service’s timeout expires before the acknowledgement arrives, it treats the app as unresponsive. The timeout is enforced by the service, not by the app, and the response is to show a dialog or, for a broadcast receiver, to abandon the delivery.

Callbacks can be skipped. If the ranking changes twice before the app processes the first notification, the service may collapse the intermediate state. The app is told that it is now stopped; it is not told that it was briefly paused and then stopped. This is why code that assumes onPause always precedes onStop is fragile, and why the platform’s own guidance describes the callbacks as notifications of states rather than as steps in a script.

The irreversible transitions

Most lifecycle transitions are reversible: an app that was paused can be resumed, one that was stopped can be restarted. Two are not, and they are the ones that determine what an app must persist.

The first is process death. The process can be reclaimed at any moment after its importance drops to cached, with no callback. There is no “about to be killed” notification, because the whole point of the reclaim is to free memory immediately, and notifying the app would require waking it, which allocates. The app must therefore treat the last moment it was in the foreground as the deadline for writing anything that must survive. What is written in a paused or stopped state is a best-effort optimisation, not a guarantee.

The second is configuration change. When the display rotates, the locale changes, or a font scale is adjusted, the default behaviour destroys and recreates the activity so that it can be laid out against the new configuration. This is a destroy that is not caused by the app leaving the screen, and it is distinguishable from the real thing only by a flag the service passes with the destroy notification. An app that cannot afford the recreation declares that it handles the configuration change itself, and then receives a callback with the new configuration instead. The trade is explicit: handling it means the app must re-layout its own view hierarchy, and failing to do so produces a view sized for the old configuration.

Cooperative cancellation is not lifecycle

A separate mechanism covers the case where the app is alive but the user has moved on. An activity that is finishing, or a task that the user has dismissed, gets a cancellation signal in addition to the lifecycle callbacks. On Apple’s platforms this is the difference between an app being backgrounded and an app’s scene being disconnected; on Android it is the difference between an activity being stopped and being finished.

The distinction matters because cleanup that is correct for one is wrong for the other. Releasing a camera session when the app is merely obscured means reacquiring it, with its latency, when the user returns. Releasing it when the activity is finishing is mandatory, because the activity will not return and the device resource must be freed. The platform cannot infer which the app intends, so it reports both events and leaves the choice to the app.

Ordering guarantees that are load-bearing

Within a single process, the ordering between lifecycle callbacks and other callbacks is specified, and the specification is what makes certain patterns safe.

A service is created before it is started, and started before it is bound. A broadcast receiver’s callback completes before the next broadcast is delivered to the same receiver. An activity’s onResume completes before input events are dispatched to it, which is why registering a listener in onResume and unregistering in onPause does not lose events. These orderings are properties of the main-thread queue, and they hold as long as the app does not move work off that queue and reintroduce concurrency.

The ordering between two activities in the same process deserves a precise statement, because it is the one most often assumed wrongly. When activity A starts activity B, B is created, started, and resumed before A is stopped, and both share one main thread, so the sequence is strictly nested: A pauses, B creates, B starts, B resumes, A stops. An app that releases a shared resource in A’s stop callback and expects B to have acquired it by then is relying on an ordering the platform does not provide, because B’s resume callback has already run and completed before A’s stop callback was even queued.

Transitions with no clean predecessor

An app with no visible activity and no running service is a cached process. It has no lifecycle state to be in; the callbacks it last received describe the state it was in when it stopped being important. If it is later resumed, it receives the callbacks needed to bring it from that state to the resumed state, and the intermediate ones may be collapsed.

A process that is killed and restarted does not resume where it left off. The service creates a new process, and the app is asked to recreate its state from what it persisted, which is the subject of Saved State, Process Death, and the Bundle Size Limit. The lifecycle begins again from creation, and any state that was held only in memory is gone.

An app that holds a foreground service is not reclaimable, but the platform restricts how long a service may run in that state and what it may do while there. The restriction is a separate policy layered on the ranking, and it is enforced by the same service that computes the ranking, which is why a foreground service that exceeds its allowance is demoted rather than warned.

References

  1. Android Developers — The activity lifecycle — Android Developers
  2. Apple Developer Documentation — Managing the app life cycle in UIKit — Apple Inc.
  3. Android Developers — Processes and app lifecycle — Android Developers