Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Tema 11
Procesos e Hilos
TEMA 11. PROCESOS E HILOS
Introducción
Como ya se ha comentado en temas anteriores, por defecto cada aplicación Android ejecuta
todos sus componentes en un único proceso propio. Por otro lado, para evitar que la interfaz
de usuario se ralentice o incluso se bloquee, es necesario realizar las tareas pesadas en nuevos
hilos o threads.
Cuando se inicia el primer componente de una aplicación (en general una actividad), el sistema
inicia un nuevo proceso Linux con un único hilo de ejecución. A no ser que se especifique lo
contrario, el resto de componentes que se inicien a continuación también lo harán sobre el
mismo proceso y sobre dicho hilo, denominado main thread, por ser el hilo principal de la
aplicación. No obstante, existe la posibilidad de gestionar los componentes de la aplicación
para que se ejecuten en procesos separados, y además se pueden crear múltiples hilos en cada
proceso.
Procesos
La mayoría de las aplicaciones utilizarán un único proceso para todos sus componentes. En
caso de querer separar ciertos componentes de la aplicación en diferentes procesos, se tendrá
que editar el manifiesto (AndroidManifest.xml) de la aplicación.
Todos los elementos que declaran los diferentes componentes de una aplicación en el
manifiesto (<activity>, <service>, <receiver>, <provider>) soportan el atributo
android:process, que permite especificar el proceso en el que el componente será
ejecutado, de forma que se podrá compartir el mismo proceso entre distintos componentes.
Además, se podrá compartir el mismo proceso entre componentes de diferentes aplicaciones
(en caso de que compartan el mismo ID de usuario Linux y estén firmadas con los mismos
certificados). Para asignar el mismo proceso a todos los componentes de una aplicación, se
podrá declarar el atributo android:process en el elemento <application>.
Debido a que Android gestiona automáticamente el ciclo de vida de los procesos, en casos de
que la memoria disponible sea escasa y otro proceso con mayor prioridad deba realizar tareas,
el sistema podrá finalizar procesos menos prioritarios, destruyendo por lo tanto los
componentes de la aplicación que estuvieran ejecutándose en dichos procesos. La decisión de
qué proceso se finalizará se realiza en función de la importancia relativa de dicho proceso para
el usuario, la cual se determina en función del estado de los componentes que estén activos en
cada proceso.
El sistema Android intentará mantener el proceso de cada aplicación tanto tiempo como sea
posible, hasta que llegue el momento de eliminar procesos antiguos para liberar memoria que
necesitan nuevos procesos o procesos que ya se están ejecutando y que son más importantes.
Para decidir qué proceso ha de mantenerse y cuál puede ser finalizado, el sistema ordena los
mismos en una jerarquía de importancia, que se basa en los componentes que se están
ejecutando en dicho proceso, así como en el estado de los mismos. Los procesos que tengan
una importancia menor serán los primeros que se finalicen, y se seguirá eliminando procesos
en la jerarquía (de menor a mayor importancia) hasta que se satisfagan las necesidades de
recursos del sistema exigidas en cada momento.
1. Procesos en primer plano (más importantes). Son aquellos procesos requeridos para
completar las operaciones que realiza el usuario. Estos procesos se identifican porque:
• albergan una actividad con la cual el usuario está interactuando, por lo que se
ha invocado su método onResume(), o
• albergan un servicio unido a una actividad con la cual está interactuando el
usuario, o
• albergan un servicio que se está ejecutando en primer plano, al haber
invocado a su método starForeground(), o
• albergan un servicio que está ejecutando uno de sus métodos callback
(onCreate(), onStart(), u onDestroy())
• albergan un receptor de Broadcast que está ejecutando su método
onReceive().
En general existirán pocos procesos en primer plano, por lo que serán finalizados
únicamente en casos extremos en los cuales la memoria disponible sea tan baja que
sea físicamente imposible mantenerlos activos. En estos casos, el dispositivo
comenzará a utilizar la memoria de paginación 1 por lo que será necesario eliminar
algún proceso en primer plano para mantener la interfaz de usuario fluida.
2. Procesos visibles. Son aquellos procesos que, pese a no tener ningún componente en
primer plano, afectan a lo que el usuario ve en la pantalla. Por lo tanto, serán
actividades cuyo método onPause() ha sido invocado (están en segundo plano porque
están mostrando, por ejemplo, un diálogo) o bien servicios unidos a actividades que
están en primer plano y que requieren que el servicio esté activo (ya que están
unidos).
1
La información que gestione el proceso deberá escribirse en la tarjeta de memoria en vez de en la memoria RAM,
disminuyendo así la velocidad de lectura y escritura.
3. Procesos servicio. Son procesos que están ejecutando un servicio invocado a través
del método de contexto startService() y que no cumplen ninguno de los casos
anteriores. Aunque estos servicios no estén ligados a ningún componente en primer
plano, sí que están realizando tareas en las que el usuario, en general, está poniendo
su atención (como, por ejemplo, la reproducción de música en segundo plano, o la
descarga de algún archivo), por lo que el sistema los mantendrá salvo que no haya
suficiente memoria como para que se ejecuten junto con los procesos con mayor
preferencia.
5. Procesos vacíos. Son procesos que no albergan ningún componente pero que se
mantienen vivos por cuestiones de eficiencia, ya que se agiliza la inicialización de
nuevos componentes sobre dicho proceso al estar el proceso ya creado.
Existen otras condiciones que rigen la importancia de los procesos activos. Si un proceso
alberga un servicio y una actividad visible, dicho proceso siempre se categorizará como un
proceso visible. Además, la importancia de un proceso puede ser incrementada si un segundo
proceso más importante depende de él, de forma que el primero esté realizando tareas para el
segundo.
En cuanto a las actividades que realicen tareas de larga duración las cuales permanezcan
activas incluso cuando la actividad pase a background, será conveniente que inicien servicios
para dichas tareas en vez de simples hilos, puesto que se garantizará que, al menos, las tareas
tendrán prioridad de proceso servicio.
2
Por eso es importante implementar los métodos del ciclo de vida de las actividades correctamente, y guardar el
estado de las mismas. De esta forma, aunque dicha actividad pase a segundo plano y sea eliminada, cuando el
usuario navegue a ella de nuevo, recordará su estado anterior.
Hilos
Cada aplicación es lanzada en un hilo de ejecución principal llamado main. Este hilo está
encargado de enviar los eventos a la interfaz de usuario 3, incluyendo los eventos de
renderizado (encargados de dibujar los diferentes elementos de la pantalla). Además, es el hilo
sobre el cual la aplicación interacciona con los componentes visuales de Android (widgets y
views). Todas las instancias de un mismo componente son lanzadas sobre este hilo por lo que
las llamadas a estos componentes son enviadas sobre dicho hilo. De esta forma, los métodos
callback también se ejecutan siempre en el hilo main.
Si una aplicación realiza tareas de larga duración en el hilo principal, debido al modelo “single
thread” que implementa Android, la interfaz de usuario se bloqueará y no se podrán enviar
eventos (ni siquiera los de renderizado). Si dicho bloqueo dura más de 5 segundos, el sistema
mostrará el mensaje “La aplicación no responde” (mensaje ANR: Application Not Responding),
lo cual es siempre indeseable.
Por otro lado, los widgets y las views de Android no son “thread-safe”, por lo que no se deberá
nunca manipular la interfaz de usuario desde un hilo tipo worker.
Hilos Worker
Tal y como se ha mencionado, las tareas de larga duración deberán realizarse en hilos en
segundo plano o workers.
Debido a que no se puede acceder a un componente visual desde un hilo que no sea el hilo
principal (hilo UI), no es posible implementar un hilo worker para que realice una tarea de
larga duración así:
3
De ahí que también se llame hilo UI.
Para posibilitar el acceso desde otros hilos worker al hilo main, Android provee tres métodos:
• Activity.runOnUiThread(Runnable)
• View.post(Runnable)
• View.postDelayed(Runnable, long)
Utilizando estos métodos, el código anterior es thread-safe, ya que la tarea de larga duración
se realiza en un hilo separado, y la actualización del TextView con el resultado de dicha tarea,
se realiza en el hilo principal, gracias al método post().
TextView resultadoTextView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
resultadoTextView = ((TextView) findViewById(R.id.textViewResultado));
}
Uso de AsyncTask
Para simplificar el uso de los hilos worker, Android proporciona otra solución que consiste en
extender la clase AsyncTask. Esta clase permite realizar tareas asíncronas en la interfaz de
usuario. Para ello, utiliza un hilo worker que se encarga de realizar las tareas de larga duración
cuyos resultados publica en el hilo principal, sin que sea necesario gestionar directamente ni
hilos ni handlers.
Para utilizar esta solución, se deberá extender la clase AsyncTask y al menos implementar el
método doInBackground(), que se ejecuta en un pool de hilos en segundo plano. En general,
también se implementará el método onPostExecute() que se ejecuta en el hilo main y que
recibe el resultado del método doInBackground(). Como este segundo método se ejecuta en
el hilo main, se puede actualizar de forma segura la interfaz de usuario.
Esta clase ayuda a que el código creado sea legible y fácil de mantener, ya que separa el código
correspondiente a la tarea de larga duración (método doInBackground()) del código que
actualiza la interfaz de usuario (método onPostExecute()).
Para ejecutar la tarea asíncrona, bastará invocar a su método execute(Params…) desde el hilo
main (desde la actividad o servicio).
Al declarar una tarea asíncrona, se podrán especificar los tres tipos genéricos de parámetros
que se utilizarán en los diferentes métodos que se implementarán. Además, se puede ampliar
el control de la tarea asíncrona implementando cuatro métodos: onPreExecute(),
doInBackground(), onProgressUpdate() y onPostExecute().
Si no se desea declarar uno de los tipos genéricos, se utilizará Void. Por ejemplo,
En la declaración anterior, se definen los parámetros tipo de los métodos, al usar la notación
de Java de tipos genéricos 4 en AsyncTask<…>. En dicha declaración se establece que el
método doInBackground() recibirá parámetros de tipo Long, que no se utilizará el método
onProgressUpdate() ya que se define su tipo como Void, y se establece, por último un tipo
String como el tipo de resultado obtenido de la tarea en background.
Existe una característica adicional de los tipos genéricos Params y Progress: los métodos de la
tarea asíncrona que los utilizan pueden recibir múltiples parámetros del tipo que definan los
4
Se puede consultar más información en este enlace:
http://docs.oracle.com/javase/tutorial/java/generics/types.html
Cuando una tarea asíncrona se inicia, su ciclo de vida evoluciona a través de los siguientes
métodos:
Las tareas asíncronas pueden ser canceladas en cualquier momento invocando el método
cancel(boolean). Al invocar a cancel(boolean), el método callback isCancelled()
devolverá true, por lo que el método doInBackground(Params…) deberá invocar
frecuentemente a este método para finalizar cuanto antes la tarea iniciada una vez se haya
solicitado su cancelación. Además, en caso de ser cancelada la tarea, una vez finalice el
método doInBackground(Params…), se invocará al método onCancelled(Result) en vez de
a onPostExecute(Result).
5
Esto se indica con puntos suspensivos a continuación del tipo de parámetro, en la declaración del método.
Por último, se ha de mencionar que AsyncTask garantiza que las llamadas a sus métodos
callback están sincronizadas, de forma que las siguientes operaciones son seguras (sin
necesidad de establecerlas explícitamente como synchronized):
@Override
public void onPreExecute() {
// Se crea el diálogo, pasando el contexto
dialogoBarraProgreso = new ProgressDialog(procesosEHilosActivity);
// Se establece el estilo a barra horizontal (por defecto es el
// círculo)
dialogoBarraProgreso.setProgressStyle(
ProgressDialog.STYLE_HORIZONTAL);
// Si no se hace cancelable, la aplicación se bloqueará hasta
// que concluya la hipotética tarea
dialogoBarraProgreso.setCancelable(true);
dialogoBarraProgreso.setIndeterminate(false);
dialogoBarraProgreso.setMessage(
procesosEHilosActivity.getString(R.string.progreso));
dialogoBarraProgreso.setTitle(
procesosEHilosActivity.getString(
R.string.tarea_asincrona_ejecucion));
dialogoBarraProgreso.show();
}
((TextView) procesosEHilosActivity.
findViewById(R.id.textViewResultado)).setText(resultadoFinal);
}
}