Mehdi Tmimi Mobile

Mobile Development Capsule 02 · Android

How does Android run an app?

In Node, your program runs from line 1 until it ends. On Android, nobody runs your file. The system calls into your app, and ends it when it decides.

Every app is its own process, under its own Linux user. The system starts each one, and ends any of them.

The short answerAndroid runs your app on its own terms: no main() of yours. It installs the package under a Linux user of its own. It starts a process when you tap, drives each screen, and kills the process when memory runs short.

Nobody calls your main()

Every app carries a manifest, the list of its parts. Android reads it, and calls each part when it needs it. What is not declared does not exist.

AndroidManifest.xml as merged into the APK

<manifest >
  <uses-sdk />
  <uses-permission />
  <application>
    <activity  android:exported="true">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category />
      </intent-filter>
    </activity>
  </application>
</manifest>

The manifest

Press a highlight, or a note.

activity · LAUNCHER

Your entry point: tapping the icon opens this screen. No main() of yours runs.

package

The app's identity, on the phone and in the store; in your project, the applicationId in app/build.gradle.kts. Two apps cannot share it.

targetSdkVersion

The Android version the app targets; in your project, targetSdk in app/build.gradle.kts. It decides which of the platform's newer rules apply.

uses-permission

What the app may ask for. What is not declared cannot be asked for later.

Read a manifest, and you know what an app can do before reading its code.

Go deeper Components, and the manifest in the APK 1 min

The parts are called components, and there are four kinds. Activities are screens. Services are work with no screen. Broadcast receivers react to system events, and can also be registered from code while the app runs. Content providers offer data to other apps.

The manifest in your project is not the one in the APK. The build merges it with your libraries' manifests, and writes package from the applicationId in app/build.gradle.kts. Android Studio's APK Analyzer shows the merged one.

The targeted version decides which of the platform's newer behaviour changes apply to the app; a few apply to every app. Google Play requires a recent one: API 36 for new apps and updates since 31 August 2026.

Strictly, every app process does start in a main(): the framework's ActivityThread.main(), which loops on the main thread and calls your code. None of it is yours.

Installing creates a user for your app

An app travels as an APK, a zip with a fixed layout. At install, Android creates a Linux user for it, and a folder only that user can read: the sandbox.

  • Notes.apk holds AndroidManifest.xml, classes.dex, res/, assets/ and lib/.
  • At install, Notes gets the Linux user u0_a231 and the folder /data/data/com.example.notes/.
  • Notes reads the bank's folder, owned by u0_a198: Permission denied.

No app reads another's files. Apps talk through the system, or not at all.

Go deeper The numbers, the bundle, SELinux 1 min

u0_a160 reads: user 0 of the phone, app 160, Linux user ID 10160. The package manager records it at install: for a fresh Expo app, dumpsys package showed appId=10217, and ps showed u0_a217.

On Google Play you upload an app bundle, an AAB. From it, Play builds APKs for each phone: a base APK and configuration APKs that hold only what that phone needs. The phone still installs APKs.

The sandbox is the Linux kernel's ordinary user and file permissions. Since Android 5, SELinux rules that the app cannot change reinforce it.

A fresh Expo app's APK holds 1,146 files: two .dex files, lib/ for four processor types, and assets/index.android.bundle, your JavaScript compiled to Hermes bytecode. Rename any .apk to .zip and open it.

Checkpoint

Your notes app stores a draft in its private folder. Another app wants to read it. What happens?

See the answer

The folder belongs to one Linux user, and only that user and the system can open it. A storage permission covers shared storage, such as photos and downloads, never another app's private folder. Sharing goes through the system: a copy, or a temporary grant to one file.

The system kills without warning

When the user leaves, the process stays cached, so coming back is instant. When memory runs short, Android kills cached processes first. The app is not asked, and is not told.

Foreground → visible → service → cached. The lowest rank is killed first, and no callback runs in a killed process.

Next time, a new process starts. It gets back only what the app saved.

Go deeper zygote, the freezer, the ranking 1 min

Every app process is forked from zygote, a process started at boot with the framework already loaded. That is the picture at the top of this page; step 1 of Try it shows it on your own phone.

Since Android 14, on phones that support it, a cached process is also frozen about ten seconds after it becomes cached: it keeps its memory but gets no processor time.

