Cuando empiezas a desarrollar en Android, una de las cosas que más confusión genera es eso llamado Context. Lo usas para todo: crear vistas, lanzar activities, acceder a recursos… pero muchas veces lo haces casi por inercia, sin tener claro qué es exactamente, qué tipos hay y, sobre todo, cómo puede provocar memory leaks muy puñeteros si lo usas mal.
En este artículo vamos a ver con lupa todo lo relacionado con el Context en Android: qué es, qué tipos existen (Application, Activity, Service, etc.), cómo se relaciona con el ciclo de vida y qué patrones típicos provocan fugas de memoria tanto en vistas clásicas como en Jetpack Compose. La idea es que, cuando termines de leer, sepas elegir el Context adecuado en cada caso y evites esos leaks que tiran tu app abajo sin que sepas muy bien por qué.
¿Qué es realmente el Context en Android?
Según la documentación oficial, Context es una interfaz global de información sobre el entorno de la aplicación. Es una clase abstracta que implementa Android y que te da acceso a recursos, clases propias de la app y operaciones a nivel de sistema: lanzar actividades, enviar y recibir intents, acceder a preferencias, bases de datos, servicios del sistema y un largo etcétera.
Dicho de forma menos formal, el Context es el “entorno” desde el que tu código se está ejecutando. Ese entorno define qué recursos están disponibles, qué ciclo de vida le afecta y qué operaciones puedes realizar. No todos los objetos de tu app tienen contexto; sólo lo tienen aquellos que heredan (directa o indirectamente) de la clase android.content.Context o reciben una instancia de ella.
Si alguna vez has hecho algo del estilo Context contexto = this; dentro de una Activity, lo que estás haciendo es usar la propia Activity como contexto, porque una Activity hereda de Context. Ese this no es “cualquier objeto”, es el objeto que está actuando como entorno actual de ejecución, con acceso a recursos, a su UI y a todo lo que implica su ciclo de vida.
Una metáfora para entender el Context
Para visualizarlo mejor, imagina que el año 1492 es una aplicación Android completa. Dentro de ese año tienes barcos, personas, ciudades, libros… Cada uno de estos elementos vive dentro del contexto histórico de 1492, que incluye costumbres, tecnología disponible, tipo de barcos, etc.
Si le pides al contexto 1492 que te dé un barco, nunca te devolverá un portaaviones moderno: te dará una carabela o una nao. El contexto limita y define lo que existe y lo que puedes pedir. En Android pasa algo muy similar: el Context determina a qué recursos puedes acceder y qué operaciones tienen sentido en ese entorno concreto.
Ahora baja un nivel: cada barco (por ejemplo, La Niña, La Pinta y La Santa María) tendría también su propio contexto particular, con su tripulación, su carga y su día a día. Esos barcos siguen dentro del contexto global de 1492, pero cada uno tiene un entorno más concreto. En Android, eso sería como tener el Context de la aplicación y, dentro de él, el Context de cada Activity.
Cómo se traduce esto en código Android
Supón que tienes una app llamada 1492.apk con varias Activities que representan barcos y cortes reales. Una de ellas es Barco_La_Pinta, cuyo layout es la “pared del camarote del capitán”, donde vas a colgar un cuadro (una imagen drawable de Android). El layout podría tener una ImageView que hace de hueco en la pared para colocar el cuadro.
En el código de la Activity, algo típico sería:
public class Barco_La_Pinta extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.pared_del_camarote_del_barco_la_pinta);
ImageView iv = (ImageView) findViewById(R.id.imageView_Barco_La_Pinta_lugarParaCuadro);
Context contexto = this;
Drawable cuadroElegido = contexto.getResources().getDrawable(R.drawable.cuadro_torre);
iv.setBackground(cuadroElegido);
}
}
En este ejemplo, el Context de la Activity te permite acceder a los recursos (las imágenes de la carpeta res), obtener el drawable concreto y asignarlo a una vista. El contexto es el “1492” del ejemplo: contiene todos los recursos y servicios disponibles para esa Activity.
Qué permite hacer un Context
La gracia del Context es que centraliza un montón de operaciones comunes que, si no, tendrías que programar a mano. Algunos ejemplos típicos donde se pide un Context son:
- Acceso a recursos: imágenes, colores, strings, dimensiones… usando
getResources(),getString(), etc. - Creación de vistas, listeners, adapters: por ejemplo
new TextView(context)o unArrayAdapterque necesita un contexto para inflar layouts. - Lanzar Activities o servicios: creando
Intentcon un Context origen:new Intent(context, OtraActivity.class). - Acceso a preferencias y bases de datos: mediante
getSharedPreferences()oopenOrCreateDatabase(). - Acceso a componentes del sistema: como
getSystemService()para obtener elLayoutInflater, notificaciones, sensores, etc.
En resumen, el Context es la puerta de entrada al estado de la aplicación y a los servicios del sistema. Pero no hay un único Context; hay varios tipos y elegir mal es una fuente clásica de memory leaks.
Tipos de Context en Android
En Android podemos distinguir varios tipos de Context, cada uno con su ámbito y ciclo de vida:
- Application
- Activity
- Service
- BroadcastReceiver
- ContentProvider
Aunque todos se tratan como Context, no exponen exactamente la misma información ni tienen el mismo ciclo de vida, y eso es justo lo que marca la diferencia a la hora de provocar o evitar fugas de memoria.
Application Context
El Application Context es un Singleton que representa el contexto global de la aplicación. Se crea cuando arranca el proceso de la app y vive mientras el proceso esté en memoria. Sólo hay una instancia por aplicación, y la puedes obtener desde casi cualquier sitio mediante getApplicationContext() o getApplication() (si estás en una Activity o clase que hereda de Context).
Este Context es ideal cuando necesitas algo que vaya a vivir más tiempo que una Activity concreta: por ejemplo, un objeto global, un gestor de caché, un componente que se comparte entre varias pantallas o un objeto que sobrevive a cambios de configuración (como rotaciones de pantalla).
Si creas un objeto de larga duración y, por error, le pasas un Activity Context, estás atando ese objeto a la Activity. Cuando la Activity se destruya, el objeto seguirá manteniendo una referencia a ella, impidiendo que el garbage collector la libere, y se quedará consumiendo memoria de forma innecesaria cada vez que vuelvas a abrir esa pantalla.
Activity Context
Una Activity hereda de ContextWrapper, que a su vez hereda de Context. Por tanto, cada Activity es también un Context propio, con su ciclo de vida: se crea, se inicia, se pausa, se detiene y se destruye. Mientras la Activity esté viva, su Context es perfecto para todo lo relacionado con la interfaz de usuario.
Por ejemplo, para mostrar un Toast simple mientras la Activity está en primer plano, lo habitual es usar el contexto de la Activity:
Toast.makeText(this, "Mensaje", Toast.LENGTH_SHORT).show();
O para inflar layouts, obtener referencias a views, acceder a su Intent asociado, etc., también tiene sentido usar ese Activity Context. Lo importante es tener claro que el Activity Context muere cuando la Activity muere. Por eso no debes usarlo para objetos que vayan a vivir más allá del ciclo de vida de la propia Activity.
Service, BroadcastReceiver y ContentProvider
Los Service también heredan de ContextWrapper, y por tanto son Context. Su contexto está ligado al ciclo de vida del servicio. Puedes usarlos para operaciones en segundo plano que necesitan acceso a recursos, preferencias o servicios del sistema, pero sin interfaz gráfica propia.
Los BroadcastReceiver no heredan de Context, pero reciben uno en el método onReceive(Context context, Intent intent) cada vez que entra un broadcast. Ese Context que se pasa es válido sólo durante la ejecución de onReceive. Guardarlo para usarlo más tarde suele ser una mala idea y fuente de fugas si no se maneja con cuidado.
Por su parte, los ContentProvider obtienen un Context mediante getContext(), que devuelve el contexto de la aplicación que los está ejecutando (que puede ser la tuya o la de otra app). En este caso, el Context suele equivaler al Application Context del proceso que aloja el proveedor de contenido.
Cómo obtener un Context según el caso
Dependiendo de dónde estés en el código, hay distintas formas de conseguir un Context. Algunos patrones típicos son:
- Desde una Activity o Service (heredan de Context): puedes usar directamente
thisen la mayoría de los casos.
public class ObtenerContexto extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Context contexto = this;
}
} - Desde una clase interna anónima dentro de una Activity: se suele usar
NombreActivity.thispara referirse al Context de la Activity externa.
boton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Context contexto = ObtenerContexto.this;
}
}); - Desde una View: las vistas tienen su propio método
getContext(), que devuelve el Context donde están alojadas (normalmente el de la Activity).
Button boton = findViewById(R.id.boton);
Context contexto = boton.getContext(); - Application Context desde cualquier clase que herede de Context: puedes usar
getApplicationContext().
Context contexto = getApplicationContext(); - Base Context en clases que heredan de ContextWrapper: se obtiene con
getBaseContext(), que devuelve el Context subyacente envuelto por el wrapper. - Desde un Fragment: normalmente se usa
getActivity()(orequireActivity()), que devuelve la Activity que contiene al Fragment.
public class MiFragment extends Fragment {
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
Context contexto = getActivity();
}
}
La regla general suele ser usar el Context más pequeño y específico que tenga sentido para la operación que necesitas, pero sin olvidar nunca el ciclo de vida asociado para no liarla con leaks.
Handlers, clases internas y fugas de memoria con Context
Uno de los ejemplos más clásicos de fuga de memoria en Android viene de usar Handler y clases internas no estáticas dentro de una Activity. Android Lint suele avisar con el mensaje: “In Android, Handler classes should be static or leaks might occur”.
El problema se entiende mejor si repasas cómo funciona el bucle de mensajes del hilo principal:
- Cuando se inicia la app, el framework crea un Looper para el hilo principal, con su cola de mensajes.
- Al instanciar un Handler en el hilo principal, se asocia con esa cola. Cada mensaje que se envía a la cola mantiene una referencia al Handler para poder invocar
handleMessage()cuando llegue su turno. - En Java, las clases internas no estáticas y las anónimas guardan una referencia implícita a la clase externa. En cambio, las clases internas estáticas no lo hacen.
Combina esto con un Handler definido como clase interna no estática dentro de una Activity, y ya tienes el lío montado. Imagina este escenario: programas un mensaje con 10 minutos de delay usando ese Handler. Si el usuario cierra la Activity antes de que se procese el mensaje, el Looper seguirá manteniendo ese mensaje en la cola, el mensaje mantendrá la referencia al Handler, y el Handler mantendrá una referencia implícita a la Activity.
Mientras el mensaje siga pendiente, la Activity no puede ser recolectada por el garbage collector, aunque en teoría ya haya terminado su ciclo de vida. Eso significa que sus vistas, recursos y todo su grafo de objetos siguen vivos en memoria, sin que el usuario pueda volver a esa pantalla, generando una fuga injustificada.
Lo mismo ocurre con una clase anónima Runnable no estática que uses con postDelayed() o en cualquier operación asíncrona: el Runnable guarda una referencia implícita a la Activity externa, por lo que si se ejecuta mucho tiempo después de que la Activity haya sido destruida, estarás filtrando ese contexto.
Cómo evitar leaks con Handlers y clases internas
La solución recomendada por Google y por la propia advertencia de Lint es clara: define tus Handlers como clases internas estáticas (o como clases top-level en archivos separados), y si necesitas acceder a la Activity, hazlo mediante una WeakReference.
Al ser estática, la clase Handler ya no guarda una referencia implícita a la Activity. En su lugar, tú controlas una referencia débil que puede desaparecer si la Activity se destruye, evitando que el Handler la mantenga viva de forma artificial.
Para el caso de los Runnable anónimos, la idea es parecida: evita que guarden una referencia implícita innecesaria. Puedes usar clases estáticas o diseñar tu código para que no almacene Context de Activity en operaciones de larga duración. Si necesitas un Context que sobreviva a la Activity, usa el Application Context.
Fugas de memoria por variables estáticas y dobles referencias
Otro patrón típico de fuga de memoria ocurre cuando guardas en una variable estática algo que indirectamente mantiene un Context de Activity. Un ejemplo muy ilustrativo es guardar en estático un Drawable que se usa como fondo en una TextView.
Imagina este código simplificado:
public class MiActivity extends Activity {
private static Drawable sFondoDelTexto;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Context contexto = this;
TextView label = new TextView(contexto);
label.setText("Texto con fondo");
if (sFondoDelTexto == null) {
sFondoDelTexto = getDrawable(R.drawable.mi_imagen);
}
label.setBackgroundDrawable(sFondoDelTexto);
setContentView(label);
}
}
Aquí, el Drawable está en una variable estática, así que no se destruye al rotar el dispositivo. Ese Drawable, a su vez, puede mantener referencias internas a la TextView y a su Context, lo que genera una doble referencia circular: el Drawable estático apunta a la vista, la vista al Context de la Activity, y así sucesivamente.
Cuando giras el dispositivo, la Activity antigua debería destruirse y crearse una nueva. Pero como hay una referencia estática que la sigue “enganchando”, el recolector de basura no la puede liberar. Cada rotación va dejando una nueva Activity flotando en memoria, hasta que la app se queda sin memoria y termina con un crash.
La forma correcta de evitar este tipo de fuga es usar el Application Context cuando vayas a manejar recursos almacenados en variables estáticas o en Singletons. Por ejemplo:
Context contexto = getApplicationContext();
TextView label = new TextView(contexto);
De este modo, las referencias estáticas se vinculan al ciclo de vida de la aplicación, no al de una Activity concreta, y no impiden que las actividades se destruyan cuando corresponde.
Singletons y Context: combinación peligrosa
Muy relacionado con lo anterior está el uso de patrones Singleton que almacenan Context. El patrón Singleton, por definición, mantiene una única instancia global en memoria mientras dure el proceso de la app, así que lo que guardes ahí vive tanto como el proceso.
Si tu Singleton guarda un Activity Context, estás provocando que esa Activity jamás se pueda liberar mientras no se mate el proceso completo. Un ejemplo típico sería:
public class MiSingleton {
private static MiSingleton sInstancia;
private Context mContexto;
public static MiSingleton getInstance(Context contexto) {
if (sInstancia == null) {
sInstancia = new MiSingleton(contexto);
}
return sInstancia;
}
private MiSingleton(Context contexto) {
mContexto = contexto;
}
}
Si llamas a getInstance(this) desde una Activity, el Singleton guardará ese this, es decir, el Activity Context. Aunque cierres la Activity, la instancia seguirá en memoria porque el Singleton la referencia.
La solución, en caso de que de verdad necesites almacenar un Context ahí (que muchas veces ni siquiera hace falta), es guardar siempre el Application Context usando contexto.getApplicationContext():
public class MiSingleton {
private static MiSingleton sInstancia;
private Context mContexto;
public static MiSingleton getInstance(Context contexto) {
if (sInstancia == null) {
sInstancia = new MiSingleton(contexto.getApplicationContext());
}
return sInstancia;
}
private MiSingleton(Context contexto) {
mContexto = contexto;
}
}
Aun así, desde un punto de vista de diseño, es recomendable no guardar Contexts en Singletons ni en variables estáticas salvo que tengas clarísimo lo que haces. Siempre que puedas, pasa el Context como parámetro cuando lo necesites y libera referencias cuando dejen de ser necesarias.
Application Context vs Activity Context: cuándo usar cada uno
Recapitulando todo lo anterior, podemos establecer unas pautas claras sobre cuándo usar Application Context y cuándo Activity Context:
- Activity Context
Úsalo para todo lo que esté directamente ligado a la UI de esa pantalla: inflar layouts, crear widgets, mostrar Toasts en la Activity activa, acceder al intent que la lanzó, gestionar la ActionBar, etc. Si el objeto que creas no va a sobrevivir más allá de la Activity, este es el Context adecuado. - Application Context
Úsalo cuando el objeto que creas o el proceso que lanzas va a vivir más tiempo que la Activity actual, o no depende de una pantalla específica: Singletons (cuando son necesarios), componentes compartidos entre varias activities, librerías que mantengan cachés a nivel de app, servicios de análisis, etc.
Un error muy común es usar siempre getApplicationContext() “por si acaso” para evitar leaks. Eso tampoco es buena idea, porque el Application Context no tiene información de UI necesaria para algunas operaciones (como mostrar ciertos diálogos o temas específicos de una Activity). La clave está en usar el Context más apropiado al ciclo de vida del objeto que estás creando.
Y en Jetpack Compose, ¿qué pasa con el Context?
Con Jetpack Compose ya no pasamos el Context por parámetros como se solía hacer en vistas tradicionales, pero sigue existiendo y sigue siendo crítico para muchas operaciones. En Compose se utiliza LocalContext.current para obtener el Context adecuado al punto actual del árbol de composición.
Un ejemplo típico en un composable sería:
@Composable
fun MiPantalla() {
val context = LocalContext.current
Button(onClick = {
Toast.makeText(context, "Mensaje", Toast.LENGTH_SHORT).show()
}) {
Text("Púlsame")
}
}
Aquí, LocalContext.current suele devolver el Activity Context de la Activity que alberga el contenido de Compose, pero el concepto de ciclo de vida sigue siendo el mismo. Lo que hay que vigilar es no capturar ese Context en lambdas o estados de larga duración que sobrevivan a la Activity o al propio composable.
Por ejemplo, si guardas val context = LocalContext.current en un objeto de estado que sigue vivo aunque la Activity cambie de configuración o se destruya, podrías estar filtrando esa Activity. La recomendación es usar siempre el Context solo donde se necesita, y dejar que Compose y el sistema se encarguen del resto.
Buenas prácticas con Context para evitar memory leaks
Resumiendo las ideas clave, estas son algunas buenas prácticas para trabajar con Context sin provocar fugas de memoria:
- Usa el Activity Context para todo lo relacionado con UI (vistas, toasts, inflar layouts, etc.) siempre que el objeto no vaya a sobrevivir a la Activity.
- Usa el Application Context cuando vayas a crear objetos de larga duración, componentes globales o servicios cuyo ciclo de vida no dependa de una Activity concreta.
- No guardes Context en Singletons ni en variables estáticas, salvo que sea el Application Context y tengas muy claro lo que haces. Mejor pasar el Context por parámetros cuando sea necesario.
- Evita clases internas no estáticas y anónimas de larga duración dentro de Activities (Handlers, Runnables, listeners que viven demasiado). Prefiere clases internas estáticas + WeakReference a la Activity.
- En Compose, usa
LocalContext.currentsólo en el ámbito necesario y evita almacenarlo en estados o estructuras que puedan vivir más allá de la pantalla actual.
Cómo detectar fugas de memoria relacionadas con Context
Aunque tengas mucho cuidado, es fácil que se te cuele alguna fuga. Por eso Android Studio ofrece herramientas potentes para perfilado de memoria y análisis de leaks, que te ayudan a encontrar referencias a Context que no deberían seguir vivas.
Una forma muy útil de investigar es capturar un volcado de montón (heap dump) desde el Memory Profiler de Android Studio. Puedes usar la tarea “Analyze Memory Usage (Heap Dump)” para generar un snapshot del estado de memoria de tu app en un momento dado, idealmente después de usarla un buen rato o tras reproducir patrones que sabes que pueden provocar leaks (por ejemplo, girar repetidamente el dispositivo para forzar recreaciones de Activities).
En el heap dump verás una lista de clases con información como:
- Allocations: número de objetos de esa clase.
- Native Size: memoria nativa consumida por esas instancias.
- Shallow Size: memoria Java directa ocupada por esos objetos.
- Retained Size: memoria total que queda retenida por esas instancias (incluyendo los objetos que dominan en el grafo).
Además puedes filtrar el análisis por App heap (montón principal de la app), Image heap (clases precargadas) o zygote heap, y agrupar por clase o paquete para encontrar patrones sospechosos.
Un filtro especialmente interesante es “Show activity/fragment leaks”, que muestra las clases que Android Studio sospecha que están provocando fugas de Activity o Fragment. En apps con Jetpack Compose de actividad única, puedes no tener muchos fragments, pero detectar fugas de Activity es crucial, porque filtrar la Activity de host implica retener toda la jerarquía de Compose y su estado asociado.
Cuando encuentres una Activity o cualquier clase con un retained size muy grande que debería haber sido destruida, puedes inspeccionar las pestañas de Fields y References de cada instancia para ver qué está manteniendo viva esa referencia: un Singleton, un Handler interno no estático, una lambda que capturó el Context, un listener no liberado, etc.
También puedes exportar el .hprof generado e incluso convertirlo con la herramienta hprof-conv que viene en platform-tools del SDK de Android, para analizarlo con otras herramientas Java si lo necesitas.
Forzar y analizar fugas de memoria en pruebas
Para que las fugas salgan a la luz, viene bien “estresar” la app en situaciones donde suelen aparecer problemas con Context. Por ejemplo:
- Rotar el dispositivo varias veces entre vertical y horizontal en distintas pantallas, especialmente aquellas donde uses operaciones asíncronas (Handlers, corrutinas, threads, etc.).
- Ir y venir entre la app y el launcher o entre tu app y otra, para forzar cambios de estado de las Activities.
- Ejecutar sesiones largas de uso y luego capturar un heap dump para ver si hay Activities o Contexts que siguen vivos sin motivo.
Herramientas como LeakCanary (integrable con Android Studio Profiler a partir de ciertas versiones) automatizan gran parte de este análisis, generando informes cuando detectan que una Activity, Fragment o cualquier otro objeto no se ha recolectado cuando debería.
Entender a fondo qué es el Context, qué tipos hay y cómo se relaciona con el ciclo de vida de cada componente es clave para escribir apps Android estables y eficientes en memoria. Usar el Activity Context para la UI, el Application Context para objetos de larga duración, evitar guardar Context en Singletons o variables estáticas, y vigilar clases internas no estáticas y operaciones asíncronas te permitirá esquivar la mayoría de memory leaks típicos; combinado con un buen uso del profiler y herramientas como LeakCanary, tendrás mucho más control sobre cómo tu app maneja la memoria y evitarás que un simple descuido con el Context acabe tirando tu aplicación por los suelos.