Apuntes DAM
Volver al inicio

El ciclo de vida de una Activity de Android: pila, giros y procesos eliminados

Ejercicio de JavaDifícilUnos 75 minutos

Simula lo que hace Android con las actividades: el orden de onCreate, onStart, onResume, onPause, onStop y onDestroy al abrir pantallas, volver atrás, salir al inicio, girar el móvil o tapar con un diálogo, y cómo el Bundle salva el estado cuando el sistema elimina el proceso.

  • Ciclo de vida de una Activity
  • Pila de actividades (back stack)
  • onSaveInstanceState y Bundle
  • Cambios de configuración
  • Muerte del proceso
  • Máquinas de estados

Enunciado

En Android no eres tú quien decide cuándo se crea o se destruye una pantalla: lo decide el sistema, y avisa a la actividad llamando a sus métodos del ciclo de vida. Entenderlo es la diferencia entre una aplicación que pierde lo escrito al girar el móvil y una que no: al girar, la actividad se destruye y se vuelve a crear, y solo sobrevive lo que se guardó en el Bundle de onSaveInstanceState.

La clase Actividad del código de partida ya tiene sus métodos (escriben su nombre y, onCreate, restaura clics del Bundle) y dos contadores: clics, que se guarda en el Bundle, y temporal, un atributo normal que se pierde con la instancia. Tú escribes Sistema, que hace de ActivityManager: con la pila de actividades decide a qué métodos se llama y en qué orden, como en Android 9 o posterior. Para simplificar, al girar solo se recrea la actividad visible (en un móvil real, las de debajo también se recrean cuando vuelven a verse).

Qué tiene que hacer el programa

  1. abrir X: si hay otra visible, primero onPause() de esa; después onCreate(null), onStart() y onResume() de la nueva, que se apila; y al final onStop() y onSaveInstanceState() de la anterior (guarda el Bundle y queda detenida). Con la aplicación cerrada, solo los tres de la nueva.
  2. atras: onPause() de la visible; si hay otra debajo, se trae al frente (onRestart(), onStart(), onResume(); o, si su instancia no existe, se recrea: onCreate(Bundle), onStart(), onRestoreInstanceState(Bundle), onResume()); y después onStop() y onDestroy() de la que se va. Si era la única, se escribe La aplicación se cierra.
  3. inicio (botón de inicio): onPause(), onStop() y onSaveInstanceState() de la visible, y la aplicación pasa a segundo plano. volver: trae la visible al frente (reiniciándola o recreándola, como arriba). matar (solo en segundo plano): todas las instancias desaparecen sin llamar a nada (El sistema elimina el proceso para liberar memoria (sin llamar a ningún método)), pero los Bundle guardados se conservan.
  4. girar: onPause(), onStop(), onSaveInstanceState() y onDestroy() de la visible y después se recrea. dialogo: onPause() de la visible (queda pausada); cerrar: su onResume(). tocar: suma uno a clics y a temporal de la visible y escribe X: clics = N, temporal = M.
  5. estado: Aplicación: cerrada, o Pila: con las entradas de abajo arriba separadas por → (X [estado, clics=N, temporal=M] o, sin instancia, X [destruida, guardado Bundle{clics=N}]) y Aplicación: en primer plano o en segundo plano, con , proceso eliminado si lo fue y aún no se ha vuelto.
  6. Errores, por este orden: con la aplicación cerrada, La aplicación está cerrada; en segundo plano, La aplicación está en segundo plano: usa volver (también para abrir); con un diálogo abierto, Primero cierra el diálogo (salvo cerrar y estado). Además: La aplicación no está en segundo plano (volver), El sistema solo elimina procesos en segundo plano (matar) y No hay ningún diálogo abierto (cerrar). El main ya escribe cada orden y los errores de formato.

Entrada

Una orden por línea: abrir Nombre (empieza por mayúscula), atras, inicio, volver, matar, girar, dialogo, cerrar, tocar o estado.

Datos de referencia

