Core Concepts

Process and Lifecycles

How Android and iOS differ from desktop in process ownership - Android's kill priority, Activity/Fragment/Service lifecycles, ProcessLifecycleOwner, background execution limits, and Doze / App Standby.

Free~25 min

Mobile operating systems - Android and iOS alike - do not treat apps the way desktop OSes do. On a desktop, a process you launch keeps running until you close it: it can hold open sockets, spin background threads, and poll timers indefinitely, because the power cord is in the wall. On a phone, the device runs on a battery, the user is looking at one app at a time, and a background process draining power is the most common reason users uninstall.

Both platforms solve this the same way: the OS, not the app, owns the process lifecycle. An app on screen gets full access to CPU, network, and sensors. An app the user has left gets progressively less - its callbacks fire, its process becomes reclaimable, and under memory pressure it may be killed without further app code. The app's job is to persist anything it needs before that happens, and to use the platform-provided schedulers if it has work that must run while the user is not looking.

The difference is in the mechanism. iOS has a true suspended state: when an app goes to background, UIKit freezes the process - it stays in memory but executes no code, and the system can terminate it at any time. Android does not suspend processes. A backgrounded Android process stays in memory and can still run code if it has active components, but the system is free to kill it whenever it needs the RAM. There is no "frozen but alive" middle ground on Android - a process is either running or gone. This is why Android has such a detailed callback contract: the system tells the app exactly which state it is entering so the app can persist before the kill comes, because the kill itself will not run any app code.

Process importance and reclaim

Android ranks every process by what it is hosting. The system uses the most important level found among all active components in the process. Under memory pressure it reclaims from the bottom of this list upward:

ImportanceTypical hostReclaim behavior
ForegroundResumed Activity the user is interacting with; or a Service / BroadcastReceiver mid-callbackLast resort; only when memory is so low the device reaches paging state
VisiblePaused-but-visible Activity (for example behind a dialog); or a running foreground serviceKept unless Foreground processes need the RAM
ServiceStarted Service via startService()Kept after Visible is gone; services running ~30+ minutes can be demoted toward Cached
CachedNo needed components; often Activities that already returned from onStop()First to go; the only processes normally involved in resource reclamation

A foreground service does not put the process in Foreground importance. Official process-lifecycle wording treats a running foreground service as Visible: user-aware work, still below a resumed Activity. If process A binds to a Service in process B (with BIND_AUTO_CREATE) or uses a ContentProvider in process B, then process B's classification is always at least as important as process A's - dependency raises the callee.

Once onStop() has returned, the process sits in Cached and the system may end it without running further app code. Official Android documentation is explicit: when the system kills a background app for memory, onDestroy() is not guaranteed to be called. The process is killed by the system; no app code runs after that point. Write anything that must survive into onSaveInstanceState / SavedStateHandle or to durable storage on that stop path. Under rising pressure the system may also call onTrimMemory() so the app can drop caches while it still can; Memory and GC covers the trim levels.

lmkd (the Low Memory Killer daemon) is what actually reclaims cached processes. It is a userspace process that monitors memory pressure using PSI (Pressure Stall Information, Android 10+) or the older vmpressure signals, and kills the least essential process when memory runs short. On the next cold start, ActivityManager.getHistoricalProcessExitReasons() (API 30+) can report that exit as REASON_LOW_MEMORY, or as REASON_SIGNALED with status SIGKILL on devices that do not expose a dedicated low-memory reason. Call ActivityManager.isLowMemoryKillReportSupported() to know which report you will see. Crashes, ANRs, and force-stops use other reason codes on the same API.

Warning
onDestroy is best-effort on reclaim - When the system kills a background app for memory, there is no guarantee that onDestroy runs. Persist on the stop path or to disk.

Sources: Processes and app lifecycle · Activity state changes · ApplicationExitInfo · lmkd

Activity, Fragment, and Service lifecycles

