9 komentarze

AsyncTask – asynchroniczne wykonywanie czasochłonnych zadań

Sierpień 19, 2011 Tutoriale Wielowątkowość

Android, jak większość dzisiejszych systemów operacyjnych wspiera wielowątkowość. Nie każdy jednak zdaje sobie sprawę z tego, że aby z niej skorzystać, musimy jawnie określić jakie zadania naszej aplikacji mają być wykonywane asynchronicznie, tj. poza głównym wątkiem aplikacji. Należy bowiem pamiętać, że wszystkie komponenty naszej aplikacji – zarówno te widoczne (Aktywności), jak i te, które teoretycznie pracują w tle (Broadcast Receivers, Usługi) uruchomione są w tym samym wątku głównym (UI Thread – nazwa pochodzi od tego, że zajmuje się on m.in. rysowaniem obiektów, przesyłaniem zdarzeń do komponentów czy ogólną interakcją aplikacji z wszystkimi widokami i widżetami).
Problem zaczyna pojawiać się wtedy, gdy któryś z komponentów wykonuje czasochłonną czynność. Blokuje ona bowiem wykonanie innych ważnych zadań głównego wątku (rysowanie, obsługa zdarzeń interfejsu). Z perspektywy użytkownika, który nie musi być świadomy tego, co tak naprawdę dzieje się w urządzeniu, wygląda to na zawieszenie się aplikacji.

ANR – strażnik płynności interfejsu

Aby zapobiec wspomnianym zawieszeniom, w systemie funkcjonuje mechanizm dbający o płynność działania interfejsu. Jego zadaniem jest sprawdzanie czy aplikacja nie łamie jednej z dwóch zasad:

  • Czas reakcji interfejsu na zdarzenie (dotknięcie ekranu, naciśnięcie przycisku itp.) musi być mniejszy niż 5 sekund.
  • Czas wykonania zadania przez BroadcastReceiver musi być mniejszy niż 10 sekund.

Kiedy któraś z powyższych zasad jest łamana, wyświetlane zostaje okno dialogowe ANR (Application Not Responding). Daje ono użytkownikowi możliwość zatrzymania naszej aplikacji, co skutkuje jej awaryjnym wyłączeniem.

Wykorzystanie wielowątkowości

Oczywiście nie powinniśmy dopuścić do tego by nasza aplikacja kiedykolwiek spowodowała wyświetlenie ANR. W tym celu powinniśmy upewnić się, że główny wątek wykonuje minimalną ilość zadań, szczególnie tych, które są czasochłonne. Wszystkie inne czynności powinny być przenoszone do osobnych wątków, których praca nie blokuje głównego wątku aplikacji.

Komponenty UI nie są jest thread-safe

Nim jednak przejdziemy do omówienia mechanizmów pracy w tle (tej prawdziwej, realizowanej na osobnym wątku), warto wspomnieć o jednej ważnej kwestii. Komponenty, z których składa się interfejs aplikacji nie są przystosowane do wielowątkowej pracy (m.in. ze względów wydajnościowych), w związku z czym wszelkie operacje wykonywane na nich muszą być przeprowadzane z poziomu wątku głównego. Gdybyśmy chcieli wykonać jakąkolwiek czynność na obiekcie interfejsu z innego wątku otrzymamy wyjątek:

CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

Na szczęście Android dostarcza nam mechanizmu, który znacząco upraszcza wykonywanie zadań w osobnym wątku oraz pozwala na łatwą integrację z komponentami interfejsu użytkownika.

Obsługa asynchronicznych zadań przy pomocy AsyncTask

Abstrakcyjna klasa AsyncTask jest prostym mechanizmem, który pozwala na przeniesienie czasochłonnych zadań do nowego wątku. Oprócz tego umożliwia ona wykonywanie zadań w głównym wątku aplikacji (UI Thread), dzięki czemu w łatwy sposób możemy sterować interfejsem użytkownika.

Budowa klasy AsyncTask

Oto przykład klasy rozszerzającej AsyncTask, z kompletem metod, które możemy zaimplementować:

	private class MyTask extends AsyncTask<Void, Void, Void> {

		@Override
		protected void onPreExecute() {
			super.onPreExecute();
		}

		@Override
		protected Void doInBackground(Void... params) {
			return null;
		}

		@Override
		protected void onProgressUpdate(Void... values) {
			super.onProgressUpdate(values);
		}

		@Override
		protected void onPostExecute(Void result) {
			super.onPostExecute(result);
		}

		@Override
		protected void onCancelled() {
			super.onCancelled();
		}

	}

