Pull to refresh

Процессы и потоки в Android: пишем AsyncTask правильно

Reading time 7 min
Views 121K
Продолжаю свои повествования об Android. И в этот раз хочу поделиться ценной информацией о процессах и потоках, которая должна быть хорошо усвоена и всегда оставаться под рукой во избежании ошибок и недопонимания при написании приложений. В конце статьи приведу пример реализации AsyncTask, который загружает в ImageView картинку по нажатию на кнопку.

Прежде всего отмечу, что подробнее о данной теме можно прочесть в данном руководстве — developer.android.com/guide/topics/fundamentals/processes-and-threads.html

На заметку о процессах и потоках в Android

Когда запускается компонент приложения и приложение не имеет других запущенных компонентов, Android создает новый процесс для приложения с одним потоком исполнения. По умолчанию все компоненты одного приложения запускаются в одном процессе, в потоке называемом «главный». Если компонент приложения запускается и уже существует процесс для данного приложения(какой-то компонент из приложения существует), тогда компонент запущен в этом процессе и использует его поток выполнения. Вы можете изменить данное поведение, задав разные процессы для разных компонентов вашего приложения. Кроме того вы можете добавить потоки в любой процесс.

Задать отдельный процесс для компонента можно с помощью файла манифеста. Каждый тег компонента(activity, service, receiver и provider) поддерживает атрибут android:process. Данный атрибут позволяет задать процесс, в котором будет выполняться компонент. Также вы можете задать процесс в котором будут выполняться компоненты разных приложений. Также данный атрибут поддерживается тегом application, что позволяет задать определенный процесс для всех компонентов приложения.

Android пытается поддерживать процесс приложения как можно дольше, но когда потребуются ресурсы старые процессы будут вытеснены по иерархии важности.

Существует 5 уровней иерархии важности: (процессы первого уровня из списка будут удалены последними)

1.Процесс с которым взаимодействует пользователь(Foreground process)
К таким процессам относится например: активити с которым взаимодействует пользовать; сервис(экземпляр Service), с которым взаимодействует пользователь; сервис запущенный методом startForeground(); сервис, который выполняет один из методов своего жизненного цикла; BroadcastReceiver который выполняет метод onReceive().

2.Видимый процесс
Процесс, в котором не выполнены условия из пункта №1, но который влияет на то, что пользователь видит на экране. К примеру, вызван метод onPause() активити.

3.Сервисный процесс
Служба запущенная методом startService()

4.Фоновый процесс
Процесс выполняемый в фоновом режиме, который невиден пользователю.

5.Пустой процесс

Отмечу, что в компонентах приложения существует метод onLowMemory(), но полагаться на то, что данный метод будет вызван нельзя, также как нельзя на 100% полагаться на метод onDestroy(), поэтому логику сохранения данных или каких-либо настроек можно осуществить в методе onStop(), который(как уверяют) точно вызывается.

Когда запускается приложение, система создает «главный» поток выполнения для данного приложения, который также называется UI-потоком. Этот поток очень важен, так как именно в нем происходит отрисовка виджетов(кнопочек, списков), обработка событий вашего приложения. Система не создает отдельный поток для каждого экземпляра компонента. Все компоненты, которые запущенны в одном процессе будут созданы в потоке UI. Библиотека пользовательского интерфейса Android не является потоково-безопасной, поэтому необходимо соблюдать два важных правила:

1) Не блокировать поток UI
2) Не обращаться к компонентам пользовательского интерфейса не из UI-потока

Теперь, предположим, что у нас возникла задача — загрузить картину в ImageView из сети и тут же ее отобразить. Как мы поступим? По логике: мы создадим отдельный поток, который и сделает всю нашу работу, примерно так:
public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            Bitmap b = loadImageFromNetwork("http://example.com/image.png");
            mImageView.setImageBitmap(b);
        }
    }).start();
}

Выглядит правдоподобно, так как мы вынесли операцию загрузки картинки в отдельный поток. Проблема в том, что мы нарушили правило №2. Исправить эту проблему можно с помощью следующих методов:

Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)

К примеру, воспользуемся первым из них:
public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");
            mImageView.post(new Runnable() {
                public void run() {
                    mImageView.setImageBitmap(bitmap);
                }
            });
        }
    }).start();
}

Теперь реализация потоково-безопасная: сетевая операция выполняется в отдельном потоке, а к ImageView обращаемся из потока UI.
К счастью, данные операции можно объединить с помощью наследования класса Handler и реализации нужной логики, но лучшее решение — наследовать класс AsyncTask.

AsyncTask позволяет выполнить асинхронную работу и делать обновления пользовательского интерфейса.
Для обновления реализуйте метод onPostExecute(), а всю фоновую работу заключите в метод doInBackground(). После того, как вы реализуете свою собственную задачу, необходимо ее запустить методом execute().

Привожу обещанный пример AsyncTask, в котором реализована задача загрузки и отображения картинки(вариант с аннотациями и отклонением от применения стандартного протокола диалогов):
@EActivity(R.layout.main)             
public class DownloadImageActivity extends Activity {
	ProgressDialog progress;
	
	@ViewById 
	ImageView image;
	
	@ViewById
	Button runButt;
	
	String pictUrl = "http://www.bigfoto.com/sites/main/churfirsten_switzerland-xxx.JPG";
	
	@BeforeCreate
	void getProgressDialog(){
	    progress = new ProgressDialog(this);
	    progress.setMessage("Loading...");
	}
	