The callback contract is how the system tells an app which state it is entering. Every callback is a checkpoint: persist here, release here, start here - because the next callback may never come.

Activity

onPause() is the contract for "stop interacting with the user" - it must return quickly because the next Activity cannot start until it does. onStop() is "no longer visible" and the safer place for heavier teardown. onDestroy() is not guaranteed to be called (process kill skips it), so it is not a reliable cleanup hook.

Configuration changes (rotation, locale, dark mode, font scale) destroy and recreate the Activity by default. Anything held only on the Activity is rebuilt. Put UI state that should survive recreation in a ViewModel. ViewModel alone does not survive process death; that requires SavedStateHandle (or other persisted storage).

Fragment

A Fragment has the same callback shape as an Activity, plus a view lifecycle nested inside the Fragment instance lifecycle. On the back stack, onDestroyView() can fire while the Fragment instance stays alive (onDestroy() does not run yet). The Fragment outlives its view.

If view bindings or Flow collectors live in Fragment fields scoped to this, the view can die while the Fragment still holds the reference. Collect Flows with viewLifecycleOwner (and usually repeatOnLifecycle), not the Fragment as LifecycleOwner. Collection then stops when the view dies and restarts when a new view attaches.

Service

A Service is a component without UI. It can run work on the main thread (typical for IPC) or spawn its own threads. For deferred background work today, prefer WorkManager. Bound, started, and foreground services have different lifetime rules:

TypeLifetimeUse for
BoundWhile at least one client is boundIPC with another process
StartedUntil stopSelf() or stopService()Mostly obsolete for deferred work; use WorkManager
ForegroundWhile the notification is visibleUser-perceived ongoing work (music, navigation, upload)

Foreground-service rules tightened across API levels. API 28 added the FOREGROUND_SERVICE permission. API 29 (Android 10) added the android:foregroundServiceType manifest attribute and the typed startForeground(int, Notification, int) overload. On apps that target API 34 (Android 14)+, a type is required: declare it in the manifest, pass the matching type when promoting the service, and hold the matching FOREGROUND_SERVICE_<TYPE> permission (dataSync, mediaPlayback, location, phoneCall, and others). Calling startForeground() without a declared type throws MissingForegroundServiceTypeException. A type that does not match the work, or a missing type permission, fails at runtime (IllegalArgumentException / SecurityException).

Sources: Foreground service types required (Android 14) · FOREGROUND_SERVICE permission · typed startForeground (API 29)

Application and ProcessLifecycleOwner

Every process constructs one Application instance. onCreate() runs once per process, including after process death and recreation. Keep it free of signed-in-user work: that code re-runs on every cold start. Heavy synchronous work here blocks the first frame on every launch. Defer with the App Startup library or a coroutine that does not gate the first frame.

ProcessLifecycleOwner exposes a Lifecycle for the whole process. Its ON_START fires when any Activity in any task becomes visible; ON_STOP fires when the last Activity goes away. Use it for process-level foreground and background transitions. A single Activity's onStop also fires on intra-app navigation, so process-wide analytics and reconnect hooks belong on ProcessLifecycleOwner.

ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
    override fun onStart(owner: LifecycleOwner) { analytics.appForegrounded() }
    override fun onStop(owner: LifecycleOwner)  { analytics.appBackgrounded() }
})

Sources: Application · ProcessLifecycleOwner

Background work and why the OS restricts it

So far the picture is: the app runs fully when foregrounded, and the system reclaims it when cached. But many real apps have work that must happen while the user is not looking - a photo upload that should continue after the user switches to another app, a crash report that must flush on the next network connection, a feed that should sync periodically. The question is: how does an app do background work when the OS owns the process lifecycle and can kill it at any time?