Estados de una actividad
EstadoSe veTiene el focoSe llega con
reanudadasísíonResume()
pausadasí, en partenoonPause()
detenidanonoonStop()
destruidanonoonDestroy() o la muerte del proceso

Ejemplos de ejecución

Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.

Abrir, tocar, girar y volver atrás

Entrada

abrir Principal
tocar
abrir Detalle
tocar
tocar
girar
estado
atras
atras
estado

Salida por consola

> abrir Principal
Principal.onCreate(null)
Principal.onStart()
Principal.onResume()
> tocar
Principal: clics = 1, temporal = 1
> abrir Detalle
Principal.onPause()
Detalle.onCreate(null)
Detalle.onStart()
Detalle.onResume()
Principal.onStop()
Principal.onSaveInstanceState() → Bundle{clics=1}
> tocar
Detalle: clics = 1, temporal = 1
> tocar
Detalle: clics = 2, temporal = 2
> girar
Detalle.onPause()
Detalle.onStop()
Detalle.onSaveInstanceState() → Bundle{clics=2}
Detalle.onDestroy()
Detalle.onCreate(Bundle{clics=2})
Detalle.onStart()
Detalle.onRestoreInstanceState(Bundle{clics=2})
Detalle.onResume()
> estado
Pila: Principal [detenida, clics=1, temporal=1] → Detalle [reanudada, clics=2, temporal=0]
Aplicación: en primer plano
> atras
Detalle.onPause()
Principal.onRestart()
Principal.onStart()
Principal.onResume()
Detalle.onStop()
Detalle.onDestroy()
> atras
Principal.onPause()
Principal.onStop()
Principal.onDestroy()
La aplicación se cierra
> estado
Aplicación: cerrada

Segundo plano y proceso eliminado

Entrada

volver
abrir Lista
tocar
abrir Ficha
tocar
inicio
tocar
matar
estado
volver
estado
atras

Salida por consola

> volver
La aplicación no está en segundo plano
> abrir Lista
Lista.onCreate(null)
Lista.onStart()
Lista.onResume()
> tocar
Lista: clics = 1, temporal = 1
> abrir Ficha
Lista.onPause()
Ficha.onCreate(null)
Ficha.onStart()
Ficha.onResume()
Lista.onStop()
Lista.onSaveInstanceState() → Bundle{clics=1}
> tocar
Ficha: clics = 1, temporal = 1
> inicio
Ficha.onPause()
Ficha.onStop()
Ficha.onSaveInstanceState() → Bundle{clics=1}
> tocar
La aplicación está en segundo plano: usa volver
> matar
El sistema elimina el proceso para liberar memoria (sin llamar a ningún método)
> estado
Pila: Lista [destruida, guardado Bundle{clics=1}] → Ficha [destruida, guardado Bundle{clics=1}]
Aplicación: en segundo plano, proceso eliminado
> volver
Ficha.onCreate(Bundle{clics=1})
Ficha.onStart()
Ficha.onRestoreInstanceState(Bundle{clics=1})
Ficha.onResume()
> estado
Pila: Lista [destruida, guardado Bundle{clics=1}] → Ficha [reanudada, clics=1, temporal=0]
Aplicación: en primer plano
> atras
Ficha.onPause()
Lista.onCreate(Bundle{clics=1})
Lista.onStart()
Lista.onRestoreInstanceState(Bundle{clics=1})
Lista.onResume()
Ficha.onStop()
Ficha.onDestroy()

Guía paso a paso

Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.

1. La pila

Un ArrayDeque con push, pop y peek es la pila de actividades: la cima es la que se ve. Recórrela de abajo arriba con descendingIterator().

2. Un orden que tiene sentido

Al abrir otra pantalla, la anterior se pausa antes de crear la nueva (para que suelte la cámara o el audio enseguida), pero no se detiene hasta que la nueva ya se ve: así nunca hay un momento sin nada en pantalla.

java
anterior.act.onPause();
nueva.act.onCreate(null);
nueva.act.onStart();
nueva.act.onResume();
anterior.act.onStop();
anterior.guardado = anterior.act.onSaveInstanceState();
3. Reiniciar o recrear

