Wszystkie aplikacje na Androida używają wątku głównego do obsługi operacji UI. Wywoływanie długotrwałych operacji z tego wątku głównego może spowodować zawieszenie usługi i brak odpowiedzi. Jeśli na przykład aplikacja wysyła żądanie sieciowe z wątku głównego, jej interfejs użytkownika jest zablokowany, dopóki nie otrzyma odpowiedzi sieciowej. Jeśli używasz Javy, możesz utworzyć dodatkowe wątki w tle do obsługi długotrwałych operacji, a wątek główny będzie nadal obsługiwać aktualizacje interfejsu.
Ten przewodnik pokazuje, jak deweloperzy używający języka programowania Java mogą używać puli wątków do konfigurowania i używania wielu wątków w aplikacji na Androida. Dowiesz się z niego również, jak zdefiniować kod uruchamiany w wątku oraz jak komunikować się między jednym z tych wątków a wątkiem głównym.
Biblioteki równoczesności
Warto znać podstawy wątków i ich mechanizmy. Istnieje jednak wiele popularnych bibliotek, które oferują wyższe abstrakcje dotyczące tych pojęć i gotowe do użycia narzędzia do przekazywania danych między wątkami. Biblioteki te to m.in. Guava i RxJava dla użytkowników języka programowania Java oraz Coroutines, które zalecamy użytkownikom Kotlin.
W praktyce należy wybrać ten, który najlepiej sprawdza się w przypadku aplikacji i zespołu programistów, chociaż zasady tworzenia wątków pozostają takie same.
Przegląd przykładów
Zgodnie z przewodnikiem po architekturze aplikacji przykłady w tym temacie wysyłają żądanie sieciowe i zwracają wynik do wątku głównego, w którym aplikacja może wyświetlić wynik na ekranie.
W szczególności ViewModel
wywołuje warstwę danych w wątku głównym, aby aktywować żądanie sieciowe. Warstwa danych odpowiada za przeniesienie wykonania żądania sieciowego z wątku głównego i opublikowanie wyniku z powrotem do wątku głównego za pomocą wywołania zwrotnego.
Aby przenieść wykonywanie żądania sieciowego z wątku głównego, musimy utworzyć inne wątki w naszej aplikacji.
Utwórz wiele wątków
Pula wątków to zarządzany zbiór wątków, który uruchamia zadania równolegle z kolejki. W istniejących wątkach będą wykonywane nowe zadania, gdy staną się one nieaktywne. Aby wysłać zadanie do puli wątków, użyj interfejsu ExecutorService
. Pamiętaj, że ExecutorService
nie ma nic wspólnego z Usługami – komponentem aplikacji na Androida.
Tworzenie wątków jest kosztowne, dlatego pulę wątków należy utworzyć tylko raz podczas inicjowania aplikacji. Pamiętaj, aby zapisać instancję ExecutorService
w klasie Application
lub w kontenerze wstrzykiwania zależności.
Poniższy przykład pokazuje pulę wątków składających się z 4 wątków, których można używać do uruchamiania zadań w tle.
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
}
Istnieją inne sposoby konfigurowania puli wątków w zależności od oczekiwanego obciążenia. Więcej informacji znajdziesz w artykule Konfigurowanie puli wątków.
Wykonaj w wątku w tle
Wysłanie żądania sieciowego w wątku głównym powoduje, że wątek poczeka lub zablokuje go, aż otrzyma odpowiedź. Ponieważ wątek jest zablokowany, system operacyjny nie może wywołać funkcji onDraw()
, a aplikacja zawiesza się, co może prowadzić do wyświetlenia okna błędu ANR (Application Not Responding). Wykonajmy ją w wątku w tle.
Prześlij prośbę
Przyjrzyjmy się najpierw klasie LoginRepository
, aby zobaczyć, jak wysyła żądanie sieciowe:
// 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()
jest synchroniczny i blokuje wątek wywołujący. Do modelowania odpowiedzi na żądanie sieciowe mamy własną klasę Result
.
Wyślij żądanie
ViewModel
wywołuje żądanie sieciowe, gdy użytkownik kliknie na przykład przycisk:
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);
}
}
Za pomocą poprzedniego kodu LoginViewModel
blokuje wątek główny podczas wysyłania żądania sieciowego. Możemy użyć puli wątków, którą utworzyliśmy w instancji, do przeniesienia wykonania do wątku w tle.
Obsługa wstrzykiwania zależności
Po pierwsze zgodnie z zasadami wstrzykiwania zależności LoginRepository
przyjmuje wystąpienie elementu Executor zamiast ExecutorService
, ponieważ wykonuje kod i nie zarządza wątkami:
public class LoginRepository {
...
private final Executor executor;
public LoginRepository(LoginResponseParser responseParser, Executor executor) {
this.responseParser = responseParser;
this.executor = executor;
}
...
}
Metoda execute() wykonawcy wymaga klasy Runnable. Runnable
to interfejs pojedynczej metody abstrakcyjnej (SAM) z metodą run()
, która jest wykonywana w wątku po wywołaniu.
Wykonuj w tle
Utwórzmy inną funkcję o nazwie makeLoginRequest()
, która przenosi wykonanie do wątku w tle i na razie ignoruje odpowiedź:
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
}
...
}
W metodzie execute()
tworzymy nowy blok kodu Runnable
z blokiem kodu, który chcemy uruchamiać w wątku w tle – w naszym przypadku z synchroniczną metodą żądania sieci. ExecutorService
zarządza wewnętrznie elementem Runnable
i wykonuje go w dostępnym wątku.
co należy wziąć pod uwagę
Każdy wątek w aplikacji może działać równolegle do innych wątków, w tym wątku głównego, dlatego sprawdź, czy Twój kod jest w nim bezpieczny. Zwróć uwagę, że w naszym przykładzie unikamy zapisywania zmian w zmiennych udostępnianych między wątkami i przekazywania niezmiennych danych. To dobra metoda, bo każdy wątek korzysta z własnej instancji danych, a to pozwala uniknąć złożoności synchronizacji.
Jeśli musisz udostępniać stan między wątkami, pamiętaj, aby zarządzać dostępem z wątków za pomocą mechanizmów synchronizacji, takich jak blokady. Nie wchodzi to w zakres tego przewodnika. W miarę możliwości unikaj udostępniania zmiennych stanów między wątkami.
Komunikacja z wątkiem głównym
W poprzednim kroku zignorowaliśmy odpowiedź na żądanie sieciowe. Aby wyświetlić wynik na ekranie, LoginViewModel
musi o nim wiedzieć. Możemy to zrobić, stosując wywołania zwrotne.
Funkcja makeLoginRequest()
powinna przyjmować wywołanie zwrotne jako parametr, by asynchronicznie zwracać wartość. Wywołanie zwrotne z wynikiem jest wywoływane za każdym razem, gdy żądanie sieciowe zostanie ukończone lub wystąpi błąd. W Kotlin możemy
użyć funkcji wyższego rzędu. W języku Java musimy jednak utworzyć nowy interfejs wywołania zwrotnego, który będzie miał taką samą funkcjonalność:
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);
}
}
});
}
...
}
ViewModel
musi teraz wdrożyć wywołanie zwrotne. Działanie tej funkcji może być różne w zależności od wyniku:
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
}
}
});
}
}
W tym przykładzie wywołanie zwrotne jest wykonywane w wywołanym wątku, który jest wątkiem w tle. Oznacza to, że nie możesz modyfikować warstwy interfejsu ani komunikować się z nią bezpośrednio, dopóki nie przełączysz się z powrotem na wątek główny.
Używanie modułów obsługi
Aby dodać do kolejki działanie, które ma zostać wykonane w innym wątku, możesz użyć modułu obsługi. Aby określić wątek, w którym ma być uruchomione działanie, utwórz Handler
za pomocą Zapętlacza wątku. Looper
to obiekt, który uruchamia pętlę wiadomości w powiązanym wątku. Po utworzeniu Handler
możesz użyć metody post(Runnable), aby uruchomić blok kodu w odpowiednim wątku.
Looper
zawiera funkcję pomocniczą getMainLooper(), która pobiera Looper
wątku głównego. Możesz uruchomić kod w wątku głównym, używając tego polecenia Looper
do utworzenia Handler
. Jest to rozwiązanie, które często powtarzasz, więc możesz też zapisać wystąpienie obiektu Handler
w tym samym miejscu, w którym został zapisany ExecutorService
:
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}
Zalecamy wstrzyknięcie modułu obsługi do repozytorium, ponieważ zapewnia to większą elastyczność. W przyszłości możesz np. chcieć przekazywać inny Handler
, aby zaplanować zadania w osobnym wątku. Jeśli zawsze komunikujesz się z powrotem w tym samym wątku, możesz przekazać Handler
do konstruktora repozytorium, jak pokazano w poniższym przykładzie.
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);
}
});
}
...
}
Jeśli potrzebujesz większej elastyczności, możesz przekazać obiekt Handler
do każdej funkcji:
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);
}
});
}
}
W tym przykładzie wywołanie zwrotne przekazane do wywołania makeLoginRequest
repozytorium jest wykonywane w wątku głównym. Oznacza to, że możesz modyfikować interfejs bezpośrednio z poziomu wywołania zwrotnego lub używać LiveData.setValue()
do komunikowania się z nim.
Konfigurowanie puli wątków
Pulę wątków możesz utworzyć za pomocą jednej z funkcji pomocniczych wykonawcy ze wstępnie zdefiniowanymi ustawieniami, jak pokazano w poprzednim przykładowym kodzie. Jeśli chcesz dostosować szczegóły puli wątków, możesz utworzyć instancję bezpośrednio za pomocą polecenia ThreadPoolExecutor. Możesz skonfigurować te szczegóły:
- Początkowy i maksymalny rozmiar puli.
- Utrzymanie czasu aktywności i jednostka czasu. Czas utrzymywania aktywności to maksymalny czas, przez jaki wątek może pozostawać nieaktywny, zanim zostanie wyłączony.
- Kolejka wejściowa zawierająca zadania (
Runnable
). Ta kolejka musi implementować interfejs BlockQueue. Aby spełnić wymagania aplikacji, możesz wybrać jedną z dostępnych implementacji kolejek. Więcej informacji znajdziesz w omówieniu klasy funkcji ThreadPoolExecutor.
Oto przykład, który określa rozmiar puli wątków na podstawie łącznej liczby rdzeni procesora, czasu utrzymywania aktywności wynoszącego 1 sekundę i kolejki wejściowej.
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
);
...
}