	@Click(R.id.runButt)
	void runClick(){
		new DownloadImageTask().execute(pictUrl);
	}
	
	class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
		@Override
		protected Bitmap doInBackground(String... params) {
		    publishProgress(new Void[]{}); //or null
			
		    String url = "";
		    if( params.length > 0 ){
		    	url = params[0];		    	
		    }

		    InputStream input = null;
	            try {
                       URL urlConn = new URL(url);
		       input = urlConn.openStream(); 
	            }
	            catch (MalformedURLException e) {
	           	e.printStackTrace();
		    }
                    catch (IOException e) {
	        	e.printStackTrace();
		    }
	        
	            return BitmapFactory.decodeStream(input);
		}
		
		@Override
		protected void onProgressUpdate(Void... values) {
		     super.onProgressUpdate(values);
		     progress.show();
		}
		
		@Override
		protected void onPostExecute(Bitmap result) {
		     super.onPostExecute(result);
		     progress.dismiss();
		     image.setImageBitmap(result);	
		}
	}
} 


А теперь рассмотрим самый правильный вариант с точки зрения работы с диалогами:
public class DownloadImageActivity extends Activity {
	ImageView image;
        Button runButt;
	
	final static String PICT_URL = "http://www.bigfoto.com/sites/main/churfirsten_switzerland-xxx.JPG";
	final int PROGRESS_DLG_ID = 666;
	final static String DEBUG_TAG = "+++ImageDownloader+++";
	
	@Override
	public void onCreate(Bundle savedInstanceState){
	    super.onCreate(savedInstanceState);
	    setContentView(R.layout.main);
	    
	    loadViews();
	}
	
	private void loadViews(){
		runButt = (Button)findViewById(R.id.runButt);
	    image =(ImageView)findViewById(R.id.image);
	}
	
	@Override
	protected Dialog onCreateDialog(int dialogId){
		ProgressDialog progress = null;
		switch (dialogId) {
		case PROGRESS_DLG_ID:
			progress = new ProgressDialog(this);
		        progress.setMessage("Loading...");
		    
			break;
		}
		return progress;
	}
	
	public void runButtonHandler(View button){
		if(button.getId() == R.id.runButt)
		    new DownloadImageTask().execute(PICT_URL);
	}
	
	class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
		@Override
		protected Bitmap doInBackground(String... params) {
			publishProgress(new Void[]{});
			
		    String url = "";
		    if( params.length > 0 ){
		    	url = params[0];		    	
		    }
		   
		    InputStream input = null;
		    
		    try {
		    	URL urlConn = new URL(url);
		        input = urlConn.openStream();
		    }
	        catch (MalformedURLException e) {
	        	Log.d(DEBUG_TAG,"Oops, Something wrong with URL...");
	        	e.printStackTrace();
			}
	        catch (IOException e) {
	        	Log.d(DEBUG_TAG,"Oops, Something wrong with inpur stream...");
				e.printStackTrace();
			}
	        
	        return BitmapFactory.decodeStream(input);
		}
		
		@Override
		protected void onProgressUpdate(Void... values) {
			super.onProgressUpdate(values);
			showDialog(PROGRESS_DLG_ID);
		}
		
		@Override
		protected void onPostExecute(Bitmap result) {
			super.onPostExecute(result);
			dismissDialog(PROGRESS_DLG_ID);
			image.setImageBitmap(result);	
		}
	}
}

Кода стало побольше, но лучше использовать стандартный протокол работы с диалогами.
Также я убрал все аннотации, чтобы новичкам было проще попробовать данный код.

Не забудьте добавить в свою разметку кнопочки атрибут с указанным значением: android:onClick=«runButtonHandler»

И добавлю: в оффициальном документе(Тыц ) также, как и в моем случае, не используется preExecute(), но если вам понадобится выполнить какие-то действия с вашим пользовательским интерфейсом до начала выполнения задачи, то смело используйте данный метод.

Параметры передаваемые в AsyncTask:
1. Параметры(в нашем случае адрес URL).
2. Прогресс (единицы задающие ход изменения задачи). В нашем случае не используется.
3. Результат выполнения задачи(в нашем случае объект Bitmap)

Код довольно прост: всю фоновую работу мы выполняем в методе doInBackGround(), вызывая метод publishProgress(), чтобы во время загрузки картинки крутился наш ProgressDialog при вызове метода onProgressUpdate(). После выполнения фоновой работы вызывается метод onPostExecute() в который передается результат работы метода doInBackGround(), в нашем случае это объект Bitmap и тут же мы его загружаем в ImageView.
Отмечу пару важных моментов, которые нужно учитывать:

1) Метод doInBackGround() выполняется в фоновом потоке, потому доступа к потоку UI внутри данного метода нет.
2) Методы onPostExecute() и onProgressUpdate() выполняются в потоке UI, потому мы можем смело обращаться к нашим компонентам UI.

Заключение

Да, я снова применил библиотеку android-annotations, так что не пугайтесь аннотациям.

Хочу отметить важность понимания модели работы процессов в Android, чтобы не допустить ошибок при разработке приложений.

Данная статья это переработка доступной информации + личный опыт при реализации указанной задачи и работе с потоками.

Как всегда пожелания и замечания по поводу материала статьи в личку. Если вам есть что-то добавить или дополнить — смело пишите в комментарии.

UPD Статья обновленна добавлением более правильной версии в плане работы с диалогами.
Tags:
Hubs:
+18
Comments 27
Comments Comments 27

Articles