Zaznaczona metoda doInBackgrund(Params… params) jest jedyną, którą musimy zaimplementować. Reszta z nich jest opcjonalna.

Idąc kolejno, tworzenie klasy zaczynamy od podania parametrów klasy AsyncTask:

AsyncTask<Params, Progress, Result>

Pierwszym z nich jest typ danych wejściowych, jakie możemy przekazać wątkowi, drugim typ danych przedstawiających postęp w działaniu, trzecim wynik zwracany przez zadanie. Jeżeli któregoś nie potrzebujemy, wystarczy że ustawimy typ Void. Należy pamiętać, że zgodnie z definicją typów generycznych w Javie, nie możemy korzystać tutaj z typów prymitywnych (int, boolean, float etc.).

Metody, które możemy przeciążyć dziedzicząc pod AsyncTask to:

  • onPreExecute() – wywoływana zaraz przed uruchomieniem właściwego zadania. Metoda ta wykonywana jest w wątku głównym aplikacji, dzięki czemu możemy w niej skonfigurować interfejs aplikacji (np. wyświetlić ProgressBar albo zablokować przyciski).
  • doInBackground(Params… params)jedyna metoda, która uruchamiana jest w oddzielnym wątku. To tutaj powinniśmy umieścić wszystkie czasochłonne czynności. Nie ma dostępu do wątku głównego aplikacji, w związku z czym jedynym sposobem na zaktualizowanie stanu interfejsu jest wywołanie metody publishProgress(Progress… values).
  • onProgressUpdate(Progress… values)metoda wywoływana jest w momencie wywołania publishProgress(…) wspomnianego powyżej. Również wykonywana jest w głównym wątku aplikacji, w związku z czym służy ona do aktualizacji informacji o postępie wykonywanej czynności.
  • onPostExecute(Result result)wywoływana w momencie zakończenia pracy doInBackground(…). Argumentem metody jest wynik zwrócony przez doInBackground(…). Również i tutaj mamy dostęp do komponentów interfejsu, w związku z czym z tego miejsca konfigurujemy wszystkie widoki po wykonaniu pracy.
  • onCancelled() - w związku z tym, że możemy w każdej chwili anulować wykonywanie zadania (metoda cancel(…)), możemy zaimplementować obsługę takiego stanu. W takim wypadku jednak metoda doInBackground(…) będzie nadal wykonywana do końca. Natomiast po jej zakończeniu zamiast wykonać onPostExecute(…) wykonana będzie metoda onCancelled().

Podkreślam, że typy argumentów z metod doInBackground(…), onProgressUpdate(…) oraz onPostExecute(…) są parametrami klasy AsyncTask.

Pracę zaczynamy po wywołaniu metody execute(Params… params) na obiekcie naszego zadania.

new MyTask().execute("Argument1", "Argument2");

Oczywiście powyższe wywołanie będzie poprawne tylko wtedy, gdy parametrem Params będzie typ String. (AsyncTask<String, Void, Void>).

Przykładowa aplikacja

Uwaga - poniższy opis jest tylko najprostszym przykładem wykorzystania AsyncTask. Jeżeli chcesz skorzystać z tego rozwiązania polecam kod źródłowy zaktualizowany przez użytkownika delor, który rozwinął aplikację o kilka niezbędnych funkcjonalności (link). Szczegóły w komentarzach tego artykułu.

Zbudujemy teraz przykładową aplikację, która wykorzysta klasę AsyncTask. Zadaniem aplikacji będzie pobranie i wyświetlenie wszystkich obrazków naszych kontaktów w widoku GridView.

Przygotowania

