Vinculaciones del servicio y estados del proceso

Los procesos de las apps en Android no existen de forma aislada. Las aplicaciones suelen depender de los servicios que proporcionan otras aplicaciones o el propio sistema. Cuando un proceso se conecta a otro a través de una vinculación de servicio, se crea una dependencia que tiene un gran impacto en la forma en que el framework de Android administra la memoria.

Estados del proceso y puntuaciones de OOM

El framework de Android usa estados de proceso para hacer un seguimiento de la importancia de cada proceso en ejecución. Luego, el OomAdjuster usa estos estados para asignar un valor de Ajuste de la puntuación de OOM (oom_score_adj), que va de -1,000 a 1,000.

Un valor de oom_score_adj más bajo significa que el proceso es más importante y es menos probable que el optimizador de poca memoria (LMK) lo cierre.

Estados de proceso comunes

En la siguiente tabla, se muestran algunos de los estados de proceso más comunes y sus valores de oom_score_adj típicos. Para obtener una lista completa y actualizada, consulta android.app.ActivityManager y com.android.server.am.psc.Constants en el código fuente de Android.

Estado del proceso (abreviado) Descripción oom_score_adj habitual
PER (persistente) Procesos del sistema que siempre deben ejecutarse (p.ej., telefonía). -800
TOP Proceso con el que el usuario interactúa actualmente. 0
VIS (Visible) El proceso tiene una actividad visible (p.ej., detrás de un diálogo traslúcido). 100
PERC (Perceptible) Proceso en segundo plano del que el usuario es consciente (p.ej., reproducción de música). 200
FGS Proceso que aloja un servicio en primer plano. De 0 a 200 (varía)
BTOP (Bound Top) Proceso vinculado a una aplicación de TOP. 100
BFGS Servicio en primer plano vinculado (por lo general, vinculado al sistema). 0
PREV (Anterior) Es el último proceso en el que estuvo el usuario antes del actual. 700
CACHED Apps en segundo plano que se pueden cerrar de forma segura. 900 a 999

El impacto de las vinculaciones del servicio

Cuando un proceso cliente (p.ej., una app en el estado TOP) se vincula a un servicio en un proceso servidor, el proceso servidor suele heredar una prioridad elevada. Esto garantiza que el servicio siga disponible mientras el cliente lo necesite.

Diagrama que muestra el proceso A (TOP) que llama a bindService() a través de system_server y eleva el proceso B a BTOP

Cómo controlar la herencia con marcas de BIND

La herencia es el comportamiento predeterminado cuando se usa Context.BIND_AUTO_CREATE. Sin embargo, los desarrolladores pueden controlar cómo la vinculación afecta la importancia del proceso objetivo con varias marcas en bindService().

Marcas de BIND clave para la puntuación de OOM

Las siguientes marcas son más relevantes cuando se administra la presión de la memoria en todo el sistema:

  • BIND_AUTO_CREATE: Es la marca más común. Garantiza que el proceso de servicio se inicie y se mantenga activo mientras exista la vinculación. De forma predeterminada, también eleva la prioridad del proceso del servidor para que coincida con la del cliente.
  • BIND_NOT_FOREGROUND: Evita que el proceso del servicio de destino se eleve a la prioridad de programación en primer plano (prioridad de CPU). Sin embargo, aún permite que se eleve la prioridad de memoria (oom_score_adj). Esto es útil para el trabajo en segundo plano que no debe competir con la IU por los ciclos de CPU, pero que debe protegerse de la finalización.
  • BIND_WAIVE_PRIORITY: Es un parámetro muy potente que le indica al sistema que no afecte la prioridad de programación o administración de memoria del proceso objetivo. El proceso de servicio se administrará como si fuera un proceso en segundo plano normal en la lista de LRU, lo que lo hará apto para la eliminación por falta de memoria, incluso mientras esté vinculado.
  • BIND_ABOVE_CLIENT: Indica que el servicio es más importante que la app cliente en sí. Cuando el sistema necesita recuperar memoria, preferirá finalizar la app cliente antes de finalizar el servicio vinculado. Es "más fuerte" que BIND_AUTO_CREATE, ya que proporciona una capa adicional de protección para el servicio a expensas del cliente.
  • BIND_NOT_PERCEPTIBLE: Reduce la importancia del servicio de destino a un nivel inferior a PERCEPTIBLE, lo que permite que el sistema recupere su memoria para dejar espacio para procesos más críticos que el usuario puede percibir.

