Android od podstaw: operacje w tle
Polish (Polski) translation by Mateusz Kurlit (you can also view the original English article)
Oprócz najbardziej podstawowych aplikacji na Androida, wszystko co budujesz będzie wymagało w pewnym stopniu wykorzystania wątków w tle do przeprowadzenia operacji. Wszystko dlatego, że Android posiada coś znanego jako ANR (Application Not Responsive), który pojawia się, gdy operacja w wątku UI trwa ponad pięć sekund, uniemożliwiając użytkownikowi wprowadzanie danych i powodując zawieszenie się aplikacji.
Aby tego uniknąć, musisz przenieść dłużej trwające operacje, takie jak żądania sieci lub zapytania do bazy danych, do różnych wątków, aby umożliwić użytkownikowi korzystanie z aplikacji. Chociaż dogłębna analiza wątków jest dużym i złożonym tematem w informatyce, w tym poradniku przedstawię podstawowe pojęcia wątków w systemie Android i niektóre z narzędzi dostępnych, które pomogą ci zbudować aplikacje działające wydajnie dzięki procesom w tle.
Łatwiej uczysz się za pomocą wideo? Obejrzyj nasz kurs:
Omówienie wątków
Po uruchomieniu aplikacji, startuje nowy proces Linuxa z pojedynczym głównym wątkiem wykonywalnym. Posiada on dostęp do zestawu narzędzi UI Androida, nasłuchuje wpisywane przez użytkownika dane, obsługuje rysowanie na ekranie urządzenia Android. W związku z tym, powszechnie określany jest jako wątek UI.
Wszystkie komponenty aplikacji działają domyślnie wewnątrz tego samego wątku i procesu, jednakże można utworzyć dodatkowe wątku w celu odsunięcia zadań od wątku UI i uniknięcia ANR. Jeśli mówimy o wątkach w systemie Android, istnieją dwie proste zasady do zapamiętania, które umożliwiają poprawne funkcjonowanie aplikacji.
- Nie blokuj wątku UI.
- Nie próbuj uzyskać dostępu do komponentów UI Androida spoza wątku UI.
Chociaż możesz zastosować do pierwszej zasady tworząc nowy Thread
i Runnable
, poradzenie sobie z drugą regułą będzie trochę bardziej skomplikowane. Przeanalizujmy poniższy fragment kodu:
new Thread(new Runnable() { public void run() { try { Thread.sleep(6000); } catch( InterruptedException e ) { } mTextView.setText("test"); } }).start();
Powyższy kod nie opóźni wątku UI, gdy nowy wątek ominie ANR, jednakże próba ustawienia obiektu TextView
spowoduje, że aplikacja wyrzuci następujący błąd:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
Na szczęście, istnieje kilka prostych sposobów na pozbycie się go. Możesz użyć metody runOnUiThread(Runnable)
, aby wykonać kod z powrotem w głównym wątku aplikacji.
mTextView = (TextView) findViewById(R.id.text); new Thread(new Runnable() { public void run() { try { Thread.sleep(6000); } catch( InterruptedException e ) { } runOnUiThread(new Runnable() { @Override public void run() { mTextView.setText("test"); } }); } }).start();
Możesz również wziąć standardowy obiekt View
i użyć post
na Runnable
.
new Thread(new Runnable() { public void run() { try { Thread.sleep(6000); } catch( InterruptedException e ) { } mTextView.post(new Runnable() { @Override public void run() { mTextView.setText("test"); } }); } }).start();
Chociaż obie sztuczki pomogą zabezpieczyć twoje wątki, w miarę budowania coraz bardziej skomplikowanych aplikacji stanie się to bardziej kłopotliwe.
AsyncTask
AsyncTast
jest jednym z narzędzi dostępnych w systemie Android pozwalającym na zarządzanie złożonością wątków w tle. AsyncTask
dostarcza wątek roboczy do operacji blokujących, a następnie udostępnia wyniki z powrotem w wątku UI ze wstępnie utworzoną metodą wywołania zwrotnego, pozwalając na łatwe wykonywanie zadań bez konieczności bawienia się wątkami i uchwytami.
Cykl życia AsyncTask
Przed rozpoczęciem pracy z klasą AsyncTask
, musisz zrozumieć jej cykl życia w porównaniu do operacji w wątku głównym.