The naive answer - "start a background thread and keep the process alive" - is what early Android allowed, and it is what caused the battery problems that shaped every API change since. A process sitting in the background holding a wakelock, polling a server, or running a timer prevents the CPU from sleeping and burns battery for work the user is not even aware of. Google's own telemetry showed that background apps were the dominant source of battery drain on real devices, which is why each successive Android version has tightened what a background process is allowed to do.

Android 8.0 (API 26) was the inflection point. It introduced background execution limits: while an app is idle (in the background for several minutes), the system stops its background services as if the app had called Service.stopSelf(). The app has a window of several minutes after going to background during which it can still use services; after that, they are stopped. A new startForegroundService() method lets an app start a service that must call startForeground() within 5 seconds to show a user-visible notification - if it does not, the system stops the service and declares the app ANR. The same release also restricted manifest-registered broadcast receivers for most implicit broadcasts, forcing apps toward JobScheduler instead.

The modern contract is: you do not keep a background process alive yourself. You hand the work to a platform scheduler that runs it when the system decides it is efficient to do so, and you use a foreground service only when the work is user-visible and ongoing. Four mechanisms, each for a different shape of work:

MechanismSinceWhat it doesHow the system enforces limits
WorkManagerJetpack (uses JobScheduler on API 23+)Reliable, persistent, deferrable work. Persists across reboots in an internal SQLite DB.Inherits all JobScheduler limits. Expedited jobs have a quota. Long-running workers promote to a foreground service and inherit its limits.
JobSchedulerAPI 21Schedule jobs with constraints (network, charging, idle). System batches and defers. Holds a wakelock for the app during execution.Deferred during Doze. Frequency limited by App Standby bucket (API 28+). Throttles runaway apps (API 30+). Max 150 scheduled jobs (API 31+). Job runtime: 1 min (API 21), 10 min (API 23-30), flexible if system not busy (API 31+).
AlarmManagerAPI 1Fire at a specific wall-clock time. setExact() for precise timing.Repeating alarms are inexact since API 19. Doze defers standard alarms to maintenance windows (API 23+). setAndAllowWhileIdle() / setExactAndAllowWhileIdle() fire in Doze but are limited to once per 9 minutes per app. SCHEDULE_EXACT_ALARM permission required on API 31+.
Foreground ServiceAPI 1 (typed since API 29)Long-running, user-visible work. Shows a notification. Process treated as Visible importance.Cannot start from background on API 12+ (ForegroundServiceStartNotAllowedException). Must declare a type on API 34+. dataSync and mediaProcessing types get a 6-hour quota per 24 hours on API 15+. shortService type gets ~3 minutes.

WorkManager is the right default. It uses JobScheduler internally on API 23+, so it inherits all of the system's power-management constraints automatically. For work that needs to run longer than the JobScheduler window allows, a Worker can call setForeground() to promote itself to a foreground service - but that inherits all foreground service restrictions (background-start prohibition, type requirements, time quotas). AlarmManager survives for one case: firing at a specific wall-clock time when missing the deadline is a user-visible failure (an alarm clock, a calendar reminder). For everything else, WorkManager is the answer.

The Background Work primer covers the API contracts in detail. What follows here is the OS-level regime that all of those schedulers must respect.

Doze, App Standby, and background limits

Doze (Android 6 / API 23+) is the device-level power-saving state. It activates when the device is unplugged, stationary, and screen off for a period of time. While Doze is active, the OS applies the following restrictions to all apps:

  • Suspends network access for all apps.
  • Ignores wake locks (except those from Doze-exempt apps).
  • Defers standard AlarmManager alarms - including setExact() and setWindow() - to the next maintenance window.
  • Does not run Wi-Fi scans.
  • Does not let sync adapters run.
  • Does not let JobScheduler run (and therefore WorkManager, which uses it internally).

Periodically the system exits Doze for a brief maintenance window to let apps complete deferred activities: pending syncs, jobs, and alarms run, and apps can access the network. When the maintenance window concludes, the system re-enters Doze. Over time, maintenance windows are scheduled less frequently, reducing battery consumption during longer inactivity. The user wakes the device by moving it, turning on the screen, or connecting a charger - at which point all apps resume normal activity.

