Concurrencia práctica en Android con HaMeR

En Entendiendo la concurrencia en Android usando HaMeR, hablamos de los conceptos básicos de HaMeR (Entrenador de animales, Mensaje, y Ejecutable) marco de referencia. Cubrimos sus opciones, así como cuándo y cómo usarlo.. 

Hoy, crearemos una aplicación sencilla para explorar los conceptos aprendidos. Con un enfoque práctico, veremos cómo aplicar las diferentes posibilidades de HaMeR en la gestión de la concurrencia en Android.

1. La aplicación de muestra

Pongámonos a trabajar y publiquemos algunos. Ejecutable y enviar Mensaje Objetos en una aplicación de ejemplo. Para que sea lo más simple posible, exploraremos solo las partes más interesantes. Aquí se ignorarán todos los archivos de recursos y las llamadas de actividad estándar. Por eso le recomiendo encarecidamente que revise el código fuente de la aplicación de muestra con sus extensos comentarios.. 

La aplicación constará de:

  • Dos actividades, una para Ejecutable otro para Mensaje llamadas
  • Dos HandlerThread objetos:
    • Trabajador para recibir y procesar llamadas desde la interfaz de usuario
    • Contrahilo para recibir Mensaje llamadas desde el Trabajador
  • Algunas clases de utilidades (para preservar objetos durante los cambios de configuración y para el diseño)

2. Publicar y recibir Runnables

Comencemos experimentando con el Handler.post (Ejecutable) método y sus variaciones, que agregan un ejecutable a un Cola de mensajes asociado a un hilo. Vamos a crear una actividad llamada Ejecutable, que se comunica con un hilo de fondo llamado Trabajador.

los Ejecutable crea una instancia de un hilo de fondo llamado Trabajador, pasando un Entrenador de animales y un WorkerThread.Callback como parametros La actividad puede hacer llamadas en Trabajador para descargar de forma asíncrona un mapa de bits y mostrar un brindis en un momento determinado. Los resultados de las tareas realizadas por el hilo de trabajo se pasan a Ejecutable por runnables publicado en el Entrenador de animales Recibido por Trabajador.

2.1 Preparando un Handler para RunnableActivity

Sobre el Ejecutable vamos a crear un Entrenador de animales para ser pasado a Trabajador. los uiHandler será asociado con el Looper del subproceso de la interfaz de usuario, ya que se llama desde ese subproceso.

la clase pública RunnableActivity extiende la Actividad // Handler que permite la comunicación entre // el WorkerThread y la Activity protected Handler uiHandler; @Override protected void onCreate (Bundle savedInstanceState) super.onCreate (savedInstanceState); setContentView (R.layout.activity_main); // preparar el controlador de UI para enviarlo a WorkerThread uiHandler = new Handler (); 

2.2 Declarar Trabajador y su interfaz de devolución de llamada

los Trabajador Es un hilo de fondo donde comenzaremos diferentes tipos de tareas. Se comunica con la interfaz de usuario utilizando el respuestaHandler y una interfaz de devolución de llamada recibida durante su instanciación. Las referencias recibidas de las actividades son: Referencia débil <> Tipo, ya que una actividad podría ser destruida y la referencia perdida..

La clase ofrece una interfaz que puede ser implementada por la interfaz de usuario. También se extiende HandlerThread, una clase de ayuda construida sobre Hilo que ya contiene una Looper, y un Cola de mensajes. De ahí que tenga el correcto. prepararusar el framework HaMeR.