Oto kod źródłowy layoutu naszej aplikacji:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent">
	<LinearLayout
		android:layout_width="match_parent"
		android:id="@+id/linearLayout1"
		android:layout_height="wrap_content">
		<TextView
			android:layout_width="match_parent"
			android:gravity="center"
			android:text="Friends Grid"
			android:textAppearance="?android:attr/textAppearanceLarge"
			android:layout_weight="1"
			android:layout_height="match_parent"></TextView>
		<ProgressBar
			style="?android:attr/progressBarStyleSmall"
			android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:layout_margin="15dp"
			android:id="@+id/pbWheel"
			android:visibility="invisible"></ProgressBar>
		<Button
			android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:id="@+id/btnLoadImages"
			android:text="Load images"></Button>
	</LinearLayout>
	<ProgressBar
		style="?android:attr/progressBarStyleHorizontal"
		android:layout_height="wrap_content"
		android:layout_width="match_parent"
		android:id="@+id/pbHorizontal"
		android:visibility="invisible"></ProgressBar>
	<GridView
		android:layout_width="match_parent"
		android:numColumns="3"
		android:id="@+id/gvFriends"
		android:layout_height="match_parent"></GridView>
</LinearLayout>

Elementy ProgressBar będą służyły do informowania o wykonywaniu pracy w tle, w związku z czym należy pamiętać o zarządzaniu ich widocznością.

Do wypełnienia obrazkami widoku GridView potrzebujemy Adaptera, który się tym zajmie. Oto jego kompletny kod źródłowy – ImagesAdapter.java:

package pl.froger.hello.asynctask;

import java.util.List;

import android.content.Context;
import android.graphics.Bitmap;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;

public class ImagesAdapter extends BaseAdapter {
	private Context context;
	private List<Bitmap> images;

	public ImagesAdapter(Context context, List<Bitmap> images) {
		this.context = context;
		this.images = images;
	}

	@Override
	public int getCount() {
		return images.size();
	}

	@Override
	public Object getItem(int position) {
		return images.get(position);
	}

	@Override
	public long getItemId(int position) {
		return 0;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		ImageView imageView;
		if(convertView == null) {
			imageView = new ImageView(context);
			imageView.setLayoutParams(new GridView.LayoutParams(85, 85));
			imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
			imageView.setPadding(8, 8, 8, 8);
		} else {
			imageView = (ImageView) convertView;
		}
		imageView.setImageBitmap(images.get(position));
		return imageView;
	}
}

W związku z tym, że będziemy odczytywać dane naszej listy kontaktów, musimy również dodać odpowiednie pozwolenie do pliku AndroidManifest.xml:

<uses-permission android:name="android.permission.READ_CONTACTS" />

Kod źródłowy głównej Aktywności, który za chwile rozbudujemy wygląda tak:

public class MainActivity extends Activity {
	private GridView gvFriends;
	private Button btnLoadImages;
	private ProgressBar pbWheel;
	private ProgressBar pbHorizontal;

	private ArrayList<Bitmap> images;
	private ImagesAdapter imagesGridAdapter;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        gvFriends = (GridView) findViewById(R.id.gvFriends);
        btnLoadImages = (Button) findViewById(R.id.btnLoadImages);
        pbHorizontal = (ProgressBar) findViewById(R.id.pbHorizontal);
        pbWheel = (ProgressBar) findViewById(R.id.pbWheel);
        initButtonOnClick();
        initGrid();
    }

	private void initButtonOnClick() {
		btnLoadImages.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				loadFriendImagesAsynchronously();
			}
		});
	}

	private void initGrid() {
		images = new ArrayList<Bitmap>();
        imagesGridAdapter = new ImagesAdapter(getApplicationContext(), images);
        gvFriends.setAdapter(imagesGridAdapter);
	}
}

Teraz zajmiemy się implementacją zaznaczonej metody – loadFriendImagesAsynchronously().

Pobieranie danych w tle

Zdefiniujemy teraz zagnieżdżoną klasę naszego zadania.

	private class ImageLoaderTask extends AsyncTask<Void, Integer, Void> {
		@Override
		protected Void doInBackground(Void... params) {
	    	images.clear();
	    	Cursor c = getAllContacts();
	    	if(c.moveToFirst()) {
	    		do {
	    			Bitmap image = loadContactPhoto(c.getInt(0));
	    			if(image != null) images.add(image);
	    		} while (c.moveToNext());
	    	}
	    	c.close();
			return null;
		}

		private Cursor getAllContacts() {
			Uri uri = Contacts.CONTENT_URI;
			String[] projection = { Contacts._ID };
			return getContentResolver().query(uri, projection, null, null, null);
		}

		private Bitmap loadContactPhoto(long id) {
		    Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, id);
		    InputStream input = Contacts.openContactPhotoInputStream(getContentResolver(), uri);
		    return BitmapFactory.decodeStream(input);
		}
	}

