Todos os apps para Android usam uma linha de execução principal para lidar com operações de IU. Chamadas de longa duração desta linha de execução principal podem causar congelamentos e falta de resposta. Para Por exemplo, se o app fizer uma solicitação de rede pela linha de execução principal, a interface é congelado até receber a resposta da rede. Se você usa Java, é possível criar mais linhas de execução em segundo plano para lidar com operações de longa duração enquanto a linha de execução principal continua a processar atualizações de interface.
Este guia mostra como os desenvolvedores que usam a linguagem de programação Java podem usar uma pool de linhas de execução para configurar e usar várias linhas de execução em um app Android. Este guia também mostra como definir o código a ser executado em uma linha de execução e como se comunicar entre uma dessas linhas de execução e a linha de execução principal.
Bibliotecas de simultaneidade
É importante entender os princípios básicos da linha de execução e dos mecanismos subjacentes. No entanto, há muitas bibliotecas populares que oferecem sobre esses conceitos e utilitários prontos para uso para transmitir dados entre as linhas. Essas bibliotecas incluem Guava e RxJava para usuários da linguagem de programação Java e corrotinas, o que recomendamos para os usuários do Kotlin.
Na prática, você deve escolher o que funciona melhor para seu aplicativo da equipe de desenvolvimento de software, embora as regras de linhas de execução permaneçam as mesmas.
Visão geral dos exemplos
Com base no Guia para a arquitetura do app, os exemplos deste tópico fazem uma solicitação de rede e retornar o resultado para a linha de execução principal, em que o app pode exibir esse resultado na tela.
Especificamente, ViewModel
chama a camada de dados na linha de execução principal para
acionar a solicitação de rede. A camada de dados é responsável por mover
execução da solicitação de rede fora da linha de execução principal e postando o resultado de volta
à linha de execução principal usando um callback.
Para mover a execução da solicitação de rede para fora da linha de execução principal, precisamos criar outras linhas de execução no app.
Criar várias linhas de execução
Um pool de linhas de execução é uma coleção gerenciada de linhas de execução que executa tarefas em
em paralelo de uma fila. Novas tarefas são executadas em linhas de execução existentes,
em que as linhas de execução ficam ociosas. Para enviar uma tarefa a um pool de linhas de execução, use o
ExecutorService
. Observe que ExecutorService
não tem nada a fazer
com Services, o componente de aplicativo Android.
A criação de linhas de execução é cara, portanto, crie um pool de linhas de execução somente uma vez
que o app é inicializado. Salve a instância do ExecutorService
na classe Application
ou em um contêiner de injeção de dependência.
O exemplo a seguir cria um pool de quatro linhas de execução que podemos usar para
executar tarefas em segundo plano.
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
}
Há outras maneiras de configurar um pool de linhas de execução, dependendo do que é esperado. carga de trabalho do Google Cloud. Consulte Como configurar um pool de linhas de execução para mais informações.
Executar em uma linha de execução em segundo plano
Fazer uma solicitação de rede na linha de execução principal faz com que ela aguarde ou
block, até receber uma resposta. Como a linha de execução está bloqueada, o SO não pode
chame onDraw()
, e o app trava, podendo resultar em uma mensagem de erro
Caixa de diálogo de resposta (ANR). Em vez disso, vamos executar essa operação em segundo plano
fio
Fazer a solicitação
Primeiro, vamos conferir o desempenho da classe LoginRepository
solicitação de rede:
// 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);
}
}
}
makeLoginRequest()
é síncrono e bloqueia a linha de execução de chamada. Para modelar o
resposta da solicitação de rede, temos nossa própria classe Result
.
Acionar a solicitação
O ViewModel
aciona a solicitação de rede quando o usuário toca, por exemplo,
um botão:
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);
}
}
Com o código anterior, LoginViewModel
está bloqueando a linha de execução principal ao fazer
a solicitação de rede. Podemos usar o pool de linhas de execução instanciado para mover
a execução em uma linha de execução em segundo plano.
Processar a injeção de dependência
Primeiro, seguindo os princípios de injeção de dependência (link em inglês), LoginRepository
usa uma instância de Executor em vez de ExecutorService
porque ela é
executar código e não gerenciar linhas de execução:
public class LoginRepository {
...
private final Executor executor;
public LoginRepository(LoginResponseParser responseParser, Executor executor) {
this.responseParser = responseParser;
this.executor = executor;
}
...
}
O método execute() do executor usa um Runnable. Um Runnable
é um
Interface de método abstrato único (SAM, na sigla em inglês) com um método run()
que é executado em
uma linha de execução quando invocadas.
Executar em segundo plano
Vamos criar outra função chamada makeLoginRequest()
, que move a
execução para a linha de execução em segundo plano e ignora a resposta por enquanto:
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
}
...
}
No método execute()
, criamos um novo Runnable
com o bloco de código.
que queremos executar na linha de segundo plano. No nosso caso, a rede síncrona
. Internamente, o ExecutorService
gerencia os Runnable
e
o executa em uma linha de execução disponível.
Considerações
Qualquer linha de execução do app pode ser executada em paralelo com outras, incluindo a principal linha de execução, portanto, verifique se o código é thread-safe. Em nossa como evitamos gravar em variáveis compartilhadas entre linhas de execução, os dados imutáveis. Essa é uma boa prática, porque cada thread trabalha com uma instância de dados própria e evitamos a complexidade da sincronização.
Se for necessário compartilhar o estado entre as linhas de execução, tenha cuidado para gerenciar o acesso das linhas de execução usando mecanismos de sincronização, como bloqueios. Isso está fora de no escopo deste guia. Em geral, evite compartilhar estados mutáveis entre linhas de execução sempre que possível.
Comunicar-se com a linha de execução principal
Na etapa anterior, ignoramos a resposta da solicitação de rede. Para exibir o
resultado na tela, LoginViewModel
precisa saber sobre ele. Podemos fazer isso
usando callbacks.
A função makeLoginRequest()
precisa receber um callback como parâmetro para que
ele pode retornar um valor de forma assíncrona. O callback com o resultado é chamado
sempre que a solicitação de rede é concluída ou ocorre uma falha. Em Kotlin, podemos
use uma função de ordem superior. No entanto, em Java, precisamos criar um novo callback
para ter a mesma funcionalidade:
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);
}
}
});
}
...
}
O ViewModel
precisa implementar o callback agora. Ele pode executar uma lógica
diferente dependendo do 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
}
}
});
}
}
Neste exemplo, o callback é executado na linha de execução da chamada, que é uma linha de execução em segundo plano. Isso significa que você não pode modificar ou se comunicar diretamente com a camada de interface até voltar para a linha de execução principal.
Usar gerenciadores
Você pode usar um gerenciador para enfileirar uma ação a ser executada em um
fio Para especificar a linha de execução na qual executar a ação, construa a
Handler
usando um Looper da linha de execução. Um Looper
é um objeto executado
o loop de mensagens de uma conversa associada. Depois de criar um Handler
,
poderá usar o método post(Runnable) para executar um bloco de código na
linha de execução correspondente.
Looper
inclui uma função auxiliar, getMainLooper(), que recupera o
Looper
da linha de execução principal. É possível executar o código na linha de execução principal usando este
Looper
para criar um Handler
. Como isso é algo que você pode fazer com frequência,
Também é possível salvar uma instância do Handler
no mesmo lugar em que você salvou o
ExecutorService
:
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}
É uma boa prática injetar o gerenciador no repositório, pois ele oferece
mais flexibilidade. Por exemplo, no futuro, você pode querer passar um
Handler
diferente para programar tarefas em uma linha de execução separada. Se você sempre está
comunicando-se com a mesma linha de execução, você pode transmitir o Handler
para
construtor de repositório, conforme mostrado no exemplo a seguir.
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, se você quiser mais flexibilidade, transmita um Handler
para cada
função:
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);
}
});
}
}
Neste exemplo, o callback transmitido para a chamada makeLoginRequest
do repositório é executado na linha de execução principal. Isso significa que é possível modificar a interface
do callback ou usar LiveData.setValue()
para se comunicar com a interface.
Configurar um pool de linhas de execução
É possível criar um pool de linhas de execução usando uma das funções auxiliares Executor com configurações predefinidas, conforme mostrado no código de exemplo anterior. Como alternativa, Se quiser personalizar os detalhes do pool de linhas de execução, crie um instância usando ThreadPoolExecutor diretamente. É possível configurar o seguinte detalhes:
- Tamanho inicial e máximo do pool.
- Tempo de sinal de atividade e unidade de tempo. O tempo de sinal de atividade é a duração máxima pode permanecer ociosa antes de ser encerrada.
- Uma fila de entrada que contém tarefas
Runnable
. Essa fila deve implementar o BlockingQueue. Para atender aos requisitos do seu app, você pode: escolher entre as implementações de fila disponíveis. Para saber mais, consulte a aula Visão geral de ThreadPoolExecutor.
Aqui está um exemplo que especifica o tamanho do pool de linhas de execução com base no número total de núcleos de processador central, um tempo de sinal de atividade de um segundo e uma fila 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
);
...
}