la clase pública WorkerThread amplía la interfaz HandlerThread / ** * para facilitar las llamadas en la interfaz de usuario. * / public interface Callback void loadImage (imagen de mapa de bits); void showToast (String msg);  // Este controlador será el único responsable // de publicar Runnables en este Thread private Handler postHandler; // El controlador se recibe de MessageActivity y RunnableActivity // responsable de recibir las llamadas de Runnable que se procesarán // en la interfaz de usuario. La devolución de llamada ayudará a este proceso. debilidad privada respuestaHandler; // Devolución de llamada desde la interfaz de usuario // es una WeakReference porque se puede invalidar // durante los "cambios de configuración" y otros eventos WeakReference privados llamar de vuelta; Cadena final privada imageAUrl = "https://pixabay.com/static/uploads/photo/2016/08/05/18/28/mobile-phone-1572901_960_720.jpg"; / ** * El constructor recibe un controlador y una devolución de llamada de la interfaz de usuario * @param responseHandler a cargo de la publicación del Runnable en la interfaz de usuario * @param devolución de llamada funciona junto con responseHandler * permitiendo llamadas directamente en la interfaz de usuario * / public WorkerThread (Handler responseHandler, devolución de llamada de devolución de llamada) super (TAG); this.responseHandler = new WeakReference <> (responseHandler); this.callback = new WeakReference <> (callback); 

2.3 Inicializando Trabajador

Necesitamos agregar un método para Trabajador Ser llamado por las actividades que preparan el hilo. postHandler para usar. El método debe llamarse solo después de que se inicie el hilo.

public class WorkerThread extiende HandlerThread / ** * Prepare el postHandler. * Se debe llamar después de que el hilo haya comenzado * / public void prepareHandler () postHandler = new Handler (getLooper ()); 

Sobre el Ejecutable debemos implementar WorkerThread.Callback e inicializa el hilo para que pueda ser utilizado.

la clase pública RunnableActivity extiende la actividad Implementa WorkerThread.Callback // BackgroundThread, responsable de descargar la imagen protegida WorkerThread workerThread; / ** * Inicialice la instancia @link WorkerThread * solo si aún no se ha inicializado. * / public void initWorkerThread () if (workerThread == null) workerThread = new WorkerThread (uiHandler, this); workerThread.start (); workerThread.prepareHandler ();  / ** * establece la imagen descargada en el subproceso bg en imageView * / @Override public void loadImage (imagen de mapa de bits) myImage.setImageBitmap (image);  @Override public void showToast (msg. De cadena final) // a implementar

2.4 Utilizando Handler.post () en el hilo de trabajo

los WorkerThread.downloadWithRunnable () Método descarga un mapa de bits y lo envía a Ejecutable para ser mostrado en una vista de imagen. Ilustra dos usos básicos de la Handler.post (Runnable run) mando:

  • Para permitir que un subproceso publique un objeto ejecutable en un MessageQueue asociado a sí mismo cuando .enviar() se llama en un Handler asociado con el Looper de Thread.
  • Para permitir la comunicación con otros hilos, cuando .enviar() es llamado en un Handler asociado con otro Looper de Hilo.
  1. los WorkerThread.downloadWithRunnable () método publica un Ejecutable al Trabajadores Cola de mensajes utilizando la postHandler, una Entrenador de animales asociado con Trabajoes Looper.
  2. Cuando se procesa el runnable, descarga un Mapa de bits sobre el Trabajador.
  3. Después de descargar el mapa de bits, respuestaHandler, un controlador asociado con el subproceso de la interfaz de usuario, se utiliza para publicar un runnable en el Ejecutable que contiene el mapa de bits.
  4. El ejecutable se procesa, y el WorkerThread.Callback.loadImage se utiliza para exhibir la imagen descargada en una ImageView.