Tak mniej więcej wygląda implementacja czasochłonnego zadania, wykonywanego w tle. Pobieramy tu wszystkie dostępne zdjęcia, które następnie dodajemy do listy zdjęć zdefiniowanej w naszej Aktywności.
Warto zwrócić uwagę na parametry klasy AsyncTask. Typem danych odpowiedzialnych za postęp wykonywania zadania jest Integer. Za jego pomocą będziemy wypełniać pasek stanu ProgressBar.

Teraz zaimplementujemy jeszcze 3 metody naszego zadania.

Najpierw ustawiamy widoki tak by wskazywały, na to, że zadanie jest wykonywane.

		@Override
		protected void onPreExecute() {
			super.onPreExecute();
			btnLoadImages.setEnabled(false);
			pbHorizontal.setVisibility(View.VISIBLE);
			pbWheel.setVisibility(View.VISIBLE);
			pbHorizontal.setProgress(0);
		}

Następnie zajmujemy się informowaniem o postępie wykonywanych prac:

		@Override
		protected Void doInBackground(Void... params) {
	    	images.clear();
	    	Cursor c = getAllContacts();
	    	if(c.moveToFirst()) {
	    		do {
	    			Bitmap image = loadContactPhoto(c.getInt(0));
	    			if(image != null) images.add(image);
	    	    	publishProgress(c.getPosition() / c.getColumnCount());
	    		} while (c.moveToNext());
	    	}
	    	c.close();
			return null;
		}

		private Cursor getAllContacts() {
			Uri uri = Contacts.CONTENT_URI;
			String[] projection = { Contacts._ID };
			return getContentResolver().query(uri, projection, null, null, null);
		}

		private Bitmap loadContactPhoto(long id) {
		    Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, id);
		    InputStream input = Contacts.openContactPhotoInputStream(getContentResolver(), uri);
		    return BitmapFactory.decodeStream(input);
		}

		@Override
		protected void onProgressUpdate(Integer... values) {
			super.onProgressUpdate(values);
			pbHorizontal.setProgress(values[0]);
		}

Na końcu ustawiamy interfejs użytkownika tak jak powinien wyglądać po skończonym zadaniu (ukrywamy komponenty ProgressBar, wyświetlany dane w GridView).

		@Override
		protected void onPostExecute(Void result) {
			super.onPostExecute(result);
			btnLoadImages.setEnabled(true);
			pbHorizontal.setVisibility(View.GONE);
			pbWheel.setVisibility(View.INVISIBLE);
			gvFriends.invalidateViews();
		}

Teraz pozostało nam już tylko uruchomienie zadania w odpowiednim momencie. Oto kompletny kod źródłowy głównej Aktywności naszej aplikacji:

package pl.froger.hello.asynctask;

import java.io.InputStream;
import java.util.ArrayList;

import android.app.Activity;
import android.content.ContentUris;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.ContactsContract.Contacts;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.GridView;
import android.widget.ProgressBar;

public class MainActivity extends Activity {
	private GridView gvFriends;
	private Button btnLoadImages;
	private ProgressBar pbWheel;
	private ProgressBar pbHorizontal;

