martes, 14 de octubre de 2014

Procesos y hebras

Cuando una aplicación se ejecuta por primera vez, a través de alguno de sus componentes, el sistema creará para ella un proceso y un flujo de ejecución principal. Por defecto, todos los componentes de una aplicación se ejecutan en el mismo proceso y hebra principal. Sin embargo, si ello fuera necesario, podremos ejecutar los componentes de nuestra aplicación en procesos distintos y dentro de estos, crear las hebras secundarias que consideremos oportunas.

Procesos.

Por defecto, todos los componentes de una aplicación se ejecutan dentro del mismo proceso. Este comportamiento será suficiente para la mayoría de las aplicaciones. No obstante, si necesitamos cambiar este funcionamiento, podremos hacerlo desde el archivo manifest.

Todos los elementos asociados a componentes del archivo manifest: <activity>, <service>, <receiver> y <provider>, soportan el atributo android:process. A través de este atributo podremos especificar el proceso en el que queremos que sea ejecutado el componente. Incluso podríamos utilizar el mismo proceso para componentes de aplicaciones diferentes. Para este último caso, las aplicaciones deberán ejecutarse con el mismo usuario y estar firmadas con el mismo certificado.

El elemento <application> también cuenta con el atributo android:process que podríamos usar para definir el proceso por defecto para todos los componentes de una aplicación.

Hay momentos en los que el sistema se verá obligado a detener ciertos procesos para obtener los recursos requeridos por procesos con mayor prioridad. Cuando un proceso es eliminado por el sistema, todos sus componentes de aplicación serán destruidos.

A la hora de eliminar procesos, el sistema sopesa la importancia de éstos de cara al usuario. Por ejemplo, procesos con actividades que llevan un tiempo considerable siendo no visibles, tendrán un menor peso que aquellos procesos cuyas actividades sean visibles. La decisión de cuándo eliminar un proceso viene determinada por el estado de sus componentes.

Ciclo de vida de un proceso.

El sistema tratará de mantener vivo el proceso de una aplicación el mayor tiempo posible. Habrá momentos en los que tendrá que eliminar los procesos más antiguos para liberar la memoria demandada por procesos más recientes. Para decidir qué procesos se mantienen y cuáles no, se otorga a cada proceso una prioridad en base al estado en el que se encuentran sus componentes. Los procesos con menor prioridad serán los primeros en ser eliminados.

Hay cinco niveles de prioridad:
  1. Proceso en primer plano.
    Se trata del proceso de mayor prioridad y será el último en ser eliminado por el sistema caso de ser necesario. Se trata de un proceso que está siendo usado por el usuario. Un proceso se encuentra en primer plano si se cumple alguna de las siguientes condiciones:
    • Contiene una actividad con la que el usuario está interactuando (el sistema ha invocado su método onResume()).
    • Contiene un servicio enlazado desde una actividad con la que el usuario está interactuando.
    • Contiene un servicio que se está ejecutando en primer plano (se invocó a través del método startForeground()).
    • Contiene un servicio en el que se está ejecutando alguno de los siguientes métodos de ciclo de vida: onCreate(), onStart() u onDestroy().
    • Contiene un receptor de avisos BroadcastReceiver ejecutando el método onReceive().

    Generalmente solo hay unos pocos procesos en primer plano. Estos procesos serán eliminados como medida de emergencia solo si la memoria disponible no es suficiente para el propio funcionamiento del sistema.

  2. Proceso visible. Se trata de un proceso que no tiene componentes en primer plano pero que aún es visible al usuario. Un proceso se considera visible si se cumple alguna de las siguientes condiciones:
    • Contiene una actividad que no está en primer plano pero que es parcialmente visible, es decir, que se encuentra pausada. Esto ocurre, por ejemplo, cuando se nos muestra un cuadro de diálogo desde una actividad quedando ésta parcialmente visible.
    • Contiene un servicio enlazado a alguna actividad que se encuentra parcialmente visible o en primer plano.

    Un proceso de este tipo se considera bastante importante y no será eliminado por el sistema a menos que quede comprometida la supervivencia de algún proceso en primer plano.

  3. Proceso de servicio. Se trata de un proceso asociado a un servicio que ha sido iniciado con el método startService(). Aunque un servicio no está relacionado con lo que el usuario puede ver en pantalla, suelen realizar operaciones sobre las que el usuario presta su atención: reproducir música en segundo plano o descargar datos de Internet, por ejemplo.

    El sistema mantendrá activos este tipo de procesos mientras los procesos en primer plano y visibles no se vean comprometidos.

  4. Proceso en segundo plano. Se trata de un proceso con actividades no visibles al usuario, es decir, que se encuentran detenidas. Estos procesos no tienen un impacto directo en la experiencia con el usuario y el sistema los eliminará si algún proceso con mayor prioridad lo requiere. Como norma general, dispondremos de muchos procesos ejecutándose en segundo plano por lo que se utiliza una lista LRU (Least Recently Used - Menos Usado Recientemente) para asegurar que los procesos más antiguos sean los primeros en ser eliminados. Si una actividad implementa los métodos asociados a su ciclo de vida de manera correcta y es capaz de guardar su estado, eliminar su proceso no debería afectar a la experiencia con el usuario porque cuando el usuario vuelve a la actividad, ésta será capaz de recuperar su estado original.

  5. Proceso vacío. Se trata de un proceso especial sin componentes. Estos procesos almacenan datos con el objetivo de mejorar el rendimiento durante la carga de componentes.

