Android
14.HILOS Y CONTROLADORES
Página 16 de 25
14. HILOS Y CONTROLADORES
14.1. Ejecuciones en background con Thread
En Java, y también en Android, podemos ejecutar varias secciones de un programa en paralelo. Cada uno de estos procesos se denomina thread o hilo, y se ejecuta en background con una prioridad determinada bajo el control de un objeto de la clase Thread de Java. Generalmente sólo hay un hilo asociado a una actividad, el hilo principal, consistente en una serie de instrucciones o sentencias que se ejecutan secuencialmente.
Una forma de crear un hilo es definir una clase que extiende a la clase Thread. La nueva clase debe sobrescribir el método run(), que se ejecutará cuando se inicie el hilo:
class Hilo extends Thread{
// ...
@Override
public void run(){
// instrucciones a ejecutar
}
// ...
}
Para comenzar la ejecución del hilo se instancia un objeto de la clase Hilo y se ejecuta el método start():
Hilo hilo1 = new Hilo();
hilo1.start()
Podemos definir varios hilos y ejecutarlos todos al mismo tiempo con distintas prioridades.
La vista de la pantalla sólo se puede modificar desde el hilo principal. Los procesos que se ejecutan en un hilo no tienen permitido modificar los objetos de tipo View, por ejemplo un TextView. Si lo intentamos, el programa se compilará, pero dará un error durante la ejecución. Si queremos mostrar en pantalla los datos o gráficos generados en un hilo podemos utilizar un controlador (handler), que es un objeto de la clase Handler. Un controlador residente en el hilo principal puede recibir mensajes de otro hilo y ejecutar instrucciones en consecuencia, por ejemplo, escribir en pantalla.
Para enviar un mensaje a un controlador handler desde un hilo:
• Se obtiene el mensaje asociado al controlador
Message msg=handler.obtainMessage();
• Se construye un objeto de tipo Bundle para empaquetar los datos
Bundle b =new Bundle();
• Insertamos los datos en el bundle mediante parejas ("etiqueta", dato), por ejemplo para insertar un entero y una cadena en el bundle
b.putInt("etiqueta1",entero);
b.putString(``etiqueta2", cadena);
• Finalmente se inserta el bundle en el mensaje y se envia al controlador
msg.setData(b);
handler.sendMessage(msg);
En cuanto se envia un mensaje al controlador, el sistema ejecuta el método handler.handleMessage() de la clase Handler, que se habrá reescrito para aceptar el mensaje enviado y ejecutar otras instrucciones. Hay que definir el controlador handler en el hilo principal:
Handler handler= new Controlador();
donde la clase Controlador extiende la clase Handler y sobreescribe (Override) el método handleMessage():
class Controlador extends Handler{
@Override
public void handleMessage(Message msg){
int entero=msg.getData().getInt("etiqueta1");
String cadena=msg.getData().getString("etiqueta2");
// otras instrucciones....
}
}
Para leer el contenido del mensaje se usa el método msg.getData(), que extrae el objeto bundle, de donde podemos leer los datos usando getInt("etiqueta1") y getString("etiqueta2"). A continuación, podemos ejecutar otras instrucciones que se realizarán en el hilo principal.
Todo esto se pone en claro en el siguiente ejemplo. Ejecutamos dos hilos simultáneos, que envían mensajes a un controlador, que los muestra en pantalla. Cada hilo cuenta del 1 al 10 con un retraso temporal expresado en milisegundos. Para este ejemplo utilizamos la siguiente interfaz de usuario definida en el fichero main.xml.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="#ffffbb"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:textColor="#550000"
android:textSize="25sp"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/hello"
android:id="@+id/texto1"
/>
</LinearLayout
La actividad Hilos.java es la siguiente:
package es.ugr.amaro;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.TextView;
public class Hilos extends Activity {
Handler handler=new Controlador();
TextView texto1,texto2;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
texto1=(TextView) findViewById(R.id.texto1);
Hilo hilo1=new Hilo(10,100);
Hilo hilo2=new Hilo(5,200);
hilo2.setPriority(7);
hilo1.start();
hilo2.start();
}
class Hilo extends Thread{
int maximo,tiempo;
Hilo(int n,int t){
maximo=n;
tiempo=t;
}
@Override
public void run(){
for (int i=0;i<=maximo;i++){
try{
Thread.sleep(tiempo);
}
catch (InterruptedException e){ ; }
// Construye el mensaje
// para enviar al controlador handler
Message msg=handler.obtainMessage();
// inserta datos en el mensaje
//enpaquetandolos en un bundle
Bundle b =new Bundle();
b.putInt("total",i);
b.putString("thread", currentThread().toString());
msg.setData(b);
// envia el mensaje
handler.sendMessage(msg);
}
}
}
// Controlador para recibir mensajes del hilo
class Controlador extends Handler{
@Override
public void handleMessage(Message msg){
int total;
// recibe los datos enviados en el mensaje msg
total=msg.getData().getInt("total");
String thread=msg.getData().getString("thread");
texto1.append("\n"+total+" "+thread);
}
}
}
Figura 14.1. Dos hilos cuentan del 1 al 10 y un controlador
va mostrando el resultado en la pantalla.
El resultado se ve en la figura 14.1. En la primera columna escribimos el entero i, a medida que el controlador lo va recibiendo y, en la segunda, el hilo del que proviene. Hemos usado el método currentThread().toString() que devuelve información sobre el hilo actual y lo almacena en una cadena. La prioridad del segundo hilo la hemos modificado con hilo2.setPriority(7). La prioridad por defecto de un hilo es 5. El valor de i se va incrementando en cada hilo lentamente debido al retraso, expresado en microsegundos mediante la instrucción Thread.sleep(tiempo).
14.2. Diálogos de progreso
La técnica anterior se puede aplicar para mostrar una barra o diálogo de progreso, indicando que se está ejecutando un proceso en background. La barra de progreso es un tipo de diálogo: un objeto de la clase ProgressDialog, que extiende a Dialog, y es una actividad flotante. El tipo de diálogo se indica mediante setProgressStyle(estilo), donde estilo puede ser 0 (spinner) ó 1 (horizontal). Se inicia mediante showDialog(int id), donde estilo puede ser 0 (spinner) ó 1 (horizontal). Esto hace que se ejecuten los métodos onCreateDialog() y onPrepareDialog() de la clase Activity, que deben ser redefinidos (@Override) en nuestra actividad para que la barra de progreso se muestre de acuerdo con nuestras necesidades. El método onCreateDialog() sólo se ejecuta la primera vez que mostramos el diálogo. Si lo mostramos sucesivas veces, sólo se ejecutará el método onPrepareDialog().
En el ejemplo que viene a continuación se abre una barra de progreso de tipo horizontal. Utilizaremos la interfaz de usuario definida en el siguiente fichero main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="#ffffff"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:textColor="#000000"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/hello"
android:id="@+id/texto"
/>
<Button android:text="Empezar"
android:id="@+id/button1"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
</Button>
</LinearLayout>
Nuestra actividad viene en el siguiente programa BarraDeProgreso.java:
package es.ugr.amaro;
import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
public class BarraDeProgreso extends Activity implements
OnClickListener{
ProgressDialog progreso;
Controlador handler=new Controlador();
int maximo=100;
int delay=100;
int estilo=1;
TextView texto;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button boton= (Button) findViewById(R.id.button1);
boton.setOnClickListener(this);
texto=(TextView) findViewById(R.id.texto);
}
@Override
public void onClick(View v) {
showDialog(0);
}
// Crea una barra de progeso
@Override
public Dialog onCreateDialog(int id) {
progreso = new ProgressDialog(this);
progreso.setProgressStyle(estilo);
progreso.setMessage("");
return progreso;
}
// Prepara la barra de progeso
@Override
public void onPrepareDialog(int id,Dialog dialog) {
progreso=(ProgressDialog)dialog;
progreso.setProgressStyle(estilo);
progreso.setMax(maximo);
progreso.setProgress(0);
progreso.setMessage(
"Ejecutando hilo en background...");
Hilo thread=new Hilo();
thread.start();
}
class Controlador extends Handler{
@Override
public void handleMessage(Message msg){
int total=msg.getData().getInt("total");
progreso.setProgress(total);
texto.setText("Total "+total+" Maximo: "+maximo);
if(total==maximo){
removeDialog(0);
}
}
}
class Hilo extends Thread{
@Override
public void run(){
for (int i=0;i<=maximo;i++){
try{
Thread.sleep(delay);
} catch (InterruptedException e){;}
Message msg=handler.obtainMessage();
Bundle b =new Bundle();
b.putInt("total",i);
msg.setData(b);
handler.sendMessage(msg);
}
}
}
}
Vemos el resultado en la figura 14.2 (izquierda). Pulsando el botón se inicia el diálogo, ejecutándose los métodos onCreateDialog() y onPrepareDialog(). En éste se inicia el proceso thread de la clase Hilo, que consiste en un ciclo que cuenta del uno al cien con un retraso de 100 milisegundos. Se envía un mensaje al controlador con la variable incremental del ciclo. En cada paso del ciclo el controlador actualiza el valor mostrado en la barra de progreso mediante progreso.setProgress(total). Cuando termina el ciclo (total=maximo), el controlador finaliza el diálogo con removeDialog(0). Entonces se devuelve el control al hilo principal y se puede repetir el proceso pulsando de nuevo el botón, en cuyo caso se ejecuta solamente onPrepareDialog().
Si cambiamos en este programa la variable estilo=0, y comentamos la instrucción progreso.setMax(maximo) tendremos un diálogo de progreso circular adecuado al caso en que no podamos predecir cuándo finalizará el hilo secundario. El resultado se ve en la figura 14.2 (derecha).
Figura 14.2. Diálogos de Progreso Horizontal y Giratorio.
14.3. Interfaz Runnable
Existe otro procedimiento alternativo para crear un hilo. Consiste en definir una clase que implemente la interfaz Runnable. Esto requiere definir el método run(), que se ejecutará al iniciarse el hilo mediante:
Thread hilo= new Thread(runnable};
hilo.start();
donde runnable es un objeto de la clase que implementa la interfaz Runnable. La ventaja de este procedimiento en lugar de extender la clase Thread, como hemos hecho en este capítulo, es que cualquier clase puede implementar un interfaz. En particular puede ser una clase que a la vez extienda a la clase View y que podemos insertar en un layout. Así podemos tener varios objetos de tipo View en la pantalla, cada uno asociado a instrucciones que se están ejecutando en su propio hilo. Otra ventaja es que no es necesario utilizar un controlador, pues el contenido de estos objetos View puede ser actualizada continuamente usando el método postInvalidate() de la clase View. Este método hace que se vuelva a dibujar el contenido de un objeto View, ejecutándose el método onDraw()(en este caso no se puede usar invalidate() para dibujar de nuevo, porque este método sólo se puede utilizar en el hilo principal y no en hilos secundarios.)
En el siguiente ejemplo usamos esta técnica para actualizar dos contadores en dos hilos independientes. Cada hilo consiste en un objeto de la clase TextoAnimado que extiende a View y que hemos definido como clase interna. Cada hilo tiene un contador cuyo valor se incrementa con un retraso y se muestra en pantalla. Estos objetos View se añadirán al layout definido en el siguiente fichero main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="#ffffff"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/layout"
>
<TextView
android:id="@+id/texto"
android:textColor="#000000"
android:textSize="35sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Interfaz Runnable"
/>
</LinearLayout>
El programa Java, donde definimos la actividad RunnableEjemplo, es el siguiente. Los dos contadores son los elementos de un array. Los parámetros de los objetos TextoAnimado son, además de la clase actual this, el retraso temporal y el índice del contador (0,1).
package es.ugr.amaro.runnableejemplo;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.LinearLayout;
public class RunnableEjemplo extends Activity {
boolean continuar=true;
String mensaje="";
int[] contador={0,0};
TextoAnimado texto,texto2;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
LinearLayout ll=(LinearLayout)
findViewById(R.id.layout);
LayoutParams params=new LayoutParams(600,100);
texto= new TextoAnimado(this,40,0);
texto.setLayoutParams(params);
ll.addView(texto);
texto2= new TextoAnimado(this,200,1);
texto2.setLayoutParams(params);
texto2.setBackgroundColor(Color.YELLOW);
ll.addView(texto2);
Thread hilo= new Thread(texto);
hilo.start();
Thread hilo2= new Thread(texto2);
hilo2.start();
}
class TextoAnimado extends View implements Runnable{
int retraso;
int i;
Paint paint=new Paint();
public TextoAnimado(Context context,int retraso,int i) {
super(context);
this.retraso=retraso;
this.i=i;
paint.setColor(Color.BLACK);
paint.setTextSize(30);
paint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
canvas.drawText(mensaje,50,50,paint);
}
public void run() {
while(continuar){
try{Thread.sleep(retraso);}
catch(InterruptedException e){;}
contador[i]++;
mensaje="Contador: "+contador[i];
postInvalidate();
}
}
}
}
Figura 14.3. Dos hilos en dos objetos View implementando la inerfaz Runnable.
El resultado se ve en la figura 14.3. Los dos contadores correspondientes a los índices i=0, 1 del array contador[], avanzan de uno en uno con un retardo de 40 y 200 milisegundos, respectivamente.
Si se ejecuta esta aplicación y se observa detenidamente, se verá que ocasionalmente alguno de los contadores (con más frecuencia el segundo) muestra el resultado correspondiente al otro contador, pero inmediatamente después continúa con su propio contaje. Esto se debe a que el texto a escribir, almacenado en la variable de la clase principal mensaje está siendo compartido por los dos hilos, que están ejecutándose simultaneamente. Entonces puede ocurrir que inmediatamente antes de que en un hilo se escriba la variable mensaje, el otro hilo modifique súbitamente su contenido. Este ejemplo ilustra el tipo de interacciones que pueden producirse al trabajar con varios hilos simultáneamente. La solución en este caso es trasladar la declaración de la variable:
String mensaje="";
a la clase TextoAnimado, en cuyo caso cada hilo modificaría su propia copia de la variable mensaje.
14.4. Notificaciones
La barra de estado, que aparece en la parte superior de la pantalla, puede mostrar las notificaciones que envían las aplicaciones que se ejecutan en background. Esto permite lanzar a ejecutar un hilo desde una actividad, pasar a otra actividad y comprobar si sigue ejecutándose. La notificación que envía el hilo consiste en un mensaje de texto que se puede examinar en la vista expandida de notificaciones, que se despliega tirando hacia abajo de la barra de estado.
Ilustraremos el uso de notificaciones creando una actividad que usa la siguiente interfaz de usuario en main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#ffffff"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Ejemplo de notificaciones desde un hilo"
android:textSize="20sp"
android:textColor="#000000"
/>
<ImageView android:layout_width="wrap_content"
android:src="@drawable/icon"
android:id="@+id/imageView1"
android:layout_height="wrap_content">
</ImageView>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Obsérvese este icono a la derecha de la
barra de estado"
android:textSize="20sp"
android:textColor="#000000"
/>
</LinearLayout>
Figura 14.4. Notificaciones desde un hilo ejecutándose en background. Izquierda:
Obsérvese el icono de notificación en la barra de estado. Derecha: Notificación
detallada mostrando el mensaje enviado por el hilo.
El fichero Java de la actividad es el siguiente. El proceso de creación de la notificación requiere seguir varios pasos que están indicados con comentarios en el listado. En concreto:
• Instanciar una referencia al manager de notificaciones, que es un objeto de la clase NotificationManager.
• Crear la notificación, un objeto de la clase Notification, que incluye un icono, un texto a mostrar al inicio y una fecha que no tiene otra utilidad que para ordenar las distintas notificaciones del sistema cronológicamente.
• Definir un Intent, para acceder a nuestra aplicación desde la vista extendida de notificaciones y un PendingIntent. El PendingIntent contiene la información del intent y concede permisos para ser utilizado desde otra actividad externa.
• Actualiza el texto de la notificación expandida, actualiza la notificación con setLatestEventInfo() y la notifica al manager con notify().
En este ejemplo, la actualización y notificación se ha incluido en un hilo que incrementa un contador con un retraso. El texto de la notificación incluye el valor del contador.
Figura 14.5. La notificación persiste en la barra de estado cuando cerramos
la actividad pulsando la tecla back y volviendo a la pantalla home,
ya que el hilo sigue ejecutándose en background.
package es.ugr.amaro.notificaciones;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
public class Notificaciones extends Activity {
boolean continuar=true;
int retraso=100;
int contador=0;
// variables para la notificacion
NotificationManager manager;
Notification notificacion;
CharSequence notificacionTitle = "Hilo funcionando";
CharSequence notificacionText = "";
Context context;
Intent notificacionIntent;
PendingIntent pendingIntent;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// referencia al manager de notificaciones
String servicio=Context.NOTIFICATION_SERVICE;
manager=(NotificationManager)getSystemService(
servicio);
// crea la notificacion
int icon=R.drawable.icon;
String texto="ejecutando";
long cuando=System.currentTimeMillis();
notificacion=new Notification(icon,texto,cuando);
// define el intent de la notificacion expandida
context = getApplicationContext();
notificacionIntent = new Intent(this,
Notificaciones.class);
pendingIntent = PendingIntent.getActivity(this,
0, notificacionIntent, 0);
// inicia un nuevo hilo
Contar contar=new Contar();
Thread hilo = new Thread(contar);
hilo.start();
}
// clase para ejecutar un hilo que manda una notificación class Contar implements Runnable{
public void run() {
while(continuar){
try { Thread.sleep( retraso ); }
catch ( InterruptedException e ){ ; }
contador++;
//texto de la notificacion expandida
notificacionText="contador="+contador;
//actualiza la notificacion
notificacion.setLatestEventInfo(context,
notificacionTitle,
notificacionText,
pendingIntent);
// comunica la notificacion al manager
int ref=1;
manager.notify(ref,notificacion);
}
}
}
}
El resultado se ve en la figura 14.4. Al ejecutar nuestra aplicación aparece en la barra de estado el icono de Android icon.png, indicando que la notificación está activa. Si desplegamos la barra de estado, tirando de ella hacia abajo, veremos que se muestra el mensaje de la notificación. Si pulsamos la tecla back o Home y salimos de nuestra aplicación, figura 14.5. vemos que la barra de estado continúa mostrando el icono de la notificación. Esto nos confirma que el hilo sigue ejecutándose en background. Si abrimos la vista detallada de notificaciones y pulsamos en la notificación, se abre de nuevo nuestra actividad (es aquí donde el pendingIntent está siendo utilizado).
Puede haber más de una notificación proveniente de distintos hilos o del mismo. El número de referencia ref=1 en la notificación al manager permite controlar qué notificación se está actualizando. Por ejemplo, si al final del método run() añadimos la línea:
manager.notify(2,notificacion);
estamos notificando al manager una segunda notificación. Al ejecutar nuestra aplicación ahora se verán dos iconos en la barra de estado y dos notificaciones en la vista detallada, figura 14.6.
Figura 14.6. Dos notificaciones en la
barra de estado y en la vista detallada de notificaciones.