The system prints its own ranking with adb shell dumpsys activity lru. Here, the launcher is in front, Chrome was just left, and a demo app waits in the cache:

You should see  #59: fg     TOP  LCMNFUAT 1097:com.google.android.apps.nexuslauncher/u0a184 act:activities|recents
  #58: prev   LAST -------- 3167:com.android.chrome/u0a160 act:activities|recents
  #56: cch    CAC  -------- 6607:com.tmimiexplains.flutterdemo/u0a218 act:activities|recents

Rotating the phone destroys the screen

The screen you see is an activity. The system creates it, shows it and takes it away, calling your code at each step.

  1. onCreatecreated; set up the screen
  2. onStartbecoming visible
  3. onResumein the foreground, interactive
  4. onPausepartly hidden, by another activity on top; keep it brief
  5. onStopno longer visible: save here
  6. onDestroyfinished, or rotated; skipped when the process is killed

Leave and come back: down to onStop, back up through onStart. Rotate: all the way down, then a new activity starts at onCreate. Killed: the chain is cut, and onDestroy never runs.

Save what matters by the time the activity stops. Rebuild from it in onCreate.

Go deeper onPause, saved state, and the frameworks 1 min

onPause must stay brief. It runs when another activity covers yours, even a translucent one such as the permission dialog. A dialog your own app draws does not pause it. Since Android 10, apps side by side in split screen all stay resumed.

Save by onStop at the latest, and use onSaveInstanceState for the screen's small state; in an app that targets Android 9 or later, it runs after onStop. A ViewModel survives a rotation, but not the death of the process.

Expo, Flutter and Ionic templates declare configChanges: their activity handles a rotation itself and is not recreated. The Expo template also locks the app in portrait. For them, the kill is the test that matters.

Jetpack Compose builds screens differently, but it stands on the same activity and the same lifecycle. Since Android 12, Back on an app's first screen moves it to the background instead of destroying it.

Three more rules, three later capsules

  • 08

    Background

    An app the user left cannot keep ordinary work running. What must continue is declared.

  • 09

    Permissions

    Declared, then asked at runtime. The user can say no, and the app must still work.

  • 10

    Signature

    Every APK is signed, with no authority involved. An update must carry the same key.

Keep the last one for the next capsule: on iOS, Apple must issue the certificate.

Go deeper The three rules, in detail 2 min

The background. What must continue after the user leaves, a download, a recording, a trip being tracked, is a foreground service with a notification the user can see, or a job scheduled through WorkManager that the system runs when it allows. "It worked while I was testing" is the usual symptom: the test keeps the app in front, the user does not.

Permissions. Declared in the manifest; the sensitive ones, such as camera, microphone, location and contacts, are asked at runtime through a dialog the system shows. The user can refuse, or revoke them later in Settings, and Android resets them by itself for an app left unused for a few months.

The signature. The certificate is self-signed, from a key the developer generates; Android Studio even makes a debug key for you. It says who the app comes from, not that it is safe. On Google Play, Google keeps the app's signing key and signs the APKs it delivers: that is Play App Signing. You sign what you upload with a separate upload key. A new key can replace the old one only if the old key vouches for it: key rotation, since Android 9.

A phone also installs APKs from outside the store when the user allows it. Google is tying installs on certified devices to developers it has verified: since 30 September 2026 for apps installed from Google Play and the main phone makers' stores in Brazil, Indonesia, Singapore and Thailand, and for all apps, everywhere, from 2027.

A free limited distribution account lets a student share an app with up to 20 devices without an ID check. An advanced flow still lets a determined user install an unverified app, and installing from your own computer with adb does not change.

With knowledge · in class

Ask for an app that survives the system

An assistant gives you an app that runs on the first try, in front of you, phone upright. The kill, the rotation and the refused permission happen later, in a user's hands.

Paste first, whatever the family

This app runs on Android: the system may pause it, rotate it or kill it at any time. Save state so that a new process can rebuild the screen, and tell me where each piece of state is saved.

All families

Where the app's contract lives

  • Native: AndroidManifest.xml and app/build.gradle.kts.
  • Expo: app.json, until a build writes the manifest for you. In Expo Go, the package name and the process are Expo Go's.
  • Ionic: android/app/src/main/AndroidManifest.xml, after npx cap add android.
  • PWA: none of this. Its web manifest names the app for the browser, not for Android.