Android promociona a un proceso a la prioridad más alta que puede basándose en la importancia de los componentes activos que contiene. Por ejemplo, si un proceso contiene un servicio ya iniciado y además una actividad parcialmente visible, el proceso es catalogado como proceso visible y no como proceso de servicio.

Por otro lado, la prioridad de un proceso vendrá dada también por las dependencias que puedan existir con el resto de procesos. Un proceso servidor no podrá tener menor prioridad que sus procesos cliente. Por ejemplo, si un proveedor de contenido en un proceso A (servidor) sirve a un proceso B (cliente), o si un servicio del proceso A (servidor) es enlazado desde un componente del proceso B (cliente), la prioridad del proceso A será como mínimo la prioridad del proceso B, nunca inferior.

Debido a que un proceso que ejecuta un servicio tiene mayor prioridad que un proceso con actividades en segundo plano, una actividad que ejecuta una operación costosa en tiempo debería delegar su procesamiento en un servicio, además, debería de crear una hebra separada de la principal para que dicho procesamiento vaya más allá de su propio ciclo de vida. Por ejemplo, una actividad que sube una imagen a un sitio web, debería iniciar un servicio para realizar esta operación y así garantizar su ejecución en segundo plano aun cuando el usuario abandone la actividad. Usar un servicio nos garantiza que la operación tendrá al menos la prioridad de un proceso de servicio independientemente de lo que suceda con la actividad. Esta es la misma razón por la que los receptores de avisos deberían usar servicios en lugar de realizar operaciones pesadas en una hebra sin más.

Hebras.

Cuando se inicia una aplicación, el sistema crea un flujo de ejecución por defecto; su hebra principal. Esta hebra es muy importante debido a que es la encargada de responder a los eventos que provienen de la interfaz del usuario, incluyendo los eventos asociados a las animaciones. También es la hebra usada por componentes pertenecientes a los paquetes android.widget y android.view. Es por este último motivo por el que la hebra principal también recibe el nombre de hebra de la interfaz de usuario.

El sistema no crea hebras para cada uno de los componentes de nuestra aplicación. Todos los componentes que se ejecutan en el mismo proceso son creados desde la hebra principal y las llamadas a sus métodos también se hacen desde ésta. Por consiguiente, los métodos invocados por el sistema (tales como onKeyDown(), o métodos asociados al ciclo de vida), siempre serán ejecutados desde la hebra principal por defecto.

Cuando nuestra aplicación realiza un trabajo intensivo como respuesta a la interacción con el usuario, utilizar una única hebra puede dar lugar a un rendimiento bajo. Si además utilizamos la hebra principal para realizar operaciones de acceso a red o consultas a una base de datos, ésta podría llegar a bloquearse. Cuando la hebra se bloquea, se dejan de invocar eventos, incluyendo los eventos asociados a las animaciones. Desde el punto de vista del usuario, la aplicación aparecerá como si se hubiera colgado. En el peor de los casos, si la hebra principal queda bloqueada entorno a cinco segundos, se acabará mostrando un cuadro de diálogo al usuario con el mensaje "La aplicación no responde" (ANR). Este problema puede ocasionar la desinstalación de nuestra aplicación por parte del usuario.

Además, debemos tener en cuenta que la hebra principal en Android no es thread-safe, por lo que debemos hacer todas las operaciones relacionadas con la interfaz de usuario desde la hebra principal. Hay dos reglas simples a seguir en el modelo de hebras de Android:
  1. No bloquear la hebra principal.
  2. No acceder a objetos de la interfaz de usuario desde hebras secundarias.

Hebras secundarias o en segundo plano.

Hay determinados trabajos que por su naturaleza necesitan mayor tiempo de computo. Este tipo de trabajos deberían ser ejecutados en hebras secundarias para liberar de carga a la hebra principal.

Por ejemplo, en la secuencia de código siguiente tenemos el evento onClick de un botón. Cuando el usuario pulsa el botón, se descarga una imagen y ésta se muestra por pantalla en una vista de tipo ImageView:
public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            Bitmap b = loadImageFromNetwork("http://example.com/image.png");
            mImageView.setImageBitmap(b);
        }
    }).start();
}
A primera vista este código puede parecernos correcto ya que se crea una hebra distinta a la principal para descargar y mostrar la imagen. Sin embargo, la segunda de las reglas que vimos anteriormente, no se está cumpliendo. En el ejemplo, la hebra secundaria modifica un objeto de tipo ImageView. Esto puede dar lugar a un funcionamiento inesperado y detectar errores de este tipo nos puede llevar un tiempo considerable.

Para solucionar este problema, Android nos ofrece una serie de mecanismos para acceder a la hebra principal desde otras hebras:
  • Activity.runOnUiThread(Runnable).
  • View.post(Runnable).
  • View.postDelayed(Runnable, long).