Two AlarmManager methods fire during Doze: setAndAllowWhileIdle() and setExactAndAllowWhileIdle(), but they are limited to once per 9 minutes per app. setAlarmClock() alarms continue to fire normally - the system exits Doze shortly before they fire, because these represent user-scheduled wake-ups the user explicitly set.

App Standby buckets (Android 9 / API 28+) are the per-app layer on top of Doze. The system classifies each app by recent usage and throttles background work accordingly. The four standard buckets (Active, Working Set, Frequent, Rare) shipped in API 28. STANDBY_BUCKET_RESTRICTED was added in API 30 and is active by default from Android 12 (API 31):

BucketNetwork / job frequencyWhen
ActiveUnrestrictedApp in foreground recently, or user tapped a notification
Working SetFrequentDaily use
FrequentLess frequentWeekly use
RareMinimalMonthly use
Restricted (API 30+; active by default API 31+)Jobs run once per day in a 10-minute batched session; one alarm per day; fewer expedited jobsApp idle 8 days (API 33+) or 45 days (API 31-32), or excessive broadcasts/bindings in 24h

A foreground service with its required visible notification keeps the process at Visible importance, so the system will not reclaim it for memory while the foreground service runs. The Doze page states network access is suspended for all apps during Doze; it documents no foreground service exemption from that rule (the only documented exemption is the battery-optimization allowlist). Standard JobScheduler jobs and AlarmManager alarms are deferred by Doze regardless of whether the app has a foreground service running - the foreground service is exempt from process reclamation, not from Doze's scheduling restrictions.

The system dynamically assigns and reassigns buckets. It may use a preloaded ML model to predict app usage; if no model is present, it defaults to sorting by recency of use. User interactions that promote an app to Active include launching an activity, running a long-running foreground service, or tapping a notification (swiping it away does not count). Restrictions apply only while on battery power, except for the Restricted bucket, which applies even when charging (though loosened when charging, idle, and on unmetered network).

WorkManager picks among JobScheduler, AlarmManager, and foreground services and respects these constraints automatically; hand-rolled Doze handling is almost never needed. The Background Work primer covers the API contract.

Sources: Optimize for Doze and App Standby · App Standby Buckets · Restricted bucket (Android 12) · Background execution limits (Android 8) · Background work overview · Power management restrictions

See also

  • Concurrency - lifecycle-scoped coroutines (viewModelScope, lifecycleScope, ProcessLifecycleOwner-tied scopes) glue this vocabulary to async work.
  • Background Work - WorkManager is what you call to schedule work that respects Doze and App Standby.
  • State Management - SavedStateHandle is the ViewModel-adjacent store that survives process kill.
  • Memory and GC - lmkd under memory pressure, and onTrimMemory as the heads-up before a kill.
  • App Size and Delivery - Baseline Profiles and install-time AOT; not covered here.

Used in

  • Crash Reporter SDK - the JVM uncaught-exception handler runs synchronously on a dying process; the breadcrumb buffer and report writer must be safe under abrupt termination. Next-launch upload depends on Application.onCreate running before the worker fires.
  • Photo Gallery App - UploadWorker with setForeground() promotes to a foreground service so the process stays at Visible importance and is not killed by lmkd; the upload survives backgrounding because the OS treats it as user-visible work.
  • Messenger App - WebSocket reconnect on app foreground hooks ProcessLifecycleOwner.ON_START, not Activity-level callbacks.
  • Ride-Sharing App - a foreground service of type location keeps GPS active in background since Android 10.
  • Newsfeed App - ContentObserver registration is Application-scoped so it survives backgrounding; same pattern as device-photo detection in Photo Gallery.

Done reading? Mark it so it sticks in your dashboard.

Discussion