Choose your family above: its prompt and its checks.

Native

Say it in the prompt

  • State: the screen must survive rotation and process death: persist the draft as the user types and restore it on launch Expect a ViewModel with SavedStateHandle for the screen, and DataStore or Room for the draft itself.
  • Background: the upload must continue if the user leaves the app: use WorkManager, not a thread in the activity

Check the project

"Don't keep activities" destroys each activity as soon as you leave it, but keeps the process, so static fields survive. For the real thing, kill the process: step 3 of Try it.

Expo

Say it in the prompt

  • State: the draft must survive the app being killed in the background: save it with AsyncStorage as the user types, and restore it on launch AsyncStorage runs in Expo Go; MMKV needs a development build.
  • Background: the upload must continue if the user leaves the app; tell me which system mechanism you used, and why it survives the app leaving the screen

Check the project

Ionic

Say it in the prompt

  • State: save the draft with Capacitor Preferences as the user types, not in localStorage, and restore it on launch The OS can reclaim a WebView's localStorage when the phone runs low on space.
  • Background: the upload must continue if the user leaves the app; tell me which system mechanism you used, and why it survives the app leaving the screen

Check the project

PWA

Say it in the prompt

  • State: save the draft in IndexedDB as the user types, and restore it when the page loads
  • Background: the upload must continue if the user leaves the app; tell me which system mechanism you used, and why it survives the app leaving the screen

Check the project

Try it on your phone · 10 minutes

See the sandbox and the kill on a real phone

Before class Set up adb once 5 min

Turn on Developer options, then USB debugging. Install the Android SDK Platform-Tools: adb devices must list your phone. Since Android 11, Wireless debugging works without a cable.

Copy the commands as they are. The quotes make the filter run on the phone, so the same lines work from Windows, macOS and Linux.

No cable? Settings → Apps → any app → Permissions shows what it asked for and what you granted.

  1. Who runs Chrome. Open Chrome, press Home, then list its processes.

    adb shell "ps -A -o USER,PID,PPID,NAME | grep -e USER -e zygote64 -e com.android.chrome"
    You should seeUSER           PID  PPID NAME
    root           489     1 zygote64
    u0_a160       7531   489 com.android.chrome_zygote
    u0_a160       7975   489 com.android.chrome
    u0_i17        8017  7531 com.android.chrome:sandboxed_process0:…
    u0_a160       8043   489 com.android.chrome:privileged_process0

    This proves Chrome runs as its own Linux user, and zygote, PID 489, forked it.

  2. Its private folder. Ask the shell to list it.

    adb shell ls /data/data/com.android.chrome
    You should seels: /data/data/com.android.chrome: Permission denied

    This proves The shell's user is not Chrome's user, and the kernel refuses.

  3. Leave, kill, come back. Three moves, a command after each.

    1. Open Chrome, then clear the log.

      adb logcat -b events -c
    2. Press Home, then kill Chrome.

      adb shell am kill com.android.chrome
    3. Reopen Chrome, then read the log.

      adb logcat -b events -d -v brief -s wm_on_paused_called wm_on_stop_called wm_on_destroy_called am_kill am_proc_start wm_on_create_called
    You should seeI/wm_on_paused_called( 7975): [221999946,…ChromeTabbedActivity,performPause,8]
    I/wm_on_stop_called( 7975): [221999946,…ChromeTabbedActivity,STOP_ACTIVITY_ITEM,23]
    I/am_kill (  697): [0,7975,com.android.chrome,700,kill background,237640]
    I/am_proc_start(  697): [0,8558,10160,com.android.chrome,next-top-activity,{…}]
    I/wm_on_create_called( 8558): [221999946,…ChromeTabbedActivity,performCreate,228]

    This proves Home ran onPause and onStop. The kill ran nothing, not even onDestroy.

  4. Rotate. Turn on auto-rotate and open the Clock app.

    1. Clear the log.

      adb logcat -b events -c
    2. Turn the phone once, then read the log.

      adb logcat -b events -d -v brief -s wm_on_paused_called wm_on_stop_called wm_on_destroy_called wm_on_create_called
    You should seeI/wm_on_paused_called( 8215): [206975589,com.android.deskclock.DeskClock,performPause,0]
    I/wm_on_stop_called( 8215): [206975589,com.android.deskclock.DeskClock,handleRelaunchActivity,1]
    I/wm_on_destroy_called( 8215): [206975589,com.android.deskclock.DeskClock,performDestroy,6]
    I/wm_on_create_called( 8215): [206975589,com.android.deskclock.DeskClock,performCreate,85]

    This proves A rotation destroys the activity and creates a new one, in the same process.

    If Clock prints nothing or does not turn, run a new Android Studio project instead: by default, its activity is recreated.