	private ArrayList<Bitmap> images;
	private ImagesAdapter imagesGridAdapter;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        gvFriends = (GridView) findViewById(R.id.gvFriends);
        btnLoadImages = (Button) findViewById(R.id.btnLoadImages);
        pbHorizontal = (ProgressBar) findViewById(R.id.pbHorizontal);
        pbWheel = (ProgressBar) findViewById(R.id.pbWheel);
        initButtonOnClick();
        initGrid();
    }

	private void initButtonOnClick() {
		btnLoadImages.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				loadFriendImagesAsynchronously();
			}
		});
	}

	private void initGrid() {
		images = new ArrayList<Bitmap>();
        imagesGridAdapter = new ImagesAdapter(getApplicationContext(), images);
        gvFriends.setAdapter(imagesGridAdapter);
	}

	private class ImageLoaderTask extends AsyncTask<Void, Integer, Void> {
		@Override
		protected void onPreExecute() {
			super.onPreExecute();
			btnLoadImages.setEnabled(false);
			pbHorizontal.setVisibility(View.VISIBLE);
			pbWheel.setVisibility(View.VISIBLE);
			pbHorizontal.setProgress(0);
		}

		@Override
		protected Void doInBackground(Void... params) {
	    	images.clear();
	    	Cursor c = getAllContacts();
	    	if(c.moveToFirst()) {
	    		do {
	    			Bitmap image = loadContactPhoto(c.getInt(0));
	    			if(image != null) images.add(image);
	    	    	publishProgress(c.getPosition() / c.getColumnCount());
	    		} while (c.moveToNext());
	    	}
	    	c.close();
			return null;
		}

		private Cursor getAllContacts() {
			Uri uri = Contacts.CONTENT_URI;
			String[] projection = { Contacts._ID };
			return getContentResolver().query(uri, projection, null, null, null);
		}

		private Bitmap loadContactPhoto(long id) {
		    Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, id);
		    InputStream input = Contacts.openContactPhotoInputStream(getContentResolver(), uri);
		    return BitmapFactory.decodeStream(input);
		}

		@Override
		protected void onProgressUpdate(Integer... values) {
			super.onProgressUpdate(values);
			pbHorizontal.setProgress(values[0]);
		}

		@Override
		protected void onPostExecute(Void result) {
			super.onPostExecute(result);
			btnLoadImages.setEnabled(true);
			pbHorizontal.setVisibility(View.GONE);
			pbWheel.setVisibility(View.INVISIBLE);
			gvFriends.invalidateViews();
		}
	}

	private void loadFriendImagesAsynchronously() {
		new ImageLoaderTask().execute();
	}
}

Zrzuty ekranu

Kompletny kod źródłowy

Cały projekt dostępny jest na naszym Githubie – HelloAsyncTask.
Zaktualizowana wersja dostępna jest na Githubie użytkownika delorHelloAsyncTask.

Komentarze (9) Subskrybuj

 

  1. Fajny, prosty artykuł. Czytam Cię już od paru miesięcy zaczynając od bloga i muszę przyznać, że teraz się nie popisałeś. Trzeba było pokombinować żeby oprócz ProgressBara odświeżał się także widok zdjęć. Tak aby użytkownik widział jak się po kolei dodają kolejne obrazki :)

  2. Celem wpisu było maksymalnie proste wprowadzenie bo pracy z AsyncTask. To o czym mówisz może być dobrym materiałem na snippet i być może kiedyś takie rozwiązanie zostanie tu zamieszczone. ;) A na razie dziękuję za komentarz. ;)

  3. Fajny artykuł. Mam poboczne pytanie dotyczące pobierania zdjęć z kontaktów. Sprawdzałeś czy pobiera zdjęcia zintegrowane z fb? :)

  4. Nie, nie pobiera. Gdzieś już czytałem o tym problemie, ale przyznam, że nie zagłębiałem się w niego. W każdym razie w bazie, w kolumnie PHOTO_ID przy tych kontaktach występuje jakiś identyfikator inny niż 0, a problem występuje przy pobieraniu zdjęcia poprzez Contacts.openContactPhotoInputStream(…), która zwraca null.

  5. delor pisze:

    Na Github zrobiłem forka i dodałem trochę zmian. Najważniejsza to ta umożliwiająca pracę taska pomimo obracania ekranu.

  6. Dzięki. Wpis został zaktualizowany – podlinkowałem również Twój projekt i zawarłem informację o nim na początku opisu przykładowej aplikacji.

  7. Pawel pisze:

    Jak nie znalem tego mechanizmu androida AsyncTask to korzystalem z mechanizmu watkow w javie i rozszerzalem klase o extends Thread, ale nie mogłem przesyłać informacji z tego wątku do MainActivity, ta stronka bardzo mi w tym pomogła, wielkie dzieki

  8. Ja używam z powodzeniem rozszerzonej klasy Thread. Do komunikacji z klasą Aktywności wykorzystuje obiekt typu Handler. Ale powyższa metoda także jest bardzo interesująca i na pewno warto ją znać.

  9. Nieco krótki ten artykuł, mimo to za to jaki wartościowy :-) Może warto go trochę ulepszyć ? Ale i tak dziękuje za publikowane wpisy, bowiem rewelacyjnie się je czyta. Do usłyszenia!

Prześlij komentarz

Zaloguj się lub skorzystaj z profilu:

[rpxlogin redirect="http://www.android4devs.pl" prompt="" style="large"]

Możesz również zostawić komentarz bez rejestracji, korzystając z poniższego formularza:

Musisz być zalogowany aby móc pisać komentarze.