Trabajo asíncrono con subprocesos de Java

Todas las apps para Android usan un subproceso principal para controlar las operaciones de la IU. Llamadas de larga duración de este subproceso principal pueden generar bloqueos y falta de respuesta. Para Por ejemplo, si tu app realiza una solicitud de red desde el subproceso principal, la IU de tu app se inmoviliza hasta que recibe la respuesta de la red. Si usas Java, puedes crear subprocesos en segundo plano adicionales para controlar operaciones de larga duración mientras el subproceso principal continúa controlando las actualizaciones de la IU.

Esta guía muestra cómo los desarrolladores que usan el lenguaje de programación Java pueden usar un thread pool para configurar y usar varios subprocesos en una app para Android. Esta guía se muestra cómo definir código para ejecutar en un subproceso y cómo comunicarse entre uno de estos subprocesos y el subproceso principal.

Bibliotecas de simultaneidad

Es importante comprender los conceptos básicos de los subprocesos y sus mecanismos subyacentes. Sin embargo, existen muchas bibliotecas populares que ofrecen sobre estos conceptos y utilidades listas para usar con el objetivo de pasar datos entre subprocesos. Estas bibliotecas incluyen Guava y RxJava para los usuarios del lenguaje de programación Java y las corrutinas que recomendamos para los usuarios de Kotlin.

En la práctica, debes elegir la que mejor se adapte a tu aplicación y a tu desarrollo, aunque las reglas de los subprocesos siguen siendo las mismas.

Resumen de ejemplos

Basados en la Guía de arquitectura de apps, los ejemplos de este tema hacen solicitud de red y devuelve el resultado al subproceso principal, donde la app podría mostrar ese resultado en la pantalla.

Específicamente, ViewModel llama a la capa de datos del subproceso principal para activar la solicitud de red. La capa de datos se encarga de mover los ejecución de la solicitud de red fuera del subproceso principal y publicación del resultado al subproceso principal mediante una devolución de llamada.

Para quitar la ejecución de la solicitud de red del subproceso principal, debemos hacer lo siguiente: crear otros subprocesos en nuestra app.

Cómo crear varios subprocesos

Un conjunto de subprocesos es una colección administrada de subprocesos que ejecuta tareas en en forma paralela de una cola. Las tareas nuevas se ejecutan en subprocesos existentes como aquellas los subprocesos quedan inactivos. Para enviar una tarea a un conjunto de subprocesos, usa el ExecutorService. Ten en cuenta que ExecutorService no tiene nada que hacer con Services, el componente de la aplicación para Android.

Crear subprocesos es costoso, por lo que deberías crear un conjunto de subprocesos solo una vez cuando se inicializa tu app. Asegúrate de guardar la instancia de ExecutorService. en tu clase Application o en un contenedor de inserción de dependencias. En el siguiente ejemplo, se crea un conjunto de subprocesos que podemos usar para ejecutar tareas en segundo plano.

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
}

Existen otras formas de configurar un conjunto de subprocesos según las necesidades carga de trabajo. Consulta Cómo configurar un conjunto de subprocesos para obtener más información.

Ejecutar en un subproceso en segundo plano

Realizar una solicitud de red en el subproceso principal hace que el subproceso quede en espera. block, hasta que reciba una respuesta. Como el subproceso está bloqueado, el SO llamar a onDraw(), y tu app se bloquea, lo que puede dar lugar a una aplicación que no Cuadro de diálogo de respuesta (ANR) En su lugar, ejecutemos esta operación en segundo plano conversación.

Realiza la solicitud

Primero, veamos nuestra clase LoginRepository y veamos su rendimiento. la solicitud de red:

// Result.java
public abstract class Result<T> {
    private Result() {}

    public static final class Success<T> extends Result<T> {
        public T data;

        public Success(T data) {
            this.data = data;
        }
    }

    public static final class Error<T> extends Result<T> {
        public Exception exception;

        public Error(Exception exception) {
            this.exception = exception;
        }
    }
}

// LoginRepository.java
public class LoginRepository {

    private final String loginUrl = "https://example.com/login";
    private final LoginResponseParser responseParser;

    public LoginRepository(LoginResponseParser responseParser) {
        this.responseParser = responseParser;
    }

    public Result<LoginResponse> makeLoginRequest(String jsonBody) {
        try {
            URL url = new URL(loginUrl);
            HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
            httpConnection.setRequestMethod("POST");
            httpConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
            httpConnection.setRequestProperty("Accept", "application/json");
            httpConnection.setDoOutput(true);
            httpConnection.getOutputStream().write(jsonBody.getBytes("utf-8"));

            LoginResponse loginResponse = responseParser.parse(httpConnection.getInputStream());
            return new Result.Success<LoginResponse>(loginResponse);
        } catch (Exception e) {
            return new Result.Error<LoginResponse>(e);
        }
    }
}

La clase makeLoginRequest() es síncrona y bloquea el subproceso de llamada. Para modelar el respuesta a la solicitud de red, tenemos nuestra propia clase Result.

Activa la solicitud

ViewModel activa la solicitud de red cuando el usuario presiona, por ejemplo, en un botón:

public class LoginViewModel {

    private final LoginRepository loginRepository;

    public LoginViewModel(LoginRepository loginRepository) {
        this.loginRepository = loginRepository;
    }

    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
        loginRepository.makeLoginRequest(jsonBody);
    }
}

Con el código anterior, LoginViewModel bloquea el subproceso principal cuando se crea la solicitud de red. Podemos usar el conjunto de subprocesos del que creamos una instancia para mover la ejecución a un subproceso en segundo plano.

Cómo controlar la inyección de dependencias

Primero, siguiendo los principios de inyección de dependencias, LoginRepository toma una instancia de Executor en lugar de ExecutorService porque es ejecutar código y no administrar subprocesos:

public class LoginRepository {
    ...
    private final Executor executor;

    public LoginRepository(LoginResponseParser responseParser, Executor executor) {
        this.responseParser = responseParser;
        this.executor = executor;
    }
    ...
}

El método execute() del ejecutor toma un elemento Runnable. Un Runnable es un interfaz de método abstracto único (SAM) con un método run() que se ejecuta en un subproceso cuando se invoca.

Ejecutar en segundo plano

Ahora, creemos otra función llamada makeLoginRequest() que pase la ejecución al subproceso en segundo plano y, por el momento, ignore la respuesta:

public class LoginRepository {
    ...
    public void makeLoginRequest(final String jsonBody) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Result<LoginResponse> ignoredResponse = makeSynchronousLoginRequest(jsonBody);
            }
        });
    }

    public Result<LoginResponse> makeSynchronousLoginRequest(String jsonBody) {
        ... // HttpURLConnection logic
    }
    ...
}

Dentro del método execute(), creamos un nuevo Runnable con el bloque de código. queremos ejecutar en el subproceso en segundo plano; en nuestro caso, la red síncrona método de solicitud. De forma interna, ExecutorService administra Runnable y lo ejecuta en un subproceso disponible.

Consideraciones

Cualquier subproceso de tu app puede ejecutarse en paralelo a otros subprocesos, incluido el principal subproceso, por lo que debes asegurarte de que tu código sea seguro para los subprocesos. Ten en cuenta que, en nuestra ejemplo que evitamos escribir en variables compartidas entre subprocesos, pasar datos inmutables. Esta es una práctica recomendada, ya que cada subproceso trabaja con su propia instancia de datos, y evitamos la complejidad de la sincronización.

Si necesitas compartir el estado entre subprocesos, debes tener cuidado de administrar el acceso de subprocesos que usan mecanismos de sincronización como bloqueos Esto está fuera de el alcance de esta guía. En general, debes evitar compartir el estado mutable entre subprocesos siempre que sea posible.

Cómo comunicarse con el subproceso principal

En el paso anterior, ignoramos la respuesta a la solicitud de red. Para mostrar los resultado en la pantalla, LoginViewModel necesita conocerlo. Podemos hacerlo con devoluciones de llamada.

La función makeLoginRequest() debe tomar una devolución de llamada como parámetro para que puede mostrar un valor de forma asíncrona. Se llama a la devolución de llamada con el resultado cuando se completa la solicitud de red o se produce una falla. En Kotlin, podemos usar una función de orden superior. Sin embargo, en Java, tenemos que crear una nueva devolución de llamada. tengan la misma funcionalidad:

interface RepositoryCallback<T> {
    void onComplete(Result<T> result);
}

public class LoginRepository {
    ...
    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    callback.onComplete(result);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    callback.onComplete(errorResult);
                }
            }
        });
    }
  ...
}

Ahora, el ViewModel debe implementar la devolución de llamada. Puede usar una lógica diferente según el resultado:

public class LoginViewModel {
    ...
    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
        loginRepository.makeLoginRequest(jsonBody, new RepositoryCallback<LoginResponse>() {
            @Override
            public void onComplete(Result<LoginResponse> result) {
                if (result instanceof Result.Success) {
                    // Happy path
                } else {
                    // Show error in UI
                }
            }
        });
    }
}

En este ejemplo, se ejecuta la devolución de llamada en el subproceso de llamada, que es un subproceso en segundo plano. Esto significa que no puedes modificar ni comunicarte directamente con la capa de IU hasta que vuelvas al subproceso principal.

Cómo usar controladores

Puedes usar un Handler para poner en cola una acción que se ejecutará en un conversación. Para especificar el subproceso en el que se ejecutará la acción, crea el Handler con un Looper para el subproceso Un Looper es un objeto que se ejecuta el bucle de mensajes para un subproceso asociado. Una vez que hayas creado un Handler, podrás puedes usar el método post(Runnable) para ejecutar un bloque de código en la subproceso correspondiente.

Looper incluye una función auxiliar, getMainLooper(), que recupera el Looper del subproceso principal Puedes ejecutar código en el subproceso principal con Looper para crear un Handler Como esto es algo que podrías hacer con bastante frecuencia, También puedes guardar una instancia de Handler en el mismo lugar en el que guardaste la ExecutorService

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
    Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}

Se recomienda insertar el controlador en el repositorio, ya que le brinda mayor flexibilidad. Por ejemplo, en el futuro quizá quieras pasar una Handler diferentes para programar tareas en un subproceso independiente. Si siempre estás te comunicas con el mismo subproceso, puedes pasar el Handler al de repositorio de código abierto, como se muestra en el siguiente ejemplo.

public class LoginRepository {
    ...
    private final Handler resultHandler;

    public LoginRepository(LoginResponseParser responseParser, Executor executor,
            Handler resultHandler) {
        this.responseParser = responseParser;
        this.executor = executor;
        this.resultHandler = resultHandler;
    }

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    notifyResult(errorResult, callback);
                }
            }
        });
    }

    private void notifyResult(
        final Result<LoginResponse> result,
        final RepositoryCallback<LoginResponse> callback,
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
    ...
}

Como alternativa, si deseas más flexibilidad, puedes pasar un Handler a cada función:

public class LoginRepository {
    ...

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback,
        final Handler resultHandler,
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback, resultHandler);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    notifyResult(errorResult, callback, resultHandler);
                }
            }
        });
    }

    private void notifyResult(
        final Result<LoginResponse> result,
        final RepositoryCallback<LoginResponse> callback,
        final Handler resultHandler
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
}

En este ejemplo, la devolución de llamada que se pasó a la llamada makeLoginRequest del repositorio se ejecuta en el subproceso principal. Esto significa que puedes modificar directamente la IU de la devolución de llamada o usa LiveData.setValue() para comunicarte con la IU.

Cómo configurar un conjunto de subprocesos

Puedes crear un conjunto de subprocesos usando una de las funciones auxiliares Executor. con parámetros de configuración predefinidos, como se muestra en el código de ejemplo anterior. Por otro lado, Si quieres personalizar los detalles del conjunto de subprocesos, puedes crear un con ThreadPoolExecutor directamente. Puedes configurar los siguientes detalles:

  • Tamaño inicial y máximo del conjunto.
  • Tiempo de mantenimiento de conexión y unidad de tiempo. El tiempo de mantenimiento de actividad es la duración máxima que el subproceso puede permanecer inactivo antes de que se cierre.
  • Una cola de entrada que conserve tareas del objeto Runnable. Esta cola debe implementar el BlockingQueue. Para cumplir con los requisitos de tu app, puedes hacer lo siguiente: elegir entre las implementaciones de cola disponibles. Para obtener más información, consulta la clase Descripción general de ThreadPoolExecutor

Aquí te mostramos un ejemplo que especifica el tamaño del conjunto de subprocesos según la cantidad total de del procesador, un tiempo de mantenimiento de actividad de un segundo y una cola de entrada.

public class MyApplication extends Application {
    /*
     * Gets the number of available cores
     * (not always the same as the maximum number of cores)
     */
    private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();

    // Instantiates the queue of Runnables as a LinkedBlockingQueue
    private final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();

    // Sets the amount of time an idle thread waits before terminating
    private static final int KEEP_ALIVE_TIME = 1;
    // Sets the Time Unit to seconds
    private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;

    // Creates a thread pool manager
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            NUMBER_OF_CORES,       // Initial pool size
            NUMBER_OF_CORES,       // Max pool size
            KEEP_ALIVE_TIME,
            KEEP_ALIVE_TIME_UNIT,
            workQueue
    );
    ...
}