Por ejemplo, podríamos usar el método View.post(Runnable) para solventar el error del código anterior:
public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");
            mImageView.post(new Runnable() {
                public void run() {
                    mImageView.setImageBitmap(bitmap);
                }
            });
        }
    }).start();
}
Ahora el código es thread-safe: la operación de red se realiza en una hebra separada mientras que la vista ImageView será manipulada desde la hebra principal.

No obstante, conforme vaya aumentando la complejidad de nuestro desarrollo, escribir el código de esta manera puede resultar complejo y difícil de mantener. Para gestionar interacciones complejas con una hebra en segundo plano, deberíamos considerar utilizar un Handler para comunicarnos con la hebra principal. Android nos proporciona la clase AsyncTask para facilitarnos la interacción con la hebra principal.

Usando la clase AsyncTask.

AsyncTask nos permitirá realizar trabajos de manera asíncrona desde la interfaz de usuario. Se encargará de realizar las operaciones de bloqueo necesarias en la hebra secundaria y de comunicar los resultados a la hebra principal, por lo que no tendremos que gestionar las hebras de manera directa.

Para usar este mecanismo, extendemos la clase AsyncTask e implementamos su método doInBackGround(). Si necesitamos actualizar la interfaz de usuario a partir de los resultados del método anterior, implementaremos además el método onPostExcecute(). Este último método se ejecuta en la hebra principal, con lo que estaríamos cumpliendo con la segunda regla del modelo de hebras de Android. Para ejecutar la tarea lo haremos llamando al método execute() desde la hebra principal.

Por ejemplo, podemos implementar el ejemplo anterior usando AsyncTask de la siguiente manera:
public void onClick(View v) {
    new DownloadImageTask().execute("http://example.com/image.png");
}

private class DownloadImageTask extends AsyncTask {
    /** Descargamos la imagen en segundo plano.
      * Las URL de la imagen vienen de AsyncTask.execute(String... urls). */
    protected Bitmap doInBackground(String... urls) {
        return loadImageFromNetwork(urls[0]);
    }
    
    /** El sistema invoca este método una vez se ha descargado las imagen.
      * Actualizamos la interfaz del usuario con las imagen descargada (resultado). */
    protected void onPostExecute(Bitmap result) {
        mImageView.setImageBitmap(result);
    }
}
Básicamente el funcionamiento de AsyncTask sería:
  • Especificamos los tipos genéricos asociados a la clase AsyncTask: el tipo para los argumentos del constructor, el tipo para el resultado devuelto por el método doInBackGround() que será del mismo tipo que el especificado para el argumento del método onPostExecute() y el tipo para los argumentos del método onProgressUpdate().
  • El método doInBackground() se ejecutará automáticamente en segundo plano tras llamar al método execute().
  • Los métodos onPreExcecute(), onPostExcecute() y onProgressUpdate() serán ejecutados en la hebra principal. Estos son los métodos desde los que actualizaremos la interfaz.
  • El valor devuelto por el método doInBackground() se entrega como argumento al método onPostExecute().
  • Podremos invocar el método publishProgress() en cualquier momento dentro del método doInBackground() para ejecutar el método onProgressUpdate() encargado de actualizar una barra de progreso en la interfaz, por ejemplo.
  • Podremos cancelar la tarea en cualquier momento, desde cualquier hebra.

Importante. Un problema adicional que podemos encontrar usando hebras en segundo plano es cuando una actividad se renicia debido a cambios en la configuración (cuando cambia la orientación de la pantalla, por ejemplo). Esto acabará destruyendo nuestra hebra en segundo plano. Existen mecanismos adecuados para tratar este tipo de casos que veremos más adelante cuando veamos los recursos en detalle.

Métodos thread-safe

En algunas situaciones, los métodos que implementamos son invocados desde varias hebras y es por ello que tendremos que hacerlos thread-safe.

Un ejemplo típico serían los métodos de una interfaz IBinder asociada a un servicio enlazado a la hora de ser invocados de manera remota. Cuando invocamos el método de un servicio desde el mismo proceso al que éste pertenece, el método se acabará ejecutando en la misma hebra que originó la llamada. Sin embargo, cuando el origen de la llamada se hace desde un proceso distinto, el método se acabará ejecutado en una hebra secundaria del proceso remoto. Debido a que un servicio puede tener más de un cliente enlazado y que éstos a su vez pueden llamar de manera remota a un mismo método a la vez, los métodos de una interfaz IBinder deberán ser thread-safe.

De la misma manera, un proveedor de contenido también puede recibir múltiples peticiones desde otros procesos. Aunque las clases ContentResolver y ContentProvider nos ocultan los detalles de la comunicación entre procesos, los métodos asociados a la clase ContentProvider: query(), insert(), delete(), update() y getType(), se ejecutarán en hebras secundarias del proceso proveedor y no en su hebra principal. Como los métodos pueden ser invocados desde múltiples hebras al mismo tiempo, éstos también tendrán que ser thread-safe.

No hay comentarios:

Publicar un comentario