Tabla de Contenidos
Android proporciona los Handlers para hacer la comunicación entre procesos (entre threads) más fácil. En esta entrada veremos cómo varios threads pueden comunicar entre si gracias a los Handlers, y en que forma lo pueden hacer, con mensajes o con runnables.
Mecanismo de Paso de Mensajes en Android
La comunicación entre threads permite que un thread emita un trabajo o tarea para ser ejecutado por otro thread. Como sabemos el UI Thread es el responsable de la ejecución y gestión del interfaz de usuario de nuestra aplicación. Android crea el UI Thread para nuestra aplicación cuando esta es lanzada. Por tanto, no debemos ejecutar tareas pesadas en el UI Thread, puesto que esto daría la sensación de que la aplicación se cuelga durante el tiempo que tarda esa acción, no se atiende durante ese tiempo las interacciones de usuario lo que produce una mala experiencia de uso.
Por tanto el mecanismo habitual para trabajar con estas tareas es que el UI Thread lance estos trabajos a otros Threads. Esto lo hemos visto antes en las entradas Threads y AsyncTask . Ahora vamos a ver cómo se consigue realizar esto utilizando Handlers, qué son y que otras clases están involucradas. Pero primero tenemos que conocer cual es el esquema de clases y su relación existente para gestionar el paso de mensajes en Android, porque es gracias a este mecanismo por el cual los Threads pueden comunicar entres sí enviándose tareas a realizar en un patrón productor-consumidor. La idea básica es que desde el UI Thread, que es el thread en el que corre nuestro código de aplicación por defecto, enviemos una tarea pesada a un Worker Thread, para que sea este el que ejecute la tarea.
Fuente: Efficient Android Threading by Anders Goransson.
El UI Thread puede descargar tareas largas enviando mensajes de datos que se procesarán en threads en segundo plano, lo que hemos llamado Worker Thread. El mecanismo de paso del mensaje es un patrón productores-consumidor sin bloqueo, donde ni el hilo productor ni el hilo consumidor se bloquearán durante la transferencia del mensaje. En la Fig. 1 vemos las clases que entran en juego en el paso de mensajes en Android.
Un Looper puede tener vinculados uno o varios Handlers. Estos Handlers, pueden ser del mismo productor o de distintos productores.
Un Looper puede tener una única MessgeQueue (cola de mensajes)
Una MessageQueue puede tener cero o más mensajes.
- El Looper: Es un despachador de mensajes asociado con el único hilo consumidor.
- El Handler: Es a su vez, el procesador de mensajes del hilo de consumidor, es decir, el que se encarga de lanzar la ejecución apropiada para las tareas que llegan al consumidor, pero también es la interfaz para el hilo de productor mediante la cual puede insertar mensajes en la cola. Un Looper tener muchos handlers vinculados, pero todos insertan mensajes en la misma cola.
- La MessageQueue: Es una lista enlazada ilimitada de mensajes para procesar en el hilo del consumidor. Cada Looper, y Thread, tiene como máximo un MessageQueue. Un Thread puede tener únicamente un Looper.
- El Message: Es el mensaje a ser ejecutado en el consumidor. Como veremos estos mensajes pueden ser de dos tipos, Mensajes o Runnables.
Los mensajes son insertados por hilos de productor y procesados por el hilo de consumidor, como se ilustra en la Fig.2.
- Insert: El productor inserta mensajes en la cola usando el Handler conectado al thread consumidor.
- Retrieve: El Looper, que se ejecuta en el thread consumidor, recupera mensajes de la cola de manera secuencial (en principio).
- Dispatch: El Handler es el responsable de lanzar el procesamiento adecuado para los mensajes (Messages o Runnables) en el thread consumidor. Un thread puede tener múltiples instancias de Handler para procesar los mensajes y el looper se asegura que cada mensaje vaya a su handler correspondiente.
Handler
La clase Handler está diseñada para dar soporte a la gestión del trabajo entre dos threads cualesquiera, no siendo el UI Thread necesariamente uno de ellos. Un Handler está asociado con un thread específico. Un thread puede transferir trabajo para otro thread mediante el envío a su Handler asociado de:
- El envío de un Message: Un Message es una clase que puede contener datos tales como un código de mensaje, un objeto arbitrario o valores enteros. Se usan por el thread emisor para indicarle al receptor qué operación realizar, dejando al Handler la implementación de ésta.
- El envío un Runnable: Los Runnables los usamos cuando sabemos exactamente cuáles son pasos de ejecución a realizar, pero queremos que sean ejecutados en el thread receptor.
Los Handlers por tanto, gestionan los Messages y los Runnables haciendo uso de dos componentes asociados al thread. Cada thread está asociado con un MessageQueue y un Looper (Figura 3). Cuando se crea un thread, bien sea el UI thread creado por Android para nuestra aplicación o bien sea uno que creamos con new Thread(), se crean también su Looper y su MessageQueue. Cuando se crea un Handler con new Handler(), éste se vincula al MessageQueue y al Looper del contexto en el que se crea. Si se crea en el contexto de la aplicación o en el de una Activity, se estará vinculando al Looper y MessageQueue del UI Thread. Si el new Handler() se hace en el contexto de un Thread (dentro de su clase) se vinculará al MessageQueue y Looper del propio thread.
- El MessageQueue es una cola de datos que guarda Messages y Runnables.
- El Looper saca estos Messages y Runnables de la MessageQueue y los despacha (atiende) según proceda.
Por tanto un thread tiene finalmente asociados, como se ve en la Fig. 1 :
- Un Handler
- Una MessageQueue que a su vez tiene asociados Mensajes.
- Un Looper
Siendo el Handler el origen y destino de los mensajes de la cola.
El Looper está continuamente esperando a la llegada de trabajos al MessageQueue. Estos trabajos pueden ser Messages o Runnables. Cuando los trabajos llegan el Looper reacciona en función del tipo de trabajo de que se trate. En las figuras 6 y 7 vemos tareas (Messages y Runnables) en la MessageQueue. En la misma MessageQueue puede haber tanto Messages como Runnables, aunque en las figuras solo aparecen de un tipo. El looper, como hemos dicho, en función del tipo de trabajo hace lo siguiente, si es:
- Un Message: El Looper llamará al callback handleMessage() del Handler asociado al thread pasándole el Message.
- Un Runnable: Llamará al método run del propio Runnable.
En el caso de que lo que llega es un Runnable, no hay problema, como decimos se lanza el método run del runnable y aquí acaba el problema. Con los mensajes debemos determinar qué hacer. El propio mensaje es un dato por el que podemos hacer un switch en el callback handleMessage() y en función de qué mensaje se trate haremos una cosa u otra según queramos. Obviamente los mensajes y que debemos hacer con ellos los definimos nosotros para implementar la lógica de nuestra comunicación y aplicación.
En cuanto a la interacción con el UIThread. Si queremos interactuar con el UI Thread, enviándole Runnables (o Messages) (Figs.4,5) por ejemplo, tenemos que instanciar un Handler en el contexto de la aplicación o Activity. Cuando hacemos new Handler() en el onCreate() de una Activity por ejemplo, este nuevo Handler se está vinculando con el Looper y el MessageQueue del UI Thread, porque es en su contexto donde se está creando el handler. Por tanto cualquier runnable o message que enviemos a través de los métodos .post() del handler, serán ejecutados en el UI Thread cuando proceda.
Si un thread A quiere ejecutar un código (lo quiere ejecutar el, no otro thread). Entonces crea u obtiene un Runnable, donde el código a ejecutar está su método run(). Pretende entonces que su handler, el que está asociado al thread A, lo ejecute, es decir, que sea el propio thread el que lo ejecute, no el UI Thread u otro thread. Para ello debe entregarlo (postearlo) a su Handler asociado, que lo insertará en su cola de mensajes, es decir, al Handler que se hacreado en su clase (o que se ha vinculado a su Looper).
Supongamos que un un Thread B utiliza un Handler para enviar un Message a otro Thread C para que ejecute el código asignado al mensaje. El Message se coloca en el MessageQueue del thread C. Será por tanto el Thread C el que, cuando el Looper, extraiga el mensaje lo trate en su Handler.
Ejemplo BasicHandler
Veamos un ejemplo básico de trabajo con Handlers para la comunicación entre threads. Este ejemplo nos servirá para entender claramente cómo y donde se crean los handlers, a qué looper se vinculan y como comunican con el UI Thread.
Vamos a explicar el ejemplo apoyándonos en su código y en la figura 8. Normalmente el código que encontraréis por ahí, no hace exactamente esto, sino que el WorkerThread utiliza el Looper del UI Thread para extraer sus trabajos, es decir, se vincula al Looper del UIThread, veremos luego porqué normalmente se hace esto, que aunque tiene ventajas, también tiene desventajas.
Recordar que un Looper puede tener muchos Handlers asociados, y que el Looper sabe de qué handler debe procesar cada Trabajo.
El interfaz de usuario de la App BasicHandler es como se ve en la Figura 9. Tenemos un botón (Hello) que lanza un toast con un saludo, esto lo tenemos para poder comprobar que mientras el WorkerThread está haciendo su trabajo el usuario sigue pudiendo operar con el interfaz. También tenemos un texto que inicialmente indica “Not Working yet” es decir que no se ha empezado a trabajar, no se ha lanzado ningún trabajo. Y un botón “Launch Task” para lanzar el supuesto trabajo pesado. Al pulsar el botón Launch Task el texto cambia por “Running” indicando que está corriendo el trabajo. Cuando el trabajo pesado ha terminado nos indica con el mensaje “Run 1 times” que ha ejecutado dicho trabajo 1 vez. Cada vez que pulsamos en LaunchTask pasa a running y luego incrementa el número de veces ejecutado en el mensaje.
Aquí tenemos una sutileza. Si pulsamos LaunchTask mientras está Running, (recordar que el UI sigue atendiendo al usuario y por tanto puede volver a lanzar la tarea) podemos querer hacer dos cosas distintas.
- que no se pueda lanzar una tarea mientras hay otra corriendo
- que la segunda tarea se lance aunque haya una corriendo.
En este ejemplo hemos codificado la opción 1 usando un if en el código, pero se puede quitar el if que lo gestiona para tener la opción 2.
En la opción 2, es decir cuando permitimos lanzar un trabajo mientras otro está corriendo, podemos tener a su vez dos variantes:
- a) que se encole la segunda tarea y se ejecute cuando termine la primera o
- b) que se cree una nueva tarea en paralelo y corran las dos a la vez.
En este ejemplo, si quitáis el if que gestiona la opción 1, hemos optado por la variante a). Ya que simplifica el ejemplo. Para gestionar la variante b) tendríamos que crear otro WorkerThread y esto complicaría la explicación. Luego en comentaré más acerca de esto.
De momento tenemos: Un único trabajo pesado corriendo a la vez, impidiendo que se lance otro aunque se pulse el botón.
Las clases que componen nuestro proyecto las véis en la figura 10. A continuación tenéis el código del ejemplo, desplegar aquello que interese. También podéis descargar la App completa en el botón más abajo (Código BasicHanler). Iré explicando el código a la vez que voy incluyendo alguna referencia y explicaciones adicionales.
El código del ejemplo
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:id="@+id/textView" android:layout_width="84dp" android:layout_height="19dp" android:layout_marginTop="32dp" android:text="BasicHandler" android:textAlignment="center" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/btnButton" android:layout_width="119dp" android:layout_height="51dp" android:layout_marginTop="32dp" android:text="Launch Task" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textView" /> <Button android:id="@+id/btnHello" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="32dp" android:text="Say hello" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.498" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/btnButton" /> <TextView android:id="@+id/tvText" android:layout_width="98dp" android:layout_height="wrap_content" android:layout_marginTop="96dp" android:text="Not running yet" android:textAlignment="center" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/btnHello" /> </androidx.constraintlayout.widget.ConstraintLayout>
public class MainActivity extends AppCompatActivity implements View.OnClickListener { WorkerThread mWorkerThread; Button btnButton, btnHello; TextView tvText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnButton = (Button) findViewById(R.id.btnButton); btnButton.setOnClickListener(this); btnHello = (Button) findViewById(R.id.btnHello); btnHello.setOnClickListener(this); tvText = (TextView) findViewById(R.id.tvText); tvText.setText("Not running yet"); mWorkerThread = new WorkerThread(this); //Creamos el Thread. mWorkerThread.start(); //Lanzamos el Thread. Es un Looper Thread. Tiene un Looper propio y no para } @Override protected void onDestroy() { mWorkerThread.mHandler.getLooper().quit(); super.onDestroy(); } @Override public void onClick(View v) { TextView tvText; switch (v.getId()){ case R.id.btnButton: tvText = findViewById(R.id.tvText); if (tvText.getText() != "Running") { if (mWorkerThread.mHandler != null) { tvText.setText("Running"); Message msg = mWorkerThread.mHandler.obtainMessage(0); mWorkerThread.mHandler.sendMessage(msg); } } break; case R.id.btnHello: Toast.makeText(MainActivity.this,"Hola amigos", Toast.LENGTH_LONG).show(); } } }
public class WorkerThread extends Thread { public Handler mHandler; private Handler mUIHandler; private WeakReference<Activity> theActivity; int mDelay = 10000; //10 seconds static int counter; public WorkerThread(Context theActivity) { this.theActivity = new WeakReference<>((Activity)theActivity); this.mUIHandler = new Handler(Looper.getMainLooper()); } private class MyHandler extends Handler { @Override public void handleMessage(Message msg) { if (theActivity.get() != null){ if (msg.what == 0) { counter++; doLongRunningOperation(); mUIHandler.post(new Runnable() { @Override public void run() { TextView tvText; tvText = (TextView) theActivity.get().findViewById(R.id.tvText); tvText.setText("Run " + counter + " times."); } }); } } else{ mUIHandler.post(new Runnable() { @Override public void run() { TextView tvText; tvText = (TextView) theActivity.get().findViewById(R.id.tvText); tvText.setText("The message was lost, try again, please."); } }); } } } public void run() { counter=0; Looper.prepare(); mHandler = new MyHandler(); Looper.loop(); } private void doLongRunningOperation(){ //Sleeps total i x mDelay = x minute. for (int i=0; i<1; i++) { sleep(); } } private void sleep() { try { Thread.sleep(mDelay); } catch (InterruptedException e) { e.printStackTrace(); } } }
Su explicación
Fijémonos en la Figura 8. Por un lado tenemos una Activity (MainActivity) y el MainThread o UIThread de la aplicación y por el otro lado nuestro WorkerThread. La MainActivity se crea y lanza la creación del WorkerThread en el onCreate() de la misma, creamos por tanto nuestro WorkerThread, que hemos declarado como una clase en nuestro proyecto. Cada Thread, el UIThread y el WorkerThread tienen un único Looper y una única MessageQueue. Podemos ver (Fig.8) que el Looper del UIThread tiene dos Handlers vinculados (attached), el creado por Android y un Handler (uiHandler) que hemos creado dentro de nuestro WorkerThread. Vemos también como la tarea pesada “Long Task” se ejecuta en el ámbito del WorkerThread. Vemos que la vista, el TextView que indica “Running” etc. de nuestro layout, se actualiza en el ámbito del UIThread, puesto que el runable que se le pasa se ejecuta en el ámbito del UIThread. Y por último también vemos los eventos “insert”, “retrieve” y “dispatch” de la figura 2.
Si seguimos el esquema productores-consumidores de la figura 2, vemos que en esta aplicación tanto el UIThread como el WorkerThread están actuando como productores y consumidores en distintos momentos, es decir juegan el rol de productor en un momento y el rol de consumidor en otro. Tal como mostramos en la figura 11, donde al principio, la MainActivity, actuando como productor, es la que envía un mensaje al WorkerThread para que éste ejecute la tarea pesada, pero cuando ésta termina, es el WorkerThread el que actúa como productor, enviando al UIThread (a traves de su uiHandler) un runnable para actualizar el estado del TextView en el interfaz de usuario.
Vamos a analizar el flujo de cosas que van pasando fijándonos en la Figura 8, luego lo veremos en el código.
En primer lugar, en el onCreate de la MainActivity se crea (new) un objeto (mWorkerThread) de nuestra clase WorkerThread y se llama a su método start(). En el siguiente código se ve cómo se crea este WorkerThread y cómo se llama a su método start():
public class MainActivity extends AppCompatActivity implements View.OnClickListener { WorkerThread mWorkerThread; Button btnButton, btnHello; TextView tvText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnButton = (Button) findViewById(R.id.btnButton); btnButton.setOnClickListener(this); btnHello = (Button) findViewById(R.id.btnHello); btnHello.setOnClickListener(this); tvText = (TextView) findViewById(R.id.tvText); tvText.setText("Not running yet"); mWorkerThread = new WorkerThread(this); //Creamos el Thread. mWorkerThread.start(); //Lanzamos el Thread. Es un Looper Thread. Tiene un Looper propio y no para } ...
En el constructor de este WorkerThread recibimos un referencia a la MainActivity, bueno a su contexto al que le hacemos un casting a Activity (revisar esta entrada, Contexto). Asignamos la Activity a una varaiable miembro, theActivity para poder referenciar sus vistas en el runnable que lanzaremos posteriormente a la MessageQueue del UIThread. También creamos el Handler uiHandler, que queremos que esté vinculado con el Looper del UIThread pues por éste uiHandler enviaremos al UIThread las actualizaciones que queremos hacer en el interfaz de usuario desde el WorkerThread. El siguiente fragmento de código vemos el constructor del WorkerThread:
public class WorkerThread extends Thread { public Handler mHandler; private Handler mUIHandler; private WeakReference<Activity> theActivity; int mDelay = 10000; //10 seconds static int counter; public WorkerThread(Context theActivity) { this.theActivity = new WeakReference<>((Activity)theActivity); this.mUIHandler = new Handler(Looper.getMainLooper()); } ...
Vemos que la referencia a la MainActivity se asigna a una WeekReference<Activity>, esto, como se explica en Memory Leack – Handler & Inner Clases se hace para no generar una fuga de memoria en caso de que la Activity se destruya y se vuelva a crear como consecuencia de por ejemplo un giro del dispositivo. Pero al fin y al cabo es una referencia a la MainActivity que guardamos en la variable miembro theActivity. Con ella podremos referenciar posteriormente a cualquier vista de la activity.
Volviendo a la figura 8, en segundo lugar, cuando el usuario pulsa el botón Launch Task se envía un mensaje (msg) a través del método sendMessage() del WorkerThread. Para ello, en el onClick() del botón tenemos que acceder al myHandler y hacer uso de su método sendMessage(), esto se ve en el siguiente código:
@Override public void onClick(View v) { TextView tvText; switch (v.getId()){ case R.id.btnButton: tvText = findViewById(R.id.tvText); if (tvText.getText() != "Running") { if (mWorkerThread.mHandler != null) { tvText.setText("Running"); Message msg = mWorkerThread.mHandler.obtainMessage(0); mWorkerThread.mHandler.sendMessage(msg); } } break; case R.id.btnHello: Toast.makeText(MainActivity.this,"Hola amigos", Toast.LENGTH_LONG).show(); } }
En el onClick() también vemos un par de cosas interesantes, la primera es cómo hacemos para que no se lance una nueva tarea pesada si ya hay una en curso. Esto se hace comprobando que el texto del tvText no sea “Running”. Se podría haber hecho de otra forma, con una variable booleana u otra forma más elegante, pero así funciona y es rápido de entender. La otra cosa interesante que hacemos es una comprobación preguntando si el objeto Handler del WorkerThread no es nulo. Esto se hace porque aquí hay una condición de carrera, y no se sabe qué va a ocurrir antes, si el setup del Handler en el WorkerThread o el uso que se haga de él en el UIThread. En nuestro caso podríamos omitirla porque normalmente va a ser raro que el usuario llegue a darle al click antes de que el Handler se haya construido, pero si esa acción no depende de la acción de usuario, entonces podría ser posible, pues no podemos garantizarlo. Por tanto lo colocamos. Si diera el caso que no existiese todavía en el momento de pulsar al botón, no pasaría nada puesto que el usuario volvería a apretar al botón y en ese momento es de esperar que ya esté creado.
En definitiva, lo que hacemos en el onClick() es obtener un mensaje desde el Handler del WorkerThread (veremos más adelante que primero hay que obtener un mensaje, lo podemos modificar y luego enviarlo), en este caso no lo modificamos y lo enviamos al WorkerThread usando sendMessage(msg).
Con esto ya tenemos nuestro msg en la cola del WordkerThread. Cuando su Looper lo extraiga y lo sirva, el myHandler recibirá el callback HandleMessage(), recibiendo el msg. En ese callback está el código que llama a la tarea pesada y cuando termina modifica el texto en el TextView de la activity. Veamos su código:
private class MyHandler extends Handler { @Override public void handleMessage(Message msg) { if (theActivity.get() != null){ if (msg.what == 0) { counter++; doLongRunningOperation(); mUIHandler.post(new Runnable() { @Override public void run() { TextView tvText; tvText = (TextView) theActivity.get().findViewById(R.id.tvText); tvText.setText("Run " + counter + " times."); } }); } } else{ mUIHandler.post(new Runnable() { @Override public void run() { TextView tvText; tvText = (TextView) theActivity.get().findViewById(R.id.tvText); tvText.setText("The message was lost, try again, please."); } }); } } }
En la sobrecarga del callback handleMessage(), vemos que en función del valor del mensaje, en este caso 0 determinamos que hacer con un if. Como hemos comentado antes podríamos tener una sentencia switch para gestionar los distintos mensajes. En concreto, cuando recibimos el mensaje, lo que hacemos es incrementar el contador de veces ejecutada la tarea pesada, llamarla (aquí está simulada con un sleep()) y posteriormente creamos un runnable que, de manera inline, le pasamos al mUIHandler.post() con lo que lo estamos enviando al UIThread. Recordar que ese handler al UIThread lo hemos creado en el constructor del WorkerThread. El runable lo que hace es acceder al TextView cambiar su valor. Fijaros que lo encuentra con el findViewById() de la Activity que hemos obtenido en el constructor del WorkerThread y que ya hemos dicho que accedemos a el como una WeekReference<Activity>. Por eso, como es una WeekReference, si la Activity no está disponible en el momento de recibir el mensaje por el callback, no hacemos nada, puesto que daría error de ejecución, es decir dejamos pasar el evento callback. Si resulta que si está disponible la activity, accedemos a ella desde la WeekReference<Activity> con el médoto .get(), es decir, en theActivity.get() tenemos la activity y por eso funciona el findViewById().
Aquí surge una duda, ¿qué pasa si cuando recibimos el callback la Activity no está disponible y por tanto dejamos pasar el mensaje? Pues que no lanzamos la tarea pesada. Recordar que en este punto, el mensaje en la pantalla es “Running” y si no lo cambiamos dará la sensación que se ha colgado o está tardando demasiando, por lo que en este ejemplo tomamos la determinación de cambiarlo por “The message was lost, try again, please” para que el usuario sepa que ha pasado. Esta rama del código es difícil de conseguir ejecutar, ya que hay que cambiar la orientación de la pantalla en el momento apropiado tras pulsar el botón Launch Task y que coincida que no se ha recuperado la Activity en el momento que llegue el callback handleMessage(). Es difícil, pero se puede dar, por eso implementamos el else.
Al llamar al mUIHandler.post() estamos enviando el runnable a la cola del UIThread, con lo que en ese momento ya tenemos la tarea pesada ejecutada y un runnable en la cola del UIThread, que el Main Looper extraerá y será ejecutado llamando a su método .run() donde nosotros hemos indicado que lo que hay que hacer es cambiar el texto del TextView para darle feedback al usuario.
Consideraciones al ejemplo
Supongamos que quitamos el control de que ya existe una tarea pesada corriendo para evitar enviar otra a ejecución. Tenemos que darnos cuenta que la tarea pesada se ejecuta tan pronto como llegue el callback handleMessage(msg) y que luego, cuando esta termina, se envía un runnable al UIThread para la actualización del TextView. Si damos varias veces al botón Launch Task, no veremos cambiar el mensaje de uno en uno. Aunque se lancen dos runables para ejecutarlos, estos pueden ser extraidos por el Main Looper y ejecutarlos seguidos, cambiando tan rápido que no veamos el cambio de uno en uno. Quizás aumentando el tiempo de ejecución de la tarea pesada …. Otra opción, para ver que efectivamente se ejecutan los dos, es modificar el ejemplo para que en vez de cambiar un textView, añadir a modo de log un texto con la fecha y hora de cuando se ha ejecutado la tarea pesada.
Hemos visto como funciona la comunicación y envío de mensajes y runnables utilizando el Handler en este ejemplo. Pero me gustaría comentaros unos matices que hacen que no sea esta forma la que encontrareis de programar el MultiThreading con Handlers en Android.
Fijémonos en el método .run() de nuestro WorkerThread, que se lanza con el .start() en el onCreate() de la MainActivity.
public void run() { counter=0; Looper.prepare(); // (1) mHandler = new MyHandler(); //(2) Looper.loop(); // (3) }
La inicialización del counter es un detalle menor, pero la hacemos aquí. Lo importante son las siguientes sentencias.
- Looper.prepare(): Hay que lanzar la preparación del Looper, esto crea y vincula el Looper al Thread y crea la MessageQueue asociada también al thread.
- Creacion del handler: Creamos un Handler llamando a new. En nuestra clase myHandler no tenemos un constructor definido, por lo que se llama al constructor por defecto de la clase padre que es Handler(). El nuevo objeto lo tenemos en mHandler.
Hay que tener en cuenta que como usamos el constructor por defecto, el objeto se vincula al Looper del contexto en donde se crea, en este caso al Looper del Thread. Es decir, se vincula al Looper que acabamos de crear y preparar en el paso 1.
Por esto el Looper.prepare() tenemos que ponerlo antes de la construcción del Handler, puesto que si no éste, el Handler, no podría vincularse a ningún Looper.
Hay que tener en cuenta también, que existe otro constructor para los Handlers que recibe un Looper como parámetro, por lo que no necesariamente estamos forzados a que el Handler conecte con el Looper del thread o activity que lo crea. De hecho si nos fijamos en el código del constructor del WorkerThread, vemos que creamos el uiHandler para vincularlo con el MainLooper gracias a que su constructor recibió el parámetro Looper.getMainLooper(). - Looper.loop(): realmente lo que hace es lanzar al Looper para que se ponga a dar vueltas esperando mensajes en la cola para cogerlos y servirlos al handler apropiado.
Respecto a la sentencia 3, Looper.loop() es muy importante saber que se trata de una sentencia bloqueante. Esto quiere decir que el código que la alcanza no continúa, se bloquea, hasta que el looper es parado explícitamente con la orden Looper.quit(), que en nuestro ejemplo lo hacemos en el callback onDestroy() de la MainActivity. Si nos damos cuenta, nuestro método run() del Thread no termina, es decir, que nuestro WorkerThread se queda funcionando hasta que la activity termina. Se queda funcionando concurrentemete con el UIThread, pero no hará nada útil hasta que su Looper envíe a su Handler un mensaje que haya extraido de su cola. Tenemos que saber esto porque si la activty (que es donde se crea el myThread) termina sin que se haya parado el WorkerThread el colector de basura no la eliminaría porque sigue teniendo un proceso activo vinculada a ella, creando un problema de memoria a la larga. Es por ello que explicitamente llamamos al Looper.quit() en el onDestroy() de la Activity.
Pero que pasa si paramos el Looper con Looper.quit() antes de tiempo. Pues que ya no podremos utilizar más ese WorkerThread. Un Thread sólo puede tener asociado un Looper. Un error de ejecución ocurrirá si la aplicación intenta establecer un segundo Looper para un Thread.
Un Thread sólo puede tener una cola de mensajes, lo que significa que los mensajes que se reciben por parte de distintos productores en la cola de mensajes del thread consumidor (nuestro WorkerThread en este caso, a) en Figura 11.) son procesados secuencialmente por éste. Por tanto, el mensaje que se esté procesando bloqueará el avance de la cola hasta que termine, en ese momento el Looper podrá servir otro para su procesamiento. Este detalle es muy importante. Los mensajes o runnables, que llevan asociado un trabajo muy pesado hay que postearlos con cuidado puesto que retrasarán los siguientes y sólo si podemos permitir el delay en la cola provocado por el mensaje pesado lo podemos hacer.
En nuestro ejemplo, el trabajo pesado lo lanzamos en un Worker Thread y si llega otro no pasa nada se lanzará su ejecución cuando termine el anterior. Si nos fijamos bien el WorkerThread está diseñado específicamente para tratar un determinado tipo de tareas pesadas en nuestra aplicación (o durante la vida de una Activity). Además sólo hemos creado uno, es decir, ya asumimos que las tareas pesadas se van a gestionar secuencialmente. Si hubiésemos necesitado varias tareas pesadas del mismo tipo (del mismo WorkerThread) de manera concurrente, tendríamos que haber optado por otra solución. Una muy simple hubiera sido crear varios mWorkerThreads en el onCreate() de la activity, por ejemplo un Pool de WorkerThreads que pudieran funcionar concurrentemente ( veremos en otro artículo cómo Android da solución a esto con el Executor)
Esto nos lleva a darnos cuenta de otro detalle. Hemos dicho que un Handler puede vincularse con cualquier Thread a traves del Looper del mismo. Eso es precisamente lo que hicimos en el constructor del WorkerThread con el parámetro Looper.getMainLooper(). Pues precisamente, si un Handler se vincula con el UIThread y le da por enviarle trabajos pesados, estará ralentizando el propio interfaz de usuario de la aplicación impidiendo que el UIThread de servicio a los callbacks, y acciones de usuario. Por tanto mucho ojo con eso. Como vemos las tareas que enviamos desde nuestro WorkerThread a ejecutar en el UIThread con nuestro mUIHandler son muy livianas, se tratan de tareas de actualización del propio interfaz de usuario y no de tareas más pesadas. Precisamente eso es lo que queremos evitar.
Y un último recordatorio, al crear un Thread con new Thread() se está creando ya su Looper. Es decir el looper del nuevo thread ya está vinculado a éste. También cuando creamos un Handler éste se vincula por defecto al Looper del contexto donde se crea, si lo creamos en el propio thread se vincula al looper que se ha creado al crear el Thread, por eso en el run() del WorkerThread creamos el Hander, que como su clase es interna al Thread se vincula al looper del thread.
App del ejemplo
Aquí os dejo el código de la App Completa:
Ahora que hemos revisado lo que pasa paso a paso quizás entendamos mejor el proceso, es un tema complicado que hay que seguir con detalle para no perderse y tener los conceptos claros. Seguramente habrá que revisar esto varias veces, si necesitáis aclaración, enviar un e-mail si no estás registrado o pon un comentario si lo estás.
El ejemplo usando un HandlerThread
El ejemplo anterior parece bastante complicado la primera vez que lo analizas, luego, una vez que has entendido los entresijos no es tan complicado. Pero se puede simplificar un poco utilizando la clase HandlerThread que Android desarrollo para simplificar el manejo del Looper al programador.
La clase HandlerThread implementa un Thread para el que ya prepara el looper internamente. Podían haberla llamado LooperThread pero en fin….
Con esta clase creamos un Thread para el cual no hace falta crear un método run() donde hagamos el Looper.prepare(), esto ya lo hace internamente.
Pero si que tenemos que crear el handler para el Thread. El problema es que no podemos hacerlo en el constructor del Thread porque el Looper puede no estar preparado todavía, por lo que lo haremos en un método a parte. Este método puede llamarse Prepare(), de forma que si nuestro Handler lo hacemos público desde fuera podrían llamarlo antes de empezar a enviarnos tareas. El siguiente extracto muestra cómo sería esto.
public class WorkerThread extends HandlerThread { public Handler mHandler; private Handler mUIHandler; private WeakReference<Activity> theActivity; int mDelay = 10000; //10 seconds static int counter; private boolean handlerSetted; public WorkerThread(Context theActivity, String name) { super(name); this.theActivity = new WeakReference<>((Activity)theActivity); this.mUIHandler = new Handler(Looper.getMainLooper()); } public void Prepare(){ mHandler = new MyHandler(getLooper()); // la clase MyHandler como la anterior pero con un constructor... } .... ///// La clase MyHandler deberá llevar un constructor para poder pasarle el Looper private class MyHandler extends Handler { public MyHandler(Looper looper) { super(looper); } //// desde la activity harían: mWorkerThread = new WorkerThread(this, "My Worker Thread"); //Creamos el Thread. mWorkerThread.start(); //Lanzamos el Thread. Es un Looper Thread. Tiene un Looper propio y no para mWorkerThread.Prepare();
Hemos añadido un String name en el constructor de nuestro WorkerThread porque el constructor por defecto del HandlerThread lo lleva. Llamamos al super() y luego completamos nuestras variables miembro como antes. Vemos nuestro nuevo método Prepare() que lo único que hace es instanciar el MyHandler que está definido como en el ejemplo anterior , excepto que recibe un Looper, precisamente el que el HandlerThread crea automáticamente al cual accedemos usando la función getLooper(). Vemos como en la Main Activity hay que llamar a Prepare() tras de lanzar el start() de nuestro WorkerThread. Una vez preparado ya podremos lanzar tareas y mensajes a nuestro nuevo WorkerThread.
Para ello hemos creado un par de métodos en el WorkerThread, que llamamos SendMessage() y PostTask() para facilitar la vida en la activity y llamar directamente en vez de acceder por el mHandler. Así podemos hacer private el mHandler de nuestro WorkerThread:
public class WorkerThread extends HandlerThread { private Handler mHandler; private Handler mUIHandler; private WeakReference<Activity> theActivity; int mDelay = 10000; //10 seconds static int counter; private boolean handlerSetted; public WorkerThread(Context theActivity, String name) { super(name); this.theActivity = new WeakReference<>((Activity)theActivity); this.mUIHandler = new Handler(Looper.getMainLooper()); this.handlerSetted = false; } public void SendMessage(int what){ //NO podemos inicializar el handler hasta que el thread no esté creado, por eso no va esto en el construtor. if (!handlerSetted){ mHandler = new MyHandler(getLooper()); //El thread ya esta creado con looper, que pasamos al constructor. handlerSetted = true; } //Obtenemos el mensaje primero del Handler Message msg = mHandler.obtainMessage(what); mHandler.sendMessage(msg); } public void PostTask(Runnable runnable){ //NO podemos inicializar el handler hasta que el thread no esté creado, por eso no va esto en el construtor. if (!handlerSetted){ mHandler = new MyHandler(getLooper()); //El thread ya esta creado con looper, que pasamos al constructor. handlerSetted = true; } mHandler.post(runnable); }
Además, para evitar que desde el Activity tengan que llamar a Prepare(), podemos dejarlo privado o eliminarlo completamente. Comprobaremos gracias a un boleano, en los métodos SendMessage() y PostTask() si el Handler fue instanciado ya, si no lo fue, hacemos true el boleano e instanciamos el Handler. Es decir, la primera llamada que recibamos a SendMessge() o PostTask() será la encargada de hacer el “prepare”, es decir, de instanciar el MyHandler.
Aquí os dejo el código completo de esta versión. Fijaros también que hemos quitado la comprobación de la condición de carrera que comentábamos antes en el onClick(). También hemos creado un método Quit(), que se encarga de parar el Looper y que es llamdo en el onDestroy() de la MainActivity. El Layout es el mismo que el ejemplo anterior.
El código del ejemplo
public class MainActivity extends AppCompatActivity implements View.OnClickListener { WorkerThread mWorkerThread; Button btnButton, btnHello; TextView tvText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnButton = (Button) findViewById(R.id.btnButton); btnButton.setOnClickListener(this); btnHello = (Button) findViewById(R.id.btnHello); btnHello.setOnClickListener(this); tvText = (TextView) findViewById(R.id.tvText); tvText.setText("Not running yet"); mWorkerThread = new WorkerThread(this, "My Worker Thread"); //Creamos el Thread. mWorkerThread.start(); //Lanzamos el Thread. Es un Looper Thread. Tiene un Looper propio y no para } @Override protected void onDestroy() { mWorkerThread.Quit(); super.onDestroy(); } @Override public void onClick(View v) { TextView tvText; switch (v.getId()){ case R.id.btnButton: tvText = findViewById(R.id.tvText); if (tvText.getText() != "Running") { tvText.setText("Running"); mWorkerThread.SendMessage(0); } break; case R.id.btnHello: Toast.makeText(MainActivity.this,"Hola amigos", Toast.LENGTH_LONG).show(); } } }
public class WorkerThread extends HandlerThread { private Handler mHandler; private Handler mUIHandler; private WeakReference<Activity> theActivity; int mDelay = 10000; //10 seconds static int counter; private boolean handlerSetted; public WorkerThread(Context theActivity, String name) { super(name); this.theActivity = new WeakReference<>((Activity)theActivity); this.mUIHandler = new Handler(Looper.getMainLooper()); this.handlerSetted = false; } public void SendMessage(int what){ //NO podemos inicializar el handler hasta que el thread no esté creado, por eso no va esto en el construtor. if (!handlerSetted){ mHandler = new MyHandler(getLooper()); //El thread ya esta creado con looper, que pasamos al constructor. handlerSetted = true; } //Obtenemos el mensaje primero del Handler Message msg = mHandler.obtainMessage(what); mHandler.sendMessage(msg); } public void PostTask(Runnable runnable){ //NO podemos inicializar el handler hasta que el thread no esté creado, por eso no va esto en el construtor. if (!handlerSetted){ mHandler = new MyHandler(getLooper()); //El thread ya esta creado con looper, que pasamos al constructor. handlerSetted = true; } mHandler.post(runnable); } public void Quit(){ getLooper().quit(); } public Looper GetLooper(){ return mHandler.getLooper(); } private class MyHandler extends Handler { public MyHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { if (theActivity.get() != null){ if (msg.what == 0) { counter++; doLongRunningOperation(); mUIHandler.post(new Runnable() { @Override public void run() { TextView tvText; tvText = (TextView) theActivity.get().findViewById(R.id.tvText); tvText.setText("Run " + counter + " times."); } }); } } else{ mUIHandler.post(new Runnable() { @Override public void run() { TextView tvText; tvText = (TextView) theActivity.get().findViewById(R.id.tvText); tvText.setText("The message was lost, try again, please."); } }); } } } private void doLongRunningOperation(){ //Sleeps total i x mDelay = x minute. for (int i=0; i<1; i++) { sleep(); } } private void sleep() { try { Thread.sleep(mDelay); } catch (InterruptedException e) { e.printStackTrace(); } } }
La App del ejemplo
Os dejo también la App completa del ejemplo para cargarla en Android Studio.
Más simple e intuitiva
Finalmente vamos a simplificar y hacer más intuitiva la programación. Para ello modificaremos la ultima versión BasicHandlerThread y creamos una versión nueva llamada BasicHandlerThreadMessages.
Hay cosas que no son intuitivas, en cuanto a dónde se programa cada cosa, en la aplicación anterior y que serían más intuitivas si optáramos por estas consideraciones:
- El uiHandler se crea en la MainActivity y se pasa como parametro al WorkerThread en vez de la propia activity.
- Es por tanto en la MainActivity donde declaro una clase privada UIHandler de la que instancio el uiHandler que le pasamos al WorkerThread
- El que sabe cuando termina la tarea y cuando está corriendo es el Handler del WorkerThread y por tanto es el que envía el mensaje de terminado al uiHandler.
- El que sabe si la tarea está corriendo es el WorkerThread. No debemos depender de un texto en el interfaz de usuario para ello. El WorkerThread gestionará no lanzara la tarea si ya esta corriendo. Para ello declara una función que podrá ser llamada por su handler para establecer el estado de running o no de la tarea. Si está running, el WorkerThread envía un mensaje al uiHandler.
- Esta clase UIHandler es la que gestiona en su handleMessage() las vistas del interfaz de usuario. Para ello recibe una serie de mensajes, no runables.
- uiHandler.handleMesage() recibe dos mensajes distintos:
- WT_DONE: que indica que ha terminado la tarea pesada y que además lleva un argumento de cuantas veces ha corrido. Con este se actualiza el mensaje indicando cuantas veces ha corrido.
- WT_RUNNING: que indica que el WorkerThread recibió un mensaje para lanzar la tarea cuando esta ya estaba corriendo. Con este se actualiza el mensaje para indicar que se espere por favor.
- Hacemos uso de una clase Globals donde establecemos las constantes (variables estáticas finales) que nos servirán en todas las clases que queramos, definiéndolas en un único lugar.
Las consideraciones anteriores hacen más intuitivo que se debe codificar donde.
Os dejo el código de la nueva versión BasicHandlerThreadMessages para que analicéis estas mejoras.
Globalspublic class Globals { //Worker Thread Globals public static final int WT_DONE = 1; public static final int WT_RUNNING = 2; }
public class MainActivity extends AppCompatActivity implements View.OnClickListener { WorkerThread mWorkerThread; Handler uiHandler; Button btnButton, btnHello; TextView tvText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnButton = (Button) findViewById(R.id.btnButton); btnButton.setOnClickListener(this); btnHello = (Button) findViewById(R.id.btnHello); btnHello.setOnClickListener(this); tvText = (TextView) findViewById(R.id.tvText); tvText.setText("Not running yet"); //Creamos el uiHandler, aquí puesto que correrá las tareas en el UIThread. Parece más lógico. uiHandler = new UIHandler(); mWorkerThread = new WorkerThread(uiHandler, "My Worker Thread"); //Creamos el Thread. mWorkerThread.start(); //Lanzamos el Thread. Es un Looper Thread. Tiene un Looper propio y no para } @Override protected void onDestroy() { mWorkerThread.Quit(); super.onDestroy(); } @Override public void onClick(View v) { TextView tvText; switch (v.getId()){ case R.id.btnButton: tvText = findViewById(R.id.tvText); tvText.setText("Running"); mWorkerThread.SendMessage(0); break; case R.id.btnHello: Toast.makeText(MainActivity.this,"Hola amigos", Toast.LENGTH_LONG).show(); } } private class UIHandler extends Handler{ @Override public void handleMessage(Message msg) { switch (msg.what){ case Globals.WT_DONE: int counter = msg.arg1; tvText.setText("Run " + counter + " times."); break; case Globals.WT_RUNNING: tvText.setText("The task is currently running, please be patient."); break; } } } }
public class WorkerThread extends HandlerThread { private Handler mHandler; private Handler mUIHandler; private WeakReference<Activity> theActivity; int mDelay = 5000; //5 seconds private boolean handlerSetted; private boolean doingLongTask; protected void SetDoing(boolean newStatus){ doingLongTask = newStatus; } public WorkerThread(Handler uiHandler, String name) { super(name); this.mUIHandler = uiHandler; this.handlerSetted = false; this.doingLongTask = false; } public void SendMessage(int what){ //NO podemos inicializar el handler hasta que el thread no esté creado, por eso no va esto en el construtor. if (!handlerSetted){ mHandler = new MyHandler(getLooper()); //El thread ya esta creado con looper, que pasamos al constructor. handlerSetted = true; } //Antes de enviar una nueva tarea comprobamos si ya hay una corriendo if (!doingLongTask) { //Obtenemos el mensaje primero del Handler Message msg = mHandler.obtainMessage(what); mHandler.sendMessage(msg); } else{ //Está running la long Task, informamos Message uiMessage = mUIHandler.obtainMessage(Globals.WT_RUNNING); mUIHandler.sendMessage(uiMessage); } } public void PostTask(Runnable runnable){ //NO podemos inicializar el handler hasta que el thread no esté creado, por eso no va esto en el construtor. if (!handlerSetted){ mHandler = new MyHandler(getLooper()); //El thread ya esta creado con looper, que pasamos al constructor. handlerSetted = true; } mHandler.post(runnable); } public void Quit(){ getLooper().quit(); } public Looper GetLooper(){ return mHandler.getLooper(); } private class MyHandler extends Handler { private int counter; public MyHandler(Looper looper) { super(looper); counter=0; } @Override public void handleMessage(Message msg) { if (msg.what == 0) { //Comprobamos si está running una anterior. SetDoing(true); doLongRunningOperation(); counter++; Message uiMessage = mUIHandler.obtainMessage(Globals.WT_DONE, counter, 0); mUIHandler.sendMessage(uiMessage); SetDoing(false); } } } private void doLongRunningOperation(){ //Sleeps total i x mDelay = x minute. for (int i=0; i<1; i++) { sleep(); } } private void sleep() { try { Thread.sleep(mDelay); } catch (InterruptedException e) { e.printStackTrace(); } } }
Métodos básicos de la clase Handler para enviar Runnables y Messages:
Métodos utilizados para enviar Runnables a un Handler:
Añade el Runnable al MessageQueue
boolean post(Runnable r)
Añade el Runnable al MessageQueue para ser ejecutado en un instante concreto.
boolean postAtTime(Runnable r, long uptimeMilis)
Añade el Runnable al MessageQueue para ser ejecutado pasado un tiempo definido.
boolean postDelayed(Runnable r, long uptimeMilis)
Para enivar un Message a un Handler:
Para enviar un Message primero hay que crearlo:
- Llamando al método de la clase Handler, Handler.obtainMessage() obtenemos un Message para el cual el Handler ya está asociado
- Llamando al método de la clase Message, Message.obtain() que nos devuelve un Message del global pool sin tener asociado el Handler ni los datos.
- Hay mas variantes para realizar esto, ver la documentación.
Para enviar el Message se puede usar:
Para enviar instantaneamente el Message
public final boolean sendMessage (Message msg)
Para enviar instantáneamente y colocarlo en la cabeza de cola
public final boolean sendMessageAtFrontOfQueue (Message msg)
Para enviar el Message a la cola para su ejecución en un instante definido.
public boolean sendMessageAtTime (Message msg, long uptimeMillis)
Para enviar el Message a la cola para su ejecución tras un Delay a partir del momento de envío.
public final boolean sendMessageDelayed (Message msg, long delayMillis)
En el ejercicio ThreadingHandler Runnable & Messages tenéis el código de las dos soluciones, con runnables y con mensajes, partiendo de los ejercicios anteriores de esta sección.
Usos comunes de un Handler
Los usos principales para un Handler son por tanto:
- Planficar mesajes y runnables para ser ejecutados en un momento posterior.
- Encolar una acción para ser realizada en otro thread diferente al propio.