Un servicio en un nuevo hilo con IntentService

A la hora de diseñar aplicaciones en Android hay que tener muy en cuenta que todos los componentes (actividades, servicios y receptores de anuncios) se van a ejecutar en el hilo principal de la aplicación. Dado que este hilo ha de estar siempre disponible para atender los eventos generados por el usuario, nunca debe ser bloqueado. Es decir cualquier proceso que requiera un tiempo importante no ha de ser ejecutado desde este hilo. En su lugar hay que crear un nuevo hilo para que realice este proceso y así dejar libre al hilo principal para que este pueda seguir procesando nuevos eventos.

Podemos crear un nuevo hilo utilizando la clase estándar de Java Thread, tal y como se explicó en el capítulo 5. Para automatizar este proceso, Android nos proporcina la clase AsyncTask. Tambien nos proporciona la clase IntentService, cuando queramos lanzar un servicio en un nuevo hilo. En este apartado veremos que ocurre cuando un servicio bloquea el hilo principal y como solucionarlo mediante la clase IntentService.

Ejercicio paso a paso: Un servicio que bloquea el hilo principal

Muchos servicios han de realizar costosas operaciones o han 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 este ejercicio. 

1.     Crea un nuevo proyecto con los siguientes datos:

Project name: IntentService

Package name: com.example.intentservice

Create Activity: MainActivity

Min SDK Version: 7

2.     Reemplaza el código del layout principal 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="2.2" >

         <requestFocus/>

      </EditText>

      <Button

         android:layout_width="wrap_content"

         android:layout_height="wrap_content"

         android:onClick="calcularOperacion"

         android:text="Calcular operación"/>

   </LinearLayout>

   <TextView

      android:id="@+id/salida"

      android:layout_width="match_parent"

      android:layout_height="match_parent"

      android:text=" "

      android:textAppearance="?android:attr/textAppearanceMedium"/>

</LinearLayout>

 

3.     Reemplaza el código de MainActivity por el siguiente:

public clas sMainActivity extends Activity {

       private EditText entrada;

       public staticTextView 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) {

             double n = Double.parseDouble(entrada.getText().toString());

             salida.append(n +"^2 = ");

             Intent i = new Intent(this, ServicioOperacion.class);

              i.putExtra("numero", n);

             startService(i);

       }

}
 

Observa como la variable salida ha sido declarada como public static. Esto nos permitirá acceder a esta variable desde otras clases. El método calcularOperacion() será llamado cuando se pulse el botón. Comienza obteniendo el valor real introducido en entrada. Se muestra la operación a realizar por salida. Luego, se crea una nueva intención con nuestro contexto y la clase que acabamos de crear. A continuación y se le añade un extra con el valor introducido. Finalmente, se arranca el servicio.

4.     Crea la clase ServicioOperacion con el siguiente código:

public class ServicioOperacion extends Service {

       @Override

       public int onStartCommand(Intent i, int flags, int idArranque){

             doublen = i.getExtras().getDouble("numero");

             SystemClock.sleep(5000);

             MainActivity.salida.append(n*n + "\n");

             return START_STICKY;

       }

 

       @Override

       public IBinder onBind(Intent arg0) {

             return null;

Cuando se arranca el servicio se llamará al método onStartCommand. Este comienza obteniendo el valor a calcular a partir de un extra. Luego vamos a simular que se realizan un gran número de operaciones para ello vamos a bloquear el hilo durante 5000 ms (5 segundos) utilizando el método sleep. Una vez terminado el resultado se muestra directamente en elTextView salida. Esta forma de trabajar no resulta muy recomendable. Se ha realizado así, para ilustrar como es posible acceder al sistema gráfico de Android dado que estamos en el hilo principal. Finalmente, devolvemos START_NOT_STICKY para indicar al sistema que si por fuerza mayor ha de destruir el servicio, no hace falta que lo vuelva a crear.

5.     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.

6.     Modifica el tiempo de retardo para que este sean 25 seg. (sleep(25000)). Ejecuta de nuevo la aplicación y observa como el sistema nos mostrará en siguiente error:

Para que esto no bloquee el hilo principal podemos utilizar un IntentService. En el siguiente ejercicio mostraremos como realizarlo.

 

La clase intentService

Utilizaremos la clase IntentService en lugar de Service cuando queramos un servicio que se ejecute en su propio hilo. Esta clase tiene un constructor donde hay que indicar el nombre que queremos dar al servicio. Lo habitual va  a ser que cuando extendamos esta clase en el constructor llamemos al constructor padre pasándole este nombre. A continuación se muestra un ejemplo de código:

public class MiServicio extends IntentService{

    public MiServicio () {

          super "Nombre de mi servicio");

    }

    @Override

    protected void onHandleIntent(Intent intencion) {

           ...

    }

}


El siguiente método que hay que sobrescribir es onHandleIntent. Este método será lanzado cada vez que se arranque el servicio, pero en este caso se lanzará en hilo nuevo. A través del parámetro intención se podrán enviar datos en forma de extras. Es importante destacar que si se lanzan varias peticiones de servicio estas serán  puestas en una cola. Se irán atendiendo una tras otra sin que haya dos a la vez en ejecución. Este comportamiento puede ser interesante para algunas tareas, pero no para otras. Por ejemplo, si tenemos un servicio que descarga una foto de nuestro catálogo, seguramente sería más interesante hacerlo a medida que se piden y no de una en una. 

Finalmente para lanzar un IntentService hay que usar startService(). Se realiza exactamente igual que para lanzar un Service.

Ejercicio paso a paso: Un servicio en su  propio hilo 

En este ejercicio aprenderemos a crear servicios que se ejecutan en un hilo de ejecución diferente al principal utilizando la clase IntentService. Además veremos algunas limitaciones de este tipo de servicios, como la imposibilidad de acceder al sistema gráfico.

 

1.      Abre el proyecto IntentService creado en el ejercicio anterior.

2.      Crea la clase IntentServiceOperacion con el siguiente código:

public class IntentServiceOperacion extends IntentService{

   public IntentServiceOperacion() {

          super("IntentServiceOperacion");

   }

 

   @Override

   protected void onHandleIntent(Intent intent) {

          double n = intent.getExtras().getDouble("numero");

          SystemClock.sleep(5000);

          MainActivity.salida.append(n*n + "\n");       

   }

}

 

3.     Abre MainActivityy modifica la línea:

             Intent i = new Intent(this, ServicioOperacion.class);

por:

             Intent i = new Intent(this, IntentServiceOperacion.class);
 

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:

Te indica que solo desde el hilo principal se va a poder interactuar con las vistas del interfaz de usuario. También está prohibido desde otros hilos usar la clase Toast.

Como un hilo que hemos creado pertenece al mismo proceso que el hilo principal, compartimos con este todas las variables. Para devolver el valor calculado, podríamos implementar un método o variable público, tanto en la clase del servicio, como de la actividad. No obstante, vamos a resolver este problema utilizando un mecanismo más elegante, los receptores de anuncios. Se explica en el siguiente apartado.