Traer una actividad al frente tiene dos caminos: si su instancia sigue viva, onRestart y onStart; si murió (giro o proceso eliminado), una instancia nueva con onCreate(Bundle) y onRestoreInstanceState(Bundle). Escríbelo una vez en un método y úsalo en atras, volver y girar.

4. Lo que se pierde

Mira temporal después de un giro o de matar el proceso: vuelve a 0 porque es un atributo de la instancia. clics sobrevive porque viaja en el Bundle.

Resuélvelo aquí

El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 2 casos ocultos que buscan los errores típicos.

☕JavaEl ciclo de vida de una Activity de Android: pila, giros y procesos eliminadosDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
abrir Principal
tocar
abrir Detalle
tocar
tocar
girar
estado
atras
atras
estado
Salida esperada
> abrir Principal
Principal.onCreate(null)
Principal.onStart()
Principal.onResume()
> tocar
Principal: clics = 1, temporal = 1
> abrir Detalle
Principal.onPause()
Detalle.onCreate(null)
Detalle.onStart()
Detalle.onResume()
Principal.onStop()
Principal.onSaveInstanceState() → Bundle{clics=1}
> tocar
Detalle: clics = 1, temporal = 1
> tocar
Detalle: clics = 2, temporal = 2
> girar
Detalle.onPause()
Detalle.onStop()
Detalle.onSaveInstanceState() → Bundle{clics=2}
Detalle.onDestroy()
Detalle.onCreate(Bundle{clics=2})
Detalle.onStart()
Detalle.onRestoreInstanceState(Bundle{clics=2})
Detalle.onResume()
> estado
Pila: Principal [detenida, clics=1, temporal=1] → Detalle [reanudada, clics=2, temporal=0]
Aplicación: en primer plano
> atras
Detalle.onPause()
Principal.onRestart()
Principal.onStart()
Principal.onResume()
Detalle.onStop()
Detalle.onDestroy()
> atras
Principal.onPause()
Principal.onStop()
Principal.onDestroy()
La aplicación se cierra
> estado
Aplicación: cerrada
⏳
Test oculto #3
⏳
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada

Solución explicada