Práctica: Observa los efectos de la vinculación

Usaremos la aplicación MemoryLab para demostrar cómo una vinculación desde una app de TOP afecta el estado de un proceso independiente.

1. Cómo iniciar MemoryLab

El siguiente comando inicia la app. Después de que se abra, asegúrate de que permanezca en primer plano (no presiones el botón de inicio ni cambies de app todavía).

adb shell am start -n com.android.memorylab/.MainActivity

2. Identifica procesos

Verifica los estados del proceso antes de la vinculación. MemoryLab ejecuta su IU principal en un proceso y tiene un RemoteService que se ejecuta en un proceso :remote.

adb shell dumpsys activity processes com.android.memorylab

Fragmento de ejemplo de resultado:

  Process OOM control (154 total, non-act at 7, non-svc at 7):
    Proc #0: fg       T/A/TOP  LCMNFUATI  t: 0 13470:com.android.memorylab/u0a417 (top-activity)
        oom: max=1001 curRaw=0 setRaw=0 cur=0 set=0
        state: cur=TOP  set=TOP   lastRss=0.00 lastCachedRss=0.00

Verás el proceso principal com.android.memorylab en el estado TOP. El proceso de :remote aún no comenzó.

3. Vinculación del activador

Envía una transmisión a la app para activar la vinculación del servicio:

adb shell am broadcast -a com.android.memorylab.LEAK_BINDER

4. Observa el estado elevado

Vuelve a verificar los estados del proceso:

adb shell dumpsys activity processes | grep -A 10 "com.android.memorylab"

Fragmento de ejemplo de resultado:

  Proc #  1: vis      F/ /BTOP ---NFUATI  t: 0 13560:com.android.memorylab:remote/u0a417 (service)
    com.android.memorylab/.RemoteService<=Proc{13470:com.android.memorylab/u0a417}
    oom: max=1001 curRaw=100 setRaw=100 cur=100 set=100
    state: cur=BTOP set=BTOP  lastRss=0.00 lastCachedRss=0.00

El proceso :remote ahora se está ejecutando y se encuentra en el estado BTOP (TOP vinculado) con un oom_score_adj de 100. Esto está mucho más protegido que un servicio en segundo plano típico (que estaría en 500 o más). La notación <=Proc{...} muestra qué proceso es responsable de esta elevación de prioridad.

5. Enviar a segundo plano

Presiona el botón INICIO en el dispositivo. Vuelve a verificar los estados:

adb shell dumpsys activity processes | grep -A 10 "com.android.memorylab"

Fragmento de ejemplo de resultado:

    Proc #  2: prev     b/ /LAST --------I  t: 0 13560:com.android.memorylab:remote/u0a417 (service)
        com.android.memorylab/.RemoteService<=Proc{13470:com.android.memorylab/u0a417}
        oom: max=1001 curRaw=700 setRaw=700 cur=700 set=700
        state: cur=LAST set=LAST  lastRss=0.00 lastCachedRss=0.00
    Proc #  1: prev     b/ /LAST --------I  t: 0 13470:com.android.memorylab/u0a417 (previous)
        oom: max=1001 curRaw=700 setRaw=700 cur=700 set=700
        state: cur=LAST set=LAST  lastRss=209MB lastCachedRss=0.00

Ahora, ambos procesos cambiaron a un estado de prioridad más baja (PREV/oom_score_adj 700), ya que el proceso del cliente ya no es TOP. (Nota: LAST en el volcado de estado hace referencia al estado interno LAST_ACTIVITY, que se asigna a PREV en los resúmenes de alto nivel).

Cómo analizar con procstats

La herramienta procstats proporciona una vista histórica de estos estados.

# View stats for MemoryLab over the last hour
adb shell dumpsys procstats --hours 1 com.android.memorylab