public class WorkerThread extiende HandlerThread / ** * publica un Runnable to WorkerThread * Descarga un mapa de bits y envía la imagen * a la interfaz de usuario @link RunnableActivity * usando el @link #responseHandler con * ayuda desde el @link #callback * / public void downloadWithRunnable () // post Runnable to WorkerThread postHandler.post (new Runnable () @Override public void run () try // sleeps durante 2 segundos para emular la operación de larga duración TimeUnit.SECONDS .sleep (2); // Descargar imagen y enviarla a UI downloadImage (imageAUrl); catch (InterruptedException e) e.printStackTrace (););  / ** * Descargue un mapa de bits con su url y * envíe a la UI la imagen descargada * / private void downloadImage (String urlStr) // Cree una conexión HttpURLConnection connection = null; intente URL url = nueva URL (urlStr); connection = (HttpURLConnection) url.openConnection (); // obtener el flujo de la url InputStream en = new BufferedInputStream (connection.getInputStream ()); Bitmap final bitmap = BitmapFactory.decodeStream (in); if (bitmap! = null) // envía el bitmap descargado y un comentario a la interfaz de usuario loadImageOnUI (bitmap);  else  catch (IOException e) e.printStackTrace ();  finalmente if (connection! = null) connection.disconnect ();  / ** * envía un Bitmap a la interfaz de usuario * publicando un Runnable al @link #responseHandler * y usando @link Callback * / private void loadImageOnUI (imagen de Bitmap final) Log.d (TAG, "loadImageOnUI (" + image + ")"); if (checkResponse ()) responseHandler.get (). post (new Runnable () @Override public void run () callback.get (). loadImage (image););  // verifique si responseHandler está disponible // si no, la Actividad está pasando por algún evento de destrucción privado booleano checkResponse () return responseHandler.get ()! = null; 

2.5 utilizando Handler.postAtTime () y Activity.runOnUiThread ()

los WorkerThread.toastAtTime ()programa una tarea para ser ejecutada en un cierto tiempo, exhibiendo un tostada al usuario. El método ilustra el uso de la Handler.postAtTime () y el Activity.runOnUiThread ().

  • Handler.postAtTime (Runnable run, long uptimeMillis) publica un ejecutable en un momento dado.
  • Activity.runOnUiThread (Runnable run) utiliza el controlador de interfaz de usuario predeterminado para publicar un runnable en el hilo principal.
public class WorkerThread extiende HandlerThread / ** * muestra un Toast en la interfaz de usuario. * programa la tarea teniendo en cuenta la hora actual. * Se puede programar en cualquier momento, estamos * usando 5 segundos para facilitar la depuración * / public void toastAtTime () Log.d (TAG, "toastAtTime (): current -" + Calendar.getInstance (). ToString ()); // segundos para agregar en la hora actual int delaySeconds = 5; // prueba usando una fecha real Calendar scheduleDate = Calendar.getInstance (); // establecer una fecha futura considerando la demora en segundos definida // estamos usando este enfoque solo para facilitar las pruebas. // se podría hacer usando una fecha definida por el usuario también scheduleDate.set (scheduleDate.get (Calendar.YEAR), scheduleDate.get (Calendar.MONTH), scheduleDate.get (Calendar.DAY_OF_MONTH), scheduleDate.get (Calendar.HOUR_OF_DAY ), scheduleDate.get (Calendar.MINUTE), scheduleDate.get (Calendar.SECOND) + delaySeconds); Log.d (TAG, "toastAtTime (): programación en -" + scheduleDate.toString ()); long schedule = calculaUptimeMillis (scheduleDate); // publicar Runnable en un momento específico postHandler.postAtTime (new Runnable () @Override public void run () if (callback! = null) callback.get (). showToast ("Toast se llama utilizando 'postAtTime ()'. ");, programado);  / ** * Calcula el @link SystemClock # uptimeMillis () a * una fecha del calendario dada. * / private long calculaUptimeMillis (calendario) long time = calendar.getTimeInMillis (); long currentTime = Calendar.getInstance (). getTimeInMillis (); long diff = time - currentTime; devuelve SystemClock.uptimeMillis () + diff; 
public class RunnableActivity extiende la actividad Implementa WorkerThread.Callback / ** * Callback from @link WorkerThread * Utiliza @link #runOnUiThread (Runnable) para ilustrar * dicho método * / @Override public void showToast (String final String) ( Log.d (TAG, "showToast (" + msg + ")"); runOnUiThread (new Runnable () @Override public void run () Toast.makeText (getApplicationContext (), msg, Toast.LENGTH_LONG) .show ();); 

3. Enviando mensajes con el MensajeActividad Y Trabajador

