Trabalho assíncrono com linhas de execução Java

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