Fragmento de ejemplo de resultado:

  *   com.android.memorylab / u0a417 / v37:
      *   Prc com.android.memorylab / u0a417 / v37:
             TOTAL: 0.89% (0.00-0.00-0.00/0.00-0.00-0.00/210MB-210MB-210MB over 1)
               Top: 0.89% (0.00-0.00-0.00/0.00-0.00-0.00/210MB-210MB-210MB over 1)
      *   Prc com.android.memorylab:remote / u0a417 / v37:
             TOTAL: 0.19%
           Bnd Top: 0.19%

Aquí, Bnd Top indica el porcentaje de tiempo que el proceso remoto pasó vinculado a una aplicación en el estado TOP.

Cómo capturar y analizar vinculaciones con Perfetto

Si bien dumpsys te brinda una instantánea, Perfetto te permite ver el momento exacto en que se produce una vinculación y cómo cambia la puntuación de OOM en tiempo real.

1. Cómo registrar un seguimiento

Usa una configuración que incluya linux.process_stats y la categoría am de atrace:

adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/service_bindings.perfetto-trace <<EOF
buffers: { size_kb: 65536 }
data_sources: {
    config {
        name: "linux.process_stats"
        process_stats_config { proc_stats_poll_ms: 100 }
    }
}
data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config { ftrace_events: "am/am_proc_bound" }
    }
}
duration_ms: 15000
EOF

2. Transiciones de la puntuación de OOM de la búsqueda

Con PerfettoSQL, puedes ver cómo cambió la puntuación de OOM del proceso remoto en relación con el proceso de la IU:

SELECT ts, p.name, value AS oom_score_adj
FROM counter c
JOIN process_counter_track t ON c.track_id = t.id
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.memorylab%'
  AND t.name = 'oom_score_adj'
ORDER BY ts;

3. Identifica eventos de vinculación

Para ver exactamente cuándo se estableció una dependencia de vinculación y qué proceso la inició, usa esta consulta:

SELECT
    s.ts,
    p.name AS process_name,
    t.name AS thread_name,
    s.name AS slice_name
FROM slice s
JOIN thread_track tt ON s.track_id = tt.id
JOIN thread t USING (utid)
JOIN process p USING (upid)
WHERE s.name LIKE 'bindService:{com.android.memorylab%';

Vinculaciones de sistema a app

El propio sistema Android suele vincularse a servicios en apps de terceros para proporcionar funcionalidad principal. A menudo, el objetivo de estas vinculaciones es la reducción de la latencia. Al mantener un proceso activo y en la memoria, el sistema evita la costosa sobrecarga de un "inicio en frío" (cargar el APK, inicializar el tiempo de ejecución y crear el objeto Application) cuando se produce una interacción crítica del usuario. Existen otros vínculos para evitar inicios en frío frecuentes en las apps que necesitan controlar flujos de eventos en segundo plano.

Estos son algunos ejemplos del mundo real que puedes observar en un dispositivo típico:

VoiceInteractor

Los usuarios esperan que un asistente digital esté integrado en el SO de su teléfono, que puedan invocarlo instantáneamente con una palabra clave activadora hablada o un gesto de entrada rápido, y que la interacción sea fluida y sin problemas.

Cuando se activa un activador de asistente (por ejemplo, la palabra clave "OK Google" en los teléfonos Google Pixel), el asistente digital debe responder de inmediato. Para garantizar esto, system_server mantiene una vinculación permanente con el servicio de interacción por voz que selecciona el usuario.

Diagrama que muestra la vinculación de system_server al proceso del interactor de la app de Google

Si verificas los estados del proceso (p.ej., con dumpsys activity processes), es posible que veas un proceso como com.google.android.googlequicksearchbox:interactor en el estado BFGS (servicio en primer plano vinculado), que se mantiene activo por una vinculación de system_server (UID 1000).

NotificationListenerService