Pierwsza metoda wywoływana przez AsyncTask
to onPreExecute()
. Uruchamia ona wątek UI oraz ustawia wszystkie komponenty interfejsu potrzebne do zakomunikowania użytkownikowi, że coś się dzieje.
Po zakończeniu onPreExecute()
, wywoływana jest doInBackground(T)
. To ogólna nazwa oznaczająca informację, która musisz przekazać do metody, aby wykonać jej zadanie. Na przykład, jeśli piszesz zadanie, które pobiera JSON z adresu URL, należy przekazać URL do tej metody jako String
. Gdy operacja robi postęp w doInBackground()
, możesz wywołać onProgressUpdate(T)
w celu aktualizacji UI (na przykład paska postępu na ekranie). Tutaj ogólną wartością reprezentującą postęp jest Integer
.
Gdy metoda doInBackground()
zostanie zakończona, może zwrócić obiekt, który jest przekazywany do onPostExecute(T)
, na przykład JSONObject
pobrany z początkowego adresu URL. onPostExecute(T)
działa w wątku UI.
Podczas tworzenia klasy AsyncTask
, musisz nadpisać te obiekty zarówno w deklaracji klasy jak i w powyższych metodach. Poniżej znajduje się przykład AsyncTask
aktualizującej ProgressBar
co sekundę.
protected class DemoAsyncTask extends AsyncTask<Integer, Void, String> { @Override protected void onPreExecute() { super.onPreExecute(); mProgress.setProgress(0); mProgress.setVisibility(View.Visible); } @Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); mProgress.setProgress(values[0]); } @Override protected Void doInBackground(Void... params) { for( int i = 0; i < 100; i++ ) { try { Thread.sleep(1000); } catch( InterruptedException e ) {} publishProgress(i); } return "All done!"; } @Override protected void onPostExecute(String result) { super.onPostExecute(aVoid); if( isCancelled() ) { return; } mProgress.setVisibility(View.GONE); Toast.makeText(context, result, Toast.LENGTH_SHORT).show(); } }
Tutaj warto zauważyć, że onPostExecute(T)
sprawdza warunek isCancelled()
. To dlatego, że istnieje jeden duży problem z AsyncTask
: zarządza ona odniesieniem do Context
, nawet jeśli Context
został zniszczony.
Najlepiej to widać podczas uruchamiania AsyncTask
i obracania ekranu. Jeśli spróbujesz odnieść element Context
(taki jak View
lub Activity
) po zniszczeniu oryginalnego Context
, wyrzucony zostanie błąd Exception
. Najłatwiejszym sposobem na obejście tego jest wywołanie cancel(true)
w AsyncTask
w metodzie onDestroy()
aktywności lub fragmentu, a następnie sprawdzenie czy zadanie nie zostało anulowane w onPostExecute(T)
.
Jak wszystko w programowaniu, odpowiedź na pytanie kiedy korzystać z AsyncTask
brzmi: to zależy. Proste w użyciu AsyncTasks
nie są rozwiązaniem wszystkich kwestii dotyczących wątków, ale zdecydowanie najlepszym dla krótki operacji trwających góra kilka sekund. Jeśli posiadasz operację, która może trwać dłużej, zalecam zainteresowanie się ThreadPoolExecutor
, Service
, lub GcmNetworkManager
(kompatybilna wstecznie wersja JobScheduler
)
Usługi
Kiedy potrzebujesz przeprowadzić długotrwałe operacje w tle, takie jak odtwarzanie muzyki, transakcje sieciowe lub współpraca z dostawcą treści, rozważ wykorzystanie Service
. Podstawowy obiekt Service
może istnieć w dwóch stanach: uruchomionym i przypisanym:
Uruchomiony Service
zostaje rozpoczęty przez komponent w twojej aplikacji i pozostanie aktywny w tle urządzenia, nawet jeśli oryginalny komponent został zniszczony. Kiedy zadanie przeprowadzane przez uruchomiony Service
zostanie zakończone, Service
zostanie zatrzymany. Standardowo uruchomiony Service
jest generalnie wykorzystywany do długotrwałych zadań w tle, które nie wymagają komunikacji z pozostałą częścią aplikacji.
Podobnie działa przypisany Service
, który również dostarcza wywołanie zwrotne dla różnych komponentów aplikacji, które mogą się z nim związać. Gdy wszystkie przypisane komponenty odłączą się od Service
, zostanie on zatrzymany. Zwróć uwagę, że te dwa sposoby na uruchomienie Service
nie wykluczają się wzajemnie—możesz uruchomić Service
, który będzie działał bezterminowo i posiadał związane z nim komponenty.
IntentService
Jednym z największych problemów ze standardowym Service
jest brak wsparcia dla wielu jednoczesnych żądań, ponieważ byłoby to koszmarem wielowątkowości. Sposobem na obejście tego jest rozszerzenie IntentService
, który rozszerza standardowy Service
. IntentService
tworzy domyślny wątek roboczy do wykonywania wszystkich intencji, które są odbierane w onStartCommand()
tak, aby wszystkie operacje były przeprowadzane poza głównym wątkiem. Następnie tworzy kolejkę roboczą służącą do wysyłania po kolei każdej intencji do onHandleIntent()
, dzięki czemu nie trzeba martwić się problemami z wielowątkowością.
Oprócz obsługi wątków, IntentService
zatrzymuje się automatycznie, gdy wszystkie uruchomione żądania zostały zakończone. Ponieważ wszystkie szczegóły dotyczące implementacji są obsługiwane w IntentService
, twoja praca jako deweloper jest dość prosta.
public class ExampleIntentService extends IntentService { //required constructor with a name for the service public ExampleIntentService() { super("ExampleIntentService"); } @Override protected void onHandleIntent(Intent intent) { //Perform your tasks here try { Thread.sleep(5000); } catch (InterruptedException e) {} } }
Podsumowanie
W tym poradniku dowiedziałeś się wiele o wątkach i wielowątkowości w systemie Android. Na temat wątków zostały napisane całe książki, ale powinieneś teraz posiadać podstawy do zaprogramowania ogólnych zadań i zrozumieć szczegółową dokumentację dla przyszłych i bardziej złożonych aplikacji na Androida.