Ver la solución completa
java
1import java.util.*;
2
3/** Una actividad con los métodos de su ciclo de vida, como android.app.Activity. */
4class Actividad {
5    final String nombre;
6    int clics;          // se guarda en el Bundle con onSaveInstanceState
7    int temporal;       // un atributo normal: se pierde con la instancia
8
9    Actividad(String nombre) { this.nombre = nombre; }
10
11    private void decir(String metodo) { System.out.println(nombre + "." + metodo); }
12
13    static String bundle(Map<String, Integer> b) { return b == null ? "null" : "Bundle{clics=" + b.get("clics") + "}"; }
14
15    void onCreate(Map<String, Integer> estado) {
16        if (estado != null) clics = estado.get("clics");
17        decir("onCreate(" + bundle(estado) + ")");
18    }
19    void onStart() { decir("onStart()"); }
20    void onRestart() { decir("onRestart()"); }
21    void onResume() { decir("onResume()"); }
22    void onPause() { decir("onPause()"); }
23    void onStop() { decir("onStop()"); }
24    void onDestroy() { decir("onDestroy()"); }
25
26    Map<String, Integer> onSaveInstanceState() {
27        Map<String, Integer> b = new LinkedHashMap<>();
28        b.put("clics", clics);
29        decir("onSaveInstanceState() → " + bundle(b));
30        return b;
31    }
32
33    void onRestoreInstanceState(Map<String, Integer> b) { decir("onRestoreInstanceState(" + bundle(b) + ")"); }
34}
35
36/** Una entrada de la pila de actividades: la instancia (null si el proceso murió) y su estado guardado. */
37class Entrada {
38    final String nombre;
39    Actividad act;
40    Map<String, Integer> guardado;
41    String estado;
42
43    Entrada(String nombre) { this.nombre = nombre; }
44}
45
46/** Hace de ActivityManager: decide a qué métodos se llama y en qué orden. */
47class Sistema {
48    private final Deque<Entrada> pila = new ArrayDeque<>();      // la cima es la actividad visible
49    private boolean segundoPlano = false, dialogo = false, procesoEliminado = false;
50
51    private Entrada cima() { return pila.peek(); }
52
53    /** Comprueba que la aplicación está en primer plano y sin diálogo; si no, explica por qué. */
54    private boolean enPrimerPlano() {
55        if (pila.isEmpty()) System.out.println("La aplicación está cerrada");
56        else if (segundoPlano) System.out.println("La aplicación está en segundo plano: usa volver");
57        else if (dialogo) System.out.println("Primero cierra el diálogo");
58        else return true;
59        return false;
60    }
61
62    /** Lleva una entrada al primer plano: la recrea si su instancia ya no existe o la reinicia si estaba detenida. */
63    private void traerAlFrente(Entrada e) {
64        if (e.act == null) {
65            e.act = new Actividad(e.nombre);
66            e.act.onCreate(e.guardado);
67            e.act.onStart();
68            e.act.onRestoreInstanceState(e.guardado);
69        } else {
70            e.act.onRestart();
71            e.act.onStart();
72        }
73        e.act.onResume();
74        e.estado = "reanudada";
75    }
76
77    private void detener(Entrada e) {
78        e.act.onStop();
79        e.guardado = e.act.onSaveInstanceState();
80        e.estado = "detenida";
81    }
82
83    void abrir(String nombre) {
84        if (segundoPlano) {
85            System.out.println("La aplicación está en segundo plano: usa volver");
86            return;
87        }
88        if (dialogo) {
89            System.out.println("Primero cierra el diálogo");
90            return;
91        }
92        Entrada anterior = cima();
93        if (anterior != null) {
94            anterior.act.onPause();
95            anterior.estado = "pausada";
96        }
97        Entrada nueva = new Entrada(nombre);
98        nueva.act = new Actividad(nombre);
99        nueva.act.onCreate(null);
100        nueva.act.onStart();
101        nueva.act.onResume();
102        nueva.estado = "reanudada";
103        pila.push(nueva);
104        if (anterior != null) detener(anterior);     // la anterior se detiene cuando la nueva ya se ve
105    }
106
107    void atras() {
108        if (!enPrimerPlano()) return;
109        Entrada actual = pila.pop();
110        actual.act.onPause();
111        if (!pila.isEmpty()) traerAlFrente(cima());
112        actual.act.onStop();
113        actual.act.onDestroy();
114        if (pila.isEmpty()) {
115            System.out.println("La aplicación se cierra");
116            procesoEliminado = false;
117        }
118    }
119
120    void inicio() {
121        if (!enPrimerPlano()) return;
122        cima().act.onPause();
123        detener(cima());
124        segundoPlano = true;
125    }
126
127    void volver() {
128        if (pila.isEmpty() || !segundoPlano) {
129            System.out.println("La aplicación no está en segundo plano");
130            return;
131        }
132        segundoPlano = false;
133        procesoEliminado = false;
134        traerAlFrente(cima());
135    }
136
137    void matar() {
138        if (pila.isEmpty() || !segundoPlano) {
139            System.out.println("El sistema solo elimina procesos en segundo plano");
140            return;
141        }
142        // el proceso desaparece de golpe: no se llama a ningún método, pero los Bundle guardados sobreviven
143        for (Entrada e : pila) {
144            e.act = null;
145            e.estado = "destruida";
146        }
147        procesoEliminado = true;
148        System.out.println("El sistema elimina el proceso para liberar memoria (sin llamar a ningún método)");
149    }
150
151    void girar() {
152        if (!enPrimerPlano()) return;
153        Entrada e = cima();
154        e.act.onPause();
155        detener(e);
156        e.act.onDestroy();
157        e.act = null;
158        traerAlFrente(e);
159    }
160
161    void dialogo() {
162        if (!enPrimerPlano()) return;
163        cima().act.onPause();
164        cima().estado = "pausada";
165        dialogo = true;
166    }
167
168    void cerrar() {
169        if (!dialogo) {
170            System.out.println("No hay ningún diálogo abierto");
171            return;
172        }
173        dialogo = false;
174        cima().act.onResume();
175        cima().estado = "reanudada";
176    }
177
178    void tocar() {
179        if (!enPrimerPlano()) return;
180        Actividad a = cima().act;
181        a.clics++;
182        a.temporal++;
183        System.out.println(a.nombre + ": clics = " + a.clics + ", temporal = " + a.temporal);
184    }
185
186    void estado() {
187        if (pila.isEmpty()) {
188            System.out.println("Aplicación: cerrada");
189            return;
190        }
191        List<String> partes = new ArrayList<>();
192        Iterator<Entrada> it = pila.descendingIterator();          // de abajo arriba
193        while (it.hasNext()) {
194            Entrada e = it.next();
195            partes.add(e.act == null ? e.nombre + " [destruida, guardado " + Actividad.bundle(e.guardado) + "]"
196                    : e.nombre + " [" + e.estado + ", clics=" + e.act.clics + ", temporal=" + e.act.temporal + "]");
197        }
198        System.out.println("Pila: " + String.join(" → ", partes));
199        System.out.println("Aplicación: " + (segundoPlano ? "en segundo plano" : "en primer plano") + (procesoEliminado ? ", proceso eliminado" : ""));
200    }
201}
202
203public class Main {
204    public static void main(String[] args) {
205        Scanner sc = new Scanner(System.in);
206        Sistema s = new Sistema();
207        while (sc.hasNextLine()) {
208            String linea = sc.nextLine().trim();
209            if (linea.isEmpty()) continue;
210            System.out.println("> " + linea);
211            String[] p = linea.split("\\s+");
212            if (p[0].equals("abrir") && p.length == 2 && p[1].matches("[A-Z]\\w*")) {
213                s.abrir(p[1]);
214                continue;
215            }
216            if (p.length != 1) {
217                System.out.println("Orden no válida: " + linea);
218                continue;
219            }
220            switch (p[0]) {
221                case "atras" -> s.atras();
222                case "inicio" -> s.inicio();
223                case "volver" -> s.volver();
224                case "matar" -> s.matar();
225                case "girar" -> s.girar();
226                case "dialogo" -> s.dialogo();
227                case "cerrar" -> s.cerrar();
228                case "tocar" -> s.tocar();
229                case "estado" -> s.estado();
230                default -> System.out.println("Orden desconocida: " + p[0]);
231            }
232        }
233    }
234}

El ciclo de vida existe porque el móvil tiene poca memoria y una sola pantalla: Android pausa lo que se tapa, detiene lo que no se ve y, si necesita memoria, elimina procesos enteros sin avisar. La aplicación tiene que estar preparada para volver como si nada.

Guardar el estado en onSaveInstanceState es la red de seguridad para los giros y la muerte del proceso. Hoy se usa además un ViewModel (que sobrevive a los giros, pero no a la muerte del proceso) con SavedStateHandle, que sí sobrevive.

onPause debe ser rápido (la siguiente pantalla espera a que acabe) y onStop es el sitio para liberar lo pesado o guardar datos. Lo que se crea en onStart se libera en onStop, y lo de onResume, en onPause.

Un diálogo de la propia aplicación (un AlertDialog) no pausa la actividad: lo que la pausa es otra actividad que la tapa en parte, como un diálogo de permisos o una actividad con tema de diálogo.

Para ir más allá

  • Añade un ViewModel que sobreviva a los giros pero no a la muerte del proceso y muestra la diferencia.
  • Simula los modos de lanzamiento singleTop y singleTask en abrir.
  • Recrea también las actividades de debajo cuando vuelven a verse después de un giro, como en un móvil real.

Dónde se explica