En el caso de algunas vinculaciones del sistema a la app, el objetivo no es la latencia, sino evitar los inicios en frío frecuentes. NotificationListenerService, un servicio que recibe llamadas del sistema cuando se publican o quitan notificaciones nuevas, es un ejemplo excelente. Un usuario típico de smartphone puede recibir cientos de notificaciones a lo largo del día. Si el sistema se desvincula de un objeto de escucha de notificaciones, es probable que el proceso de esa app pase al estado almacenado en caché y que el LMK lo cierre.

Cuando llegue la próxima notificación, posiblemente segundos después, el sistema se verá obligado a iniciar en frío el proceso de la app de nuevo solo para entregar el evento. Este ciclo constante de cierre e inicio en frío consumiría mucha más CPU y batería que simplemente mantener el proceso vinculado y activo en segundo plano.

La "pantalla -1" del selector (feed de noticias)

Por lo general, las apps de selector modernas combinan la funcionalidad de navegación principal (íconos y widgets de la pantalla principal) con un feed de noticias que está disponible en una de las pantallas del selector y se integra sin problemas con la UX del selector. Es posible que el feed de noticias lo proporcione otra app. Por ejemplo, en Google Pixel, el Selector se integra con un feed que proporciona la app de Google.

Cuando deslizas el dedo hacia la izquierda en la pantalla principal para ver el feed de noticias, la transición debe ser fluida. El selector logra esto vinculándose a una interfaz de servicio en la app que proporciona el feed de noticias y manteniendo esa vinculación activa mientras el selector esté activo. Esto mantiene el contenido del feed renderizado y listo en la memoria, incluso cuando no lo estás mirando.

Otros ejemplos comunes

  • El Selector (HOME_APP_ADJ): La app del selector (Home) tiene su propio espacio especial en la lista de prioridad. Si bien no siempre está vinculado a un servicio, se le asigna el valor HOME_APP_ADJ (por lo general, 600). El sistema prefiere mantener activo el selector, ya que el usuario vuelve a él con frecuencia. De hecho, el sistema preferiría finalizar la app usada anteriormente (PREV_APP_ADJ = 700) que finalizar el selector, ya que esto último generaría una experiencia del usuario lenta al salir de cualquier app, ya que el usuario deberá esperar a que se inicie el selector en frío.
  • Editor de método de entrada (IME): Cuando escribes, el sistema se vincula a la app de teclado que elegiste (p.ej., Gboard). Esto mantiene el proceso del teclado en un estado elevado, incluso si el teclado se oculta temporalmente. Esto garantiza que el teclado pueda volver a aparecer al instante cuando presiones otro campo de texto.
  • Pagos NFC: Cuando acercas el teléfono para pagar, el sistema se vincula al servicio de pago NFC (p.ej., la Billetera de Google). Estas transacciones suelen tener requisitos estrictos en tiempo real desde la terminal del comercio. Si la app de pagos tuvo que iniciar en frío, es posible que se agote el tiempo de espera de la transacción y esta falle.

Compensaciones y el límite de rendimiento

Si bien las vinculaciones son necesarias para el rendimiento y la corrección, afectan el estado de la memoria del sistema.

  • Flexibilidad reducida: Cada proceso vinculado es un proceso que el LMK no puede cerrar fácilmente. Esto reduce el "colchón" de procesos almacenados en caché que el sistema puede usar para liberar memoria bajo presión.
  • Empeoramiento de la caída del rendimiento: Si se vinculan demasiados procesos, es posible que el sistema se quede casi sin procesos en segundo plano que se puedan cerrar. Cuando aumenta la presión de la memoria, el sistema "caerá del precipicio del rendimiento" mucho más rápido, ya que se verá obligado a cerrar procesos más importantes o a saturar la memoria caché de páginas.

Antipatrones comunes de vinculación de servicios

Dado que las vinculaciones de servicios elevan directamente oom_score_adj, los errores sutiles del ciclo de vida en la forma en que adquieres o estructuras las vinculaciones pueden fijar grandes cantidades de memoria en estados privilegiados (BTOP, BFGS o PERC) durante horas. Ten en cuenta estos antipatrones comunes cuando diseñes o audites servicios vinculados.

unbindService() olvidado en clientes de larga duración