A continuación, vamos a explorar algunas formas diferentes de usar MensajeActividad  enviar y procesar Mensaje objetos. los MensajeActividad instancia Trabajador, pasando un Entrenador de animales como parámetro los Trabajador tiene algunos métodos públicos con tareas a las que la actividad puede llamar para descargar un mapa de bits, descargar un mapa de bits aleatorio o exhibir un tostada después de algún tiempo retrasado. Los resultados de todas esas operaciones se envían de vuelta a MensajeActividad utilizando Mensaje objetos enviados por el respuestaHandler.

3.1 Preparando el manejador de respuestas desde MensajeActividad

Como en el Ejecutable, en el MensajeActividad tendremos que instanciar e inicializar una Trabajador enviando un Entrenador de animales para recibir datos del hilo de fondo. Sin embargo, esta vez no implementaremos WorkerThread.Callback; en cambio, recibiremos respuestas de la Trabajador exclusivamente por Mensaje objetos.

Como la mayoría de los MensajeActividad y Ejecutable El código es básicamente el mismo, nos concentraremos solo en el uiHandler preparación, que será enviado a Trabajador para recibir mensajes de ella.

En primer lugar, vamos a proporcionar algunos En t Claves para ser utilizadas como identificadores de los objetos de mensaje..

public class MessageActivity extiende la Actividad // Identificador de mensaje utilizado en el campo Message.what () public static final int KEY_MSG_IMAGE = 2; public static final int KEY_MSG_PROGRESS = 3; final estático público int KEY_MSG_TOAST = 4; 

En MessageHandler Implementación, tendremos que ampliar Entrenador de animales e implementar el handleMessage (Mensaje) Método, donde se procesarán todos los mensajes. Fíjate que estamos recogiendo Mensaje para identificar el mensaje, y también estamos obteniendo diferentes tipos de datos de Mensaje.obj. Repasemos rápidamente lo más importante. Mensaje propiedades antes de sumergirse en el código.

  • MensajeEn t identificando el Mensaje
  • Mensaje.arg1En t argumento arbitrario
  • Mensaje.arg2En t argumento arbitrario
  • Mensaje.objObjeto para almacenar diferentes tipos de datos
public class MessageActivity extiende la actividad / ** * El manejador es responsable de gestionar la comunicación * desde @link WorkerThread. Envía Mensajes * de vuelta a @link MessageActivity y maneja * esos Mensajes * / clase pública MessageHandler extiende el Controlador @Override public void handleMessage (Message msg) switch (msg.what) // manejar caso de imagen KEY_MSG_IMAGE:  Bitmap bmp = (Bitmap) msg.obj; myImage.setImageBitmap (bmp); descanso;  // manejar el caso de las llamadas a progressBar KEY_MSG_PROGRESS: if ((boolean) msg.obj) progressBar.setVisibility (View.VISIBLE); else progressBar.setVisibility (View.GONE); descanso;  // maneja el brindis enviado con un caso de retardo de mensaje KEY_MSG_TOAST: String msgText = (String) msg.obj; Toast.makeText (getApplicationContext (), msgText, Toast.LENGTH_LONG) .show (); descanso;  // Controlador que permite la comunicación entre // el WorkerThread y el ActivityHandler MessageHandler protegido;  

3.2 Enviando mensajes con WorkerThread

Ahora volvamos a la Trabajador clase. Agregaremos algo de código para descargar un mapa de bits específico y también código para descargar uno aleatorio. Para llevar a cabo esas tareas, le enviaremos Mensaje objetos de la Trabajador a sí mismo y enviar los resultados de nuevo a MensajeActividad utilizando exactamente la misma lógica aplicada anteriormente para el Ejecutable.

Primero necesitamos extender el Entrenador de animales para procesar los mensajes descargados.

public class WorkerThread extiende HandlerThread // envía y procesa los mensajes de descarga en el WorkerThread private HandlerMsgImgDownloader handlerMsgImgDownloader; / ** * Teclas para identificar las claves de @link Message # what * de los mensajes enviados por @link #handlerMsgImgDownloader * / private final int MSG_DOWNLOAD_IMG = 0; // mensaje que descarga una única img final privada int MSG_DOWNLOAD_RANDOM_IMG = 1; // msg que descarga aleatoriamente img / ** * Controlador responsable de administrar la descarga de la imagen * Envía y administra los mensajes identificando y luego usando * el @link Message # what * @link #MSG_DOWNLOAD_IMG: imagen única * @link #MSG_DOWNLOAD_RANDOM_IMG: imagen aleatoria * / clase privada HandlerMsgImgDownloader extiende Handler private HandlerMsgImgDownloader (Looper looper) super (looper);  @ Anular pública void handleMessage (Mensaje msg) showProgressMSG (true); switch (msg.what) case MSG_DOWNLOAD_IMG: // recibe una única url y la descarga String url = (String) msg.obj; descargarImageMSG (url); descanso;  caso MSG_DOWNLOAD_RANDOM_IMG: // recibe un String [] con múltiples urls // descarga una imagen al azar String [] urls = (String []) msg.obj; Aleatorio aleatorio = nuevo Random (); Cadena url = urls [random.nextInt (urls.length)]; descargarImageMSG (url);  showProgressMSG (falso); 

los descargarImageMSG (url de cadena) El método es básicamente el mismo que el descargarImage (String url) método. La única diferencia es que el primero envía el mapa de bits descargado de nuevo a la interfaz de usuario enviando un mensaje usando respuestaHandler.

public class WorkerThread extiende HandlerThread / ** * Descargue un mapa de bits usando su url y * muéstrelo a la interfaz de usuario. * La única diferencia con @link #downloadImage (String) * es que envía la imagen a la UI * mediante un Mensaje * / private void downloadImageMSG (String urlStr) // Crear una conexión HttpURLConnection connection = null; intente URL url = nueva URL (urlStr); connection = (HttpURLConnection) url.openConnection (); // obtener el flujo de la url InputStream en = new BufferedInputStream (connection.getInputStream ()); Bitmap final bitmap = BitmapFactory.decodeStream (in); if (bitmap! = null) // envía el bitmap descargado y un feedback a la interfaz de usuario loadImageOnUIMSG (bitmap);  catch (IOException e) e.printStackTrace ();  finalmente if (connection! = null) connection.disconnect (); 

los loadImageOnUIMSG (imagen de mapa de bits) es responsable de enviar un mensaje con el mapa de bits descargado a MensajeActividad

 / ** * envía un mapa de bits a la interfaz de usuario * enviando un mensaje al @link #responseHandler * / private void loadImageOnUIMSG (imagen de mapa de bits final) if (checkResponse ()) sendMsgToUI (responseHandler.get (). Obtenir mensaje) MessageActivity.KEY_MSG_IMAGE, imagen));  / ** * Mostrar / Ocultar barra de progreso en la interfaz de usuario. * Utiliza el @link #responseHandler para * enviar un mensaje en la UI * / private void showProgressMSG (show booleano) Log.d (TAG, "showProgressMSG ()"); if (checkResponse ()) sendMsgToUI (responseHandler.get (). obtieneMessage (MessageActivity.KEY_MSG_PROGRESS, show)); 

Tenga en cuenta que en lugar de crear un Mensaje Objeto desde cero, estamos usando el Handler.obtainMessage (int what, Object obj) método para recuperar un Mensaje desde el pool global, ahorrando algunos recursos. También es importante tener en cuenta que estamos llamando a la obtener mensaje () sobre el respuestaHandler, obteniendo un Mensaje asociado con MensajeActividades Looper. Hay dos formas de recuperar un Mensaje del grupo global: Mensaje.obtain () y Handler.obtainMessage ().

Lo único que queda por hacer en la tarea de descarga de imágenes es proporcionar los métodos para enviar un Mensaje a Trabajador para iniciar el proceso de descarga. Fíjate que esta vez llamaremos Message.obtain (Handler handler, int what, Object obj) en handlerMsgImgDownloader, asociando el mensaje con Trabajadorlooper.

 / ** * envía un Mensaje al hilo actual * utilizando el @link #handlerMsgImgDownloader * para descargar una sola imagen. * / public void downloadWithMessage () Log.d (TAG, "downloadWithMessage ()"); showOperationOnUIMSG ("Enviando mensaje ..."); if (handlerMsgImgDownloader == null) handlerMsgImgDownloader = new HandlerMsgImgDownloader (getLooper ()); Mensaje message = Message.obtain (handlerMsgImgDownloader, MSG_DOWNLOAD_IMG, imageBUrl); handlerMsgImgDownloader.sendMessage (mensaje);  / ** * envía un mensaje al hilo actual * utilizando el @link #handlerMsgImgDownloader * para descargar una imagen al azar. * / public void downloadRandomWithMessage () Log.d (TAG, "downloadRandomWithMessage ()"); showOperationOnUIMSG ("Enviando mensaje ..."); if (handlerMsgImgDownloader == null) handlerMsgImgDownloader = new HandlerMsgImgDownloader (getLooper ()); Mensaje message = Message.obtain (handlerMsgImgDownloader, MSG_DOWNLOAD_RANDOM_IMG, imagesUrls); handlerMsgImgDownloader.sendMessage (mensaje); 

Otra posibilidad interesante es el envío. Mensaje Objetos a procesar posteriormente con el comando. Message.sendMessageDelayed (Mensaje msg, long timeMillis).

/ ** * Mostrar un brindis después de un tiempo de retraso. * * envía un mensaje con tiempo de retraso en WorkerThread * y envía un nuevo mensaje a @link MessageActivity * con un texto después de que se procese el mensaje * / public void startMessageDelay () // message delay long delay = 5000; String msgText = "¡Hola desde WorkerThread!"; // Manejador responsable de enviar el mensaje a WorkerThread // usando Handler.Callback () para evitar la necesidad de extender la clase Handler Handler handler = new Handler (new Handler.Callback () @Override public boolean handleMessage (Mensaje del mensaje) responseHandler .get (). sendMessage (responseHandler.get (). obtieneMessage (MessageActivity.KEY_MSG_TOAST, msg.obj)); return true;); // enviando el mensaje handler.sendMessageDelayed (handler.obtainMessage (0, msgText), retraso); 

Creamos un Entrenador de animales Expresamente por enviar el mensaje retrasado. En lugar de extender el Entrenador de animales clase, tomamos la ruta de instanciar una Entrenador de animales usando el Handler.Callback interfaz, para lo cual implementamos el handleMessage (Mensaje msg) Método para procesar el retraso Mensaje.

4. Conclusión

Ya ha visto suficiente código para entender cómo aplicar los conceptos básicos del marco de HaMeR para administrar la concurrencia en Android. Hay otras características interesantes del proyecto final almacenado en GitHub, y le recomiendo que lo revise.. 

Finalmente, tengo algunas consideraciones finales que debes tener en cuenta:

  • No olvides considerar el ciclo de vida de la actividad de Android. Cuando se trabaja con HaMeR y Threads en general. De lo contrario, su aplicación puede fallar cuando el hilo intenta acceder a actividades que se han destruido debido a cambios de configuración o por otros motivos. Una solución común es utilizar un RetenidoFragmento para almacenar el hilo y rellenar el hilo de fondo con la referencia de la actividad cada vez que se destruye la actividad. Eche un vistazo a la solución en el proyecto final en GitHub.
  • Tareas que se ejecutan debido a EjecutableMensaje objetos procesados ​​en Manipuladores no correr asincronicamente. Se ejecutarán de forma síncrona en el subproceso asociado con el controlador. Para hacerlo asíncrono, deberá crear otro hilo, enviar / publicar el Mensaje/Ejecutable objetar en él, y recibir los resultados en el momento adecuado.

Como puede ver, el marco de trabajo de HaMeR tiene muchas posibilidades diferentes y es una solución bastante abierta con muchas opciones para administrar la concurrencia en Android. Estas características pueden ser ventajas sobre AsyncTask, Dependiendo de tus necesidades. Explore más del marco y lea la documentación, y creará grandes cosas con él.

Te veo pronto!