Go deeper Notes on the four steps 1 min

Step 1. The u0_i lines are isolated helpers: users with no permissions at all. Chrome's PID, here 7975, comes back in step 3.

Step 2. On your own debug build, adb shell run-as followed by your applicationId and ls -la works: it becomes the app's user. A debug demo app's folder:

You should seedrwx------   7 u0_a216 u0_a216        4096 2026-10-13 19:53 .

Step 3. Reopening started process 8558 at onCreate, and Chrome rebuilt its tab from what it had saved. For your own app, kill its applicationId.

Step 4. Try Chrome: nothing appears. It handles rotation itself, as the Expo and Ionic templates do.

Real outputs, captured on 14 October 2026 on an Android 16 emulator: Pixel 7 profile, API 36, Google APIs image, no root. Logs trimmed to the app's own lines; … marks a shortened name. Your PIDs and user numbers will differ; the order will not.

Quick check

From capsule 01

Layout bounds are on. Your Expo app shows a box around every button. Who draws those buttons?

See the answer

Each outlined button is a native view, which Android draws because React Native asked for it. Hermes runs the JavaScript but draws nothing, and a WebView would show one box.

The user taps your app's icon. What tells Android which screen to open?

See the answer

Android reads the manifest and starts the activity declared with the LAUNCHER category: no main() of yours runs. Your source folders mean nothing to the phone; what is not declared does not exist.

An assistant's code asks for the camera at runtime, but AndroidManifest.xml declares no CAMERA permission. What happens?

See the answer

A permission must be declared in the manifest before it can be asked for at runtime; without it, no dialog appears and the request comes back denied. Nothing adds it at install: the merged manifest is fixed when the APK is built.

With a cable, you run adb shell ls /data/data/com.android.chrome. What do you see?

See the answer

The folder belongs to Chrome's Linux user and the shell runs as another one, so the kernel refuses. Owning the phone does not open the sandbox; run-as does it only for your own debug build.

The user rotates the phone while a form is half filled. By default, what does Android do with the activity?

See the answer

A rotation is a configuration change: by default, Android destroys the activity and creates a new one, in the same process. Saved instance state and a ViewModel bring the half-filled form back.

A React Native app has been in the background for an hour. The user comes back and it starts from its first screen. What happened?

See the answer

A cached process is killed when memory is needed, and no code of the app runs, not even onDestroy. Nothing crashed: the screens lived only in the JavaScript's memory, so the app starts over.

Which line should you give the assistant so that the draft survives what Android does to the app?

See the answer

Android may kill the process with no callback, so the draft must already be saved when it happens. onDestroy is skipped when the process is killed, and an app the user left cannot simply keep running.

An assistant uploads a file from a thread started in the activity. In every one of your tests, it finishes. Why can it fail for a user?

See the answer

Your tests keep the app in front, the user does not: once the app is cached, the system can kill the process, thread included. What must continue is declared, a foreground service or WorkManager: capsule 08.

Your PWA saves a draft. On Android, which process do you kill to test that the draft survives?

See the answer

An installed PWA still runs inside Chrome, with Chrome's process, user and sandbox: kill com.android.chrome, then reopen. Its web manifest names the app for the browser, not a process for Android.

A classmate installed your lab APK from a file. You lost your key, and sign the next version with a new one. What happens when they install it?

See the answer

An update must carry the same key, and no authority can vouch for a new one: your classmate must uninstall first, data included. Key rotation, since Android 9, works only if the old key vouches for the new one.

Recap

  1. No main() of yours. The system calls the components the manifest declares.
  2. A user per app. Installed under its own Linux user: that is the sandbox.
  3. Killed without a word. Cached processes go first, and no callback runs.
  4. Rotation destroys the screen. Save by onStop at the latest; restore in onCreate.
  5. Still the system's call. Background work, permissions and the signature: capsules 08 to 10.

Sources

← All capsules