La vinculación a un servicio desde un singleton Application, un administrador en segundo plano o un Activity que llama a bindService() en onStart() sin un unbindService() coincidente en onStop() filtra el ServiceConnection.

Mientras esa vinculación permanezca activa, el proceso de destino heredará la prioridad elevada del cliente. Si el cliente es un componente persistente del sistema o una app en primer plano, el proceso del servicio vinculado permanece fijado en BFGS o BTOP de forma indefinida (aparece cerca del 100% en procstats), lo que impide que el LMK reclame su memoria, incluso cuando el servicio está completamente inactivo.

Solución: Vincula los permisos de alcance estrictamente al ciclo de vida del componente que los necesita o implementa un tiempo de espera por inactividad que llame a unbindService() después de un período de inactividad. Evita desvincular y volver a vincular en cada llamada a RPC individual, lo que provoca hiperpaginación del proceso y una sobrecarga repetida de la configuración del binder. En cambio, combina ráfagas de trabajo detrás de un temporizador de inactividad corto (por ejemplo, de 5 a 30 segundos).

Colocación conjunta de un servicio vinculado con una IU que consume mucha memoria

De forma predeterminada, todos los componentes de un APK se ejecutan en el mismo proceso. Si tu app expone un servicio vinculado ligero (como un NotificationListenerService, un proveedor de widgets o un servicio de complemento al que se vincula el sistema o el selector) en el mismo proceso que tu Activity principal, todo el proceso hereda el estado elevado del servicio (BFGS o PERC, por lo general, oom_score_adj de 200 o inferior).

Cuando el usuario abre la IU de tu app, el proceso asigna grandes jerarquías de vistas, mapas de bits decodificados y búferes de gráficos. Cuando el usuario sale, el proceso no se reduce a CACHED (oom_score_adj de 900 o más) porque la vinculación activa del servicio mantiene el proceso elevado. Esto causa dos problemas acumulativos:

  • Sin compactación de memoria en segundo plano: El CachedAppOptimizer del sistema solo compacta los procesos después de que entran en el estado CACHED.
  • No se realiza el recorte de memoria LRU en caché ni la recuperación de LMK: El sistema no entrega devoluciones de llamada de recorte en segundo plano vinculadas a la lista LRU en caché (TRIM_MEMORY_BACKGROUND y superior) mientras una vinculación de servicio mantiene el proceso en un estado elevado. Si tu app no libera explícitamente los recursos de la IU en TRIM_MEMORY_UI_HIDDEN o Activity.onStop(), las asignaciones máximas de la IU permanecen fijas en la RAM con una puntuación de OOM privilegiada en la que el LMK no puede recuperarlas fácilmente.

Solución: Desaloja de forma explícita las cachés de la IU, los mapas de bits decodificados y las referencias de vistas cuando la IU deje de estar visible, con Activity.onStop(), ComponentCallbacks2.onTrimMemory(TRIM_MEMORY_UI_HIDDEN), Application.ActivityLifecycleCallbacks o ProcessLifecycleOwner. Como alternativa, mueve el servicio siempre vinculado a un proceso ligero independiente con el atributo de manifiesto android:process para que el sistema pueda mover tu proceso principal de IU al estado CACHED y compactar o recuperar su memoria de forma independiente.

Omitir marcas de renuncia a la prioridad

Llamar a bindService() con BIND_AUTO_CREATE por sí solo transfiere la prioridad completa de programación y memoria de la persona que llama al servicio de destino. Cuando una app en primer plano se vincula a un servicio de análisis, registro o carga previa en segundo plano usando solo BIND_AUTO_CREATE, promueve de forma involuntaria ese trabajador en segundo plano a BTOP.

Solución: Cuando se vincula a servicios auxiliares o de mejor esfuerzo que no necesitan el mismo nivel de protección que la IU en primer plano, combina BIND_AUTO_CREATE con BIND_WAIVE_PRIORITY, BIND_NOT_FOREGROUND o BIND_NOT_PERCEPTIBLE para que el sistema pueda seguir administrando el proceso de destino en la lista LRU almacenada en caché.


← Localidad | ↑ Arriba | En todo el sistema →