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
);
...
}