Wszystkie aplikacje na Androida używają wątku głównego do obsługi operacji interfejsu użytkownika. Połączenia od dłuższego czasu Operacje wykonywane w tym wątku głównym mogą powodować blokady i brak odpowiedzi. Dla: na przykład jeśli aplikacja wysyła żądanie sieciowe z wątku głównego, interfejs aplikacji Urządzenie jest zablokowane do momentu otrzymania odpowiedzi sieciowej. Jeśli używasz Javy, tworzyć dodatkowe wątki w tle do obsługi długotrwałych operacji, wątki główne nadal obsługują aktualizacje interfejsu.
Z tego przewodnika dowiesz się, jak za pomocą języka programowania Java pula wątków (w języku angielskim) do konfigurowania i używania wielu wątków w aplikacji na Androida. Ten przewodnik pokazuje również, jak zdefiniować kod do uruchomienia w wątku i jak komunikować się między tymi wątkami a wątkiem głównym.
Biblioteki równoczesności
Znajomość podstaw tworzenia wątków i poznania podstaw ich działania jest niezbędna mechanizmów ochrony danych. Istnieje jednak wiele popularnych bibliotek, które oferują abstrakcyjne rozwiązania w zakresie tych koncepcji i gotowe do użycia narzędzia do przekazywania danych. między wątkami. Te biblioteki to m.in. Guava i RxJava dla użytkowników języka programowania Java i Coroutines, które zalecamy użytkownikom Kotlin.
W praktyce należy wybrać taki, który najlepiej sprawdza się w przypadku Twojej aplikacji ale zasady tworzenia wątków są takie same.
Przegląd przykładów
Zgodnie z Przewodnikiem po architekturze aplikacji przykłady w tym temacie sprawiają, i zwraca wynik do wątku głównego, w którym aplikacja może wyświetlić ten wynik na ekranie.
W szczególności funkcja ViewModel
wywołuje warstwę danych w wątku głównym,
uruchamiać żądanie sieciowe. Za przenoszenie danych odpowiada warstwa danych
Wykonanie żądania sieciowego poza wątkiem głównym i publikowanie wyniku z powrotem
do wątku głównego za pomocą wywołania zwrotnego.
Aby przenieść wykonanie żądania sieciowego z wątku głównego, musimy wykonać tworzyć inne wątki w naszej aplikacji.
Utwórz wiele wątków
Pula wątków to zarządzana grupa wątków, w której są uruchamiane zadania
równolegle z kolejki. Nowe zadania są wykonywane w istniejących wątkach, ponieważ
wątki stają się nieaktywne. Aby wysłać zadanie do puli wątków, użyj
ExecutorService
. Zwróć uwagę, że ExecutorService
nie ma nic do zrobienia.
z usługami, komponentem aplikacji na Androida.
Tworzenie wątków jest drogie, więc pulę wątków należy utworzyć tylko raz,
po uruchomieniu aplikacji. Pamiętaj, aby zapisać instancję ExecutorService
w klasie Application
lub w kontenerze do wstrzykiwania zależności.
Poniższy przykład tworzy pulę wątków składającą się z 4 wątków, której możemy użyć do
uruchamianie 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 oczekiwanych zadań. 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 oczekiwanie wątku lub
block, aż otrzyma odpowiedź. Ponieważ wątek jest zablokowany, system operacyjny nie może
onDraw()
może spowodować zawieszenie się aplikacji, co może doprowadzić do wystąpienia błędu Application Not
Okno odpowiadania (ANR). Przeprowadźmy tę operację w tle
w wątku.
Prześlij prośbę
Najpierw spójrzmy na nasze zajęcia LoginRepository
i zobaczmy, jak sobie radzą
żą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);
}
}
}
Działanie makeLoginRequest()
jest synchroniczne i blokuje wątek wywołujący. Aby modelować
na żądanie sieciowe, mamy własną klasę Result
.
Wyślij żądanie
ViewModel
wyzwala żądanie sieciowe, gdy użytkownik kliknie np.
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 tworzenia
żądania sieciowe. Do przenoszenia danych możemy użyć puli wątków, której mamy utworzyć
do wątku w tle.
Obsługa wstrzykiwania zależności
Najpierw, zgodnie z zasadami wstrzykiwania zależności, LoginRepository
korzysta z instancji Executor, a nie ExecutorService
, ponieważ
wykonywanie kodu bez zarządzania wątkami:
public class LoginRepository {
...
private final Executor executor;
public LoginRepository(LoginResponseParser responseParser, Executor executor) {
this.responseParser = responseParser;
this.executor = executor;
}
...
}
Metoda execute() wykonawcy korzysta z elementu Runnable (Możliwe do uruchomienia). Runnable
to
Interfejs pojedynczej metody abstrakcyjnej (SAM) z metodą run()
, która jest wykonywana
wątek po wywołaniu.
Wykonuj w tle
Utwórzmy kolejną funkcję o nazwie makeLoginRequest()
, która przenosi
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 Runnable
z blokiem kodu
które chcemy wykonywać w wątku działającym w tle – w naszym przypadku
metody żądania. Wewnętrznie ExecutorService
zarządza tymi funkcjami: Runnable
oraz
wykonuje je w dostępnym wątku.
co należy wziąć pod uwagę
Dowolny wątek w aplikacji, w tym wątek główny, może działać równolegle z innymi wątkami. , dlatego upewnij się, że kod jest bezpieczny. Zwróć uwagę, że w naszym Unikamy zapisywania do zmiennych współdzielonych między wątkami, za pomocą trwałych danych. To dobra metoda, ponieważ każdy wątek współpracuje z własnych instancji danych i unikamy złożoności synchronizacji.
Jeśli musisz udostępniać stan między wątkami, musisz zachować ostrożność podczas zarządzania dostępem z wątków za pomocą mechanizmów synchronizacji, takich jak blokady. To jest poza tego przewodnika. Ogólnie należy unikać udostępniania stanu zmiennego i przekazywać je między wątkami.
Komunikuj się z wątkiem głównym
W poprzednim kroku zignorowaliśmy odpowiedź na żądanie sieciowe. Aby wyświetlić
wynik na ekranie, aplikacja LoginViewModel
musi o tym wiedzieć. Możemy to zrobić
za pomocą wywołań zwrotnych.
Funkcja makeLoginRequest()
powinna przyjmować wywołanie zwrotne jako parametr,
może zwrócić wartość asynchronicznie. Wywołanie zwrotne z wynikiem jest wywoływane
za każdym razem, gdy żądanie sieciowe zakończy się lub wystąpi błąd. W Kotlin możemy
użyj funkcji wyższego rzędu. Jednak w Javie musimy utworzyć nowe wywołanie zwrotne
interfejs, aby mieć te same funkcje:
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 zaimplementować wywołanie zwrotne. Może działać w różny sposób,
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 wątku wywołującym, który jest w wątku w tle. Oznacza to, że nie można modyfikować ani komunikować się bezpośrednio z warstwą interfejsu, dopóki nie wrócisz do wątku głównego.
Korzystanie z modułów obsługi
Za pomocą modułu obsługi możesz umieścić w kolejce działanie, które ma zostać wykonane na innym urządzeniu.
w wątku. Aby określić wątek, którego dotyczy działanie, utwórz
Handler
za pomocą pętla w wątku. Looper
to obiekt uruchamiany
pętlę wiadomości w powiązanym wątku. Po utworzeniu Handler
może następnie użyć metody post(Runnable), aby uruchomić blok kodu w
odpowiedni wątek.
Looper
zawiera funkcję pomocniczą getMainLooper(), która pobiera
Looper
w wątku głównym. Możesz uruchomić kod w wątku głównym za pomocą tego
Looper
, aby utworzyć element Handler
. Jest to coś, co często robisz,
możesz również zapisać wystąpienie Handler
w tym samym miejscu, w którym
ExecutorService
:
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}
Warto wstrzyknąć moduł obsługi do repozytorium, ponieważ zapewnia to
większą elastyczność. Na przykład w przyszłości możesz zechcieć przekazać witrynę
Handler
, aby zaplanować zadania w osobnym wątku. Jeśli zawsze jesteś
komunikując się z tym samym wątkiem, możesz przekazać element Handler
jak w przykładzie poniżej.
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ć Handler
każdej osobie,
funkcja:
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 przekazywane do repozytorium makeLoginRequest
repozytorium
jest wykonywane w wątku głównym. Oznacza to, że możesz bezpośrednio modyfikować interfejs,
z wywołania zwrotnego lub użyj interfejsu LiveData.setValue()
do komunikacji z interfejsem.
Konfigurowanie puli wątków
Pulę wątków możesz utworzyć przy użyciu jednej z funkcji pomocniczych Wykonawca. ze wstępnie zdefiniowanymi ustawieniami, tak jak w poprzednim przykładowym kodzie. Ewentualnie Jeśli chcesz dostosować szczegóły puli wątków, możesz utworzyć za pomocą instancji ThreadPoolExecutor. Możesz skonfigurować te opcje szczegóły:
- Początkowy i maksymalny rozmiar puli.
- Zachowaj czas aktywności i jednostkę czasu. Czas utrzymywania aktywności to maksymalny czas, przez jaki wątek może pozostawać nieaktywny, zanim zostanie zamknięty.
- Kolejka wejściowa zawierająca
Runnable
zadania. Ta kolejka musi implementować Interfejs BlockQueue. Aby spełnić wymagania aplikacji, możesz: wybrać jedną z dostępnych implementacji kolejek. Aby dowiedzieć się więcej, zobacz te zajęcia omówienie obiektu ThreadPoolExecutor.
Oto przykład, który określa rozmiar puli wątków na podstawie łącznej liczby rdzeni procesora, czas utrzymywania aktywności wynoszący jedną sekundę i kolejkę wejściową.
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
);
...
}