Hilos de ejecución en Android

Cuando se lanza una nueva aplicación el sistema crea un nuevo hilo de ejecución (thread) para esta aplicación conocida como hilo principal. Este hilo es muy importante dado que se encarga de atender los eventos de los distintos componentes. Es decir, este hilo ejecuta los métodos onCreate(), onDraw(), onKeyDown(), … Por esta razón al hilo principal también se le conoce como hilo del interfaz de usuario.

El sistema no crea un hilo independiente cada vez que se crea un nuevo componente. Es decir, todas las actividades y servicios de una aplicación son ejecutados por el hilo principal.

Cuando tu aplicación ha de realizar trabajo intensivo como respuesta a una interacción de usuario, hay que tener cuidado porque es posible que la aplicación no responda de forma adecuada. Por ejemplo, imagina que has de esperar para descargar unos datos de Internet, si lo haces en el hilo de ejecución principal este quedará bloqueado a la espera de que termine la descarga. Por lo tanto, no se podrá redibujar la vista (onDraw()) o atender eventos del usuario (onKeyDown()). Desde el punto de vista del usuario se tendrá la impresión de que la aplicación se ha colgado. Mas todavía, si el hilo principal es bloqueado más de 5 segundos, el sistema mostrará un cuadro de dialogo al usuario “La aplicación no responde” para que el usuario decida si quieres esperar o detener la aplicación.

La solución en estos casos es crear un nuevo hilo de ejecución, para que realice este trabajo intensivo. De esta forma no bloqueamos el hilo principal, que puede seguir atendiendo los eventos de usuario. Es decir, cuando estés implementando un método del hilo principal (Empiezan por on…) nunca realices una acción que pueda bloquear este hilo, como cálculos largos o que requieran esperar mucho tiempo. En estos casos hay que crear un nuevo hilo de ejecución y encomendarle esta tarea. En el siguiente apartado describimos cómo hacerlo.

Las herramientas del interfaz de usuario de Android han sido diseñadas para ser ejecutadas desde un único hilo de ejecución, el hilo principal. Por lo tanto no se permite manipular el interfaz de usuario desde otros hilos de ejecución.  

  poli[Media] Hilos de ejecución en Android

Ejercicio paso a paso: Una tarea que bloquea el hilo principal.

En muchas ocasiones hemos de realizar costosas operaciones o hemos de esperar a que concluyan lentas operaciones en la red. En ambos casos, hay que tener la precaución de no bloquear el hilo principal. De hacerlo el resultado puede ser catastrófico, como se muestra en el siguiente ejercicio.

1. Crea un nuevo proyecto y llámalo Hilos:

2.  Reemplaza el código del layout activity_main por:

<LinearLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical" >
   <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content" >

      <EditText

         android:id="@+id/entrada"
         android:layout_width="0dip"
         android:layout_height="wrap_content"
         android:layout_weight="1"
         android:inputType="numberDecimal"
         android:text="5" >

         <requestFocus />
      </EditText>
      <Button
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:onClick="calcularOperacion"
         android:text="Calcular factorial" />

   </LinearLayout>

   <TextView

      android:id="@+id/salida"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:text=" " 
      android:textAppearance="?android:attr/textAppearanceMedium"/>

</LinearLayout>

Reemplaza el código de MainActivity por el siguiente:

public class MainActivity extends Activity {
       private EditText entrada;
       private TextView salida;


       @Override
       public void onCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
             setContentView(R.layout.activity_main);
             entrada = (EditText) findViewById(R.id.entrada);
             salida = (TextView) findViewById(R.id.salida);
       }

       public void calcularOperacion(View view) {
             int n = Integer.parseInt(entrada.getText().toString());
             salida.append(n +"! = ");
             int res = factorial(n);
             salida.append(res +"\n");
       }

       public int factorial(int n) {
             int res=1;
             for (int i=1; i<=n; i++){
                    res*=1;
                    SystemClock.sleep(1000);
             }

             return res;

       }

}

 

El método calcularOperacion() será llamado cuando se pulse el botón. Comienza obteniendo el valor entero introducido en entrada y muestra la operación a realizar por el TextView salida. Luego, se llama al factorial() y se muestra el resultado.

El método factorial() calcula la operación mátemática factorial de un entero n (n!). Se calcula multiplicando todos los enteros desde uno hasta n. Por ejemplo:

factorial(5) = 5! = 1 · 2 · 3 · 4 · 5 = 120

      Esta operación se calcula con un bucle con la variable i tomando valores de 1 hasta n. Obviamente, esta operación se va computar de forma muy rápida y apenas bloquearemos el hilo principal una milésimas de segundo. Para que aparezca el problema a tratar, vamos a simular que se realizan un gran número de operaciones en cada pasada del bucle. Para esto llamaremos a  SystemClock.sleep(1000) que bloqueará el hilo durante 1000 ms (1 segundos).

4. Ejecuta la aplicación. El resultado ha de ser similar al siguiente:

Observa como mientras se realiza la operación el usuario no puede pulsar el botón ni modificar el EditText. El usuario tendrá la sensación de que la aplicación está bloqueada.

5 Calcula ahora el factorial de 10. Si mientras se calcula tratas de interaccionar con el interfaz de usuasio el sistema nos mostrará en siguiente error:

Mensajes del tipo “La aplicación no responde” son frecuentes en Android. Aparecen cuando el hilo principal se bloquea demasiado tiempo. En el siguiente apartado mostraremos como realizar esta operación de forma correcta.

 

 

Creación de nuevos hilos con la clase Thread

Como acabamos de ver siempre que tengamos que ejecutar un método que requiera bastante tiempo de ejecución, no podremos ejecutarlo en el hilo principal. Dado que este hilo ha de estar siempre disponible para atender los eventos generados por el usuario, nunca debe ser bloqueado. En este apartado aprenderemos a crear nuevos hilos usando la clase de Java Thread. El proceso es muy sencillo, no tendremos más que escribir una clase como la siguiente:

class MiThread extends Thread {
   @Override
   public void run() {
     …
   }
}

El método run() contiene el código que queremos que el hilo ejecute. Para crear un nuevo hilo y ejecutarlo escribiremos:

 MiThread hilo = new MiThread();
hilo.start();

La llamada al método start() ocasionará que se cree un nuevo hilo y se ejecute el método run() en este hilo. La llamada al método start() es asíncrona. Es decir, continuaremos ejecutando las instrucciones siguientes, sin esperar a que el métodorun() concluya. Como veremos más adelante, si espereamos algún resultado de este método, será imprescidible establecer algún mecanismo de sincronización para saber cuando ha terminado.

 

Ejercicio paso a paso: Crear un nuevo hilo con la clase Thread

En este ejercicio ejecutaremos el método factorial() en un hilo nuevo. Además, veremos algunas limitaciones de usar nuevos hilos, como la imposibilidad de acceder al interfaz de usuario. 

  1. Abre el proyecto creado en el ejercicio anterior.
  2. Dentro de MainActivity introduce el siguiente código:
class MiThread extends Thread {
             private int n, res;


             public MiThread(int n) {
                    this.n = n;
             }
       }

Siguiendo el esquema mostrado anteriormente, hemos creado un hilo que en su método run(), llama a factorial() y muestra el resultado por pantalla. Para realizar la operación necesitamos el parámetro de entrada n. Este no puede incorporarse el el método run() dado que ha de ser sobreescrito (@Override) sin alteración alguna. Para resolverlo hemos añadido un constructor a la clase, donde se indica este parámetro.

3. Reemplaza el siguiente método en MainActivity:

public void calcularOperacion(View view) {

             int n = Integer.parseInt(entrada.getText().toString());

             salida.append(n + "! = ");

             MiThread thread = new MiThread(n);

             thread.start();

       }

4. Ejecuta la aplicación. Tras pulsar el botón el resultado ha de ser:

5. Abre la vista LogCat y busca el siguiente error:

Este mensaje indica que solo desde el hilo principal se puede interactuar con las vistas de la interfaz de usuario. NOTA: También está prohibido desde otros hilos usar la clase Toast

Una forma elegante de resolver este problema podría ser usar la clase Callable. Esta  clase de Java nos permite llamar un método en un nuevo hilo, esperar a que este termine y recoger los resultados. No obstante, nuestro objetivo es mostrar  las peculiaridades de Android con el manejo de hilos y vamos a resolver el problema de otra forma.

6. En el método run() reemplaza:

salida.append(res + "\n");

por:

             runOnUiThread(new Runnable() {
                 @Override public void run() {
                           salida.append(res + "\n");
                }
             });

 

 

De esta forma indicamos al sistema que ejecute parte de nuestro código en el hilo principal o hilo del interfaz de usuario.

7. Ejecuta la aplicación y comprueba que el resultado es satisfactorio.

 

 

Introduciendo movimiento en Asteroides

Para que el juego cobre vida será necesario animar todos los gráficos introducidos. En el siguiente ejercicio veremos cómo hacerlo.

Ejercicio paso a paso: Introduciendo movimiento en Asteroides

1.     Comienza declarando las siguientes variables en la clase VistaJuego:

 

// //// THREAD Y TIEMPO //////
// Thread encargado de procesar el juego
private ThreadJuego thread = new ThreadJuego();
// Cada cuanto queremos procesar cambios (ms)
private static int PERIODO_PROCESO = 50;
// Cuando se realizó el último proceso
private long ultimoProceso = 0;

2.     La animación del juego la llevará a cabo el método actualizaFisica() que será ejecutado a intervalos regulares definidos por la constante PERIODO_PROCESO. Esta constante ha sido inicializada a 50 ms. En ultimoProceso se almacena el instante en que se llamó por última vez a actualizaFisica().

3.     Copia el siguiente método dentro de la clase VistaJuego:

protected ahora="System.currentTimeMillis();" cumplido.="" de="" el="" ha="" hagas="" if="" long="" nada="" no="" odo="" periodo_proceso="" proceso="" se="" si="" ultimoproceso="" void=""> ahora) {
             return;
       }
       // Para una ejecución en tiempo real calculamos retardo           
       double retardo = (ahora - ultimoProceso) / PERIODO_PROCESO;
       ultimoProceso = ahora; // Para la próxima vez
       // Actualizamos velocidad y dirección de la nave a partir de 
       // giroNave y aceleracionNave (según la entrada del jugador)
       nave.setAngulo((int) (nave.getAngulo() + giroNave * retardo));
       double nIncX = nave.getIncX() + aceleracionNave *
                            Math.cos(Math.toRadians(nave.getAngulo())) * retardo;
       double nIncY = nave.getIncY() + aceleracionNave * 
                           Math.sin(Math.toRadians(nave.getAngulo())) * retardo;
       // Actualizamos si el módulo de la velocidad no excede el máximo
       if (Math.hypot(nIncX,nIncY) <= MAX_VELOCIDAD_NAVE){
             nave.setIncX(nIncX);
             nave.setIncY(nIncY);
       }
       // Actualizamos posiciones X e Y
       nave.incrementaPos(retardo);
       for (Grafico asteroide : Asteroides) {
             asteroide.incrementaPos(retardo);
       }
}

 

Como veremos a continuación este método será llamado de forma continua. Como queremos desplazar los gráficos cada PERIODO_PROCESO milisegundos, verificamos si ya ha pasado este tiempo desde la última vez que se ejecutó (ultimoProceso).

Como también es posible que el sistema esté ocupado y no nos haya podido llamar hasta un tiempo superior a PERIODO_PROCESO, vamos a calcular el factor de retardo en función del tiempo adicional que haya pasado. Si por ejemplo, desde la última llamada ha pasado dos veces PERIODO_PROCESO, la variableretardo ha de valer 2. Lo que significará que los gráficos han de desplazarse el doble que en circunstancias normales. De esta forma conseguiremos un desplazamiento continuo en tiempo real.

A continuación, se actualizan las variables que controlan la aceleración y cambios de dirección de la nave. Se consiguen por medio de las variables aceleracionNave y giroNave. En el siguiente capítulo modificaremos estas variables para que el jugador pueda pilotar la nave. A partir de estas variables se obtiene una nueva velocidad de la nave, descompuesta en sus componentes x e y. Si el módulo de estos componentes es mayor que la velocidad máxima permitida, no se actualizará la velocidad.

Finalmente se actualizan las posiciones de todos los gráficos (nave y asteroides) a partir de sus velocidades. Esto se consigue llamando al método incrementaPos() definido en la clase Grafico.

4.     Ahora necesitamos que esta función sea llamada continuamente, para lo que utilizaremos un Thread. Crea la siguiente clase dentro de la clase VistaJuego:

class ThreadJuego extends Thread {
   @Override
   public void run() {
          while (true) {
                 actualizaFisica();
          }
   }
}

5.     Introduce estas líneas al final del método onSizeChanged():

ultimoProceso = System.currentTimeMillis();
thread.start();

Esto ocasionará que se llame al método run() del hilo de ejecución. Este método es un bucle infinito que continuamente llama al actualizaFisica().

6.     Ejecuta la aplicación y observa como el juego cobra vida.

 

El trabajo con hilos de ejecución es especialmente delicado. Como veremos en próximos capítulos, este código nos va a ocasionar varios quebraderos de cabeza. Un problema es que seguirá ejecutándose aunque nuestra aplicación esté en segundo plano. Veremos cómo detener el hilo de ejecución, cuando estudiemos el ciclo de vida de una actividad. (NOTA: Si ejecutas el programa en el terminal real y detectas que este funciona más lentamente, puede ser buena idea detener la aplicación). Un segundo problema, aparecerá cuando dos hilos de ejecución traten de acceder a la misma variable a la vez. También se resolverá más adelante.

 

Ejercicio paso a paso: Introduciendo secciones críticas en Java (synchronized)

 

Cuando se realiza una aplicación que ejecuta varios hilos de ejecución hay que prestar un especial cuidado a que ambos hilos pueden acceder de forma simultánea a los datos. Cuando se limitan a leer las variables, no suele haber problemas. El problema a parece cuando un hilo esté modificando algún dato y justo en este instante se pasa a ejecutar un segundo hilo que ha de leer estos datos. Este segundo hilo va a encontrar unos datos a mitad de modificar, lo que posiblemente cause errores en su interpretación. El método más común para evitar que dos hilos accedan al mismo tiempo a un recurso es el de la exclusión mutua. En Java que se consigue utilizando la palabra reservada synchronized.

 

1.   Introduce la palabra reservada synchronized delante del método onDraw() y actualizaFisica(). De esta forma se evita que cuando actualizaFisica() esté modificando alguno de los valores de la nave o los asteroides en método onDraw() acceda a estos valores.

Nota sobre Java: La palabra clave synchronized permite definir una sección crítica en Java. Expliquemos en qué consiste: Cada vez que un hilo de ejecución (thread) entra en un método o bloque de instrucciones marcado con synchronized se pregunta al objeto si ya hay algún otro thread que haya entrado en la sección crítica de ese objeto. La sección crítica está formada por todos los bloques de instrucciones marcados con synchronized. Si nadie ha entrado en la sección crítica, se entrará normalmente. Si ya hay otro thread dentro, entonces el thread actual es suspendido y ha de esperar hasta que la sección crítica quede libere. Esto ocurrirá cuando el thread que está dentro de la sección crítica salga.

Dos matizaciones importantes: La primera es que la sección crítica se define a nivel de objeto no de clase. Es decir, cada objeto instanciado no influyen en las secciones críticas de otros objetos. En segundo lugar, solo se define una sección crítica por clase. Aunque se haya utilizado synchronized en varios métodos, realmente solo hay una sección crítica.