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

    Продолжаю свои повествования об 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 Статья обновленна добавлением более правильной версии в плане работы с диалогами.
    Поделиться публикацией

    Комментарии 27

    • НЛО прилетело и опубликовало эту надпись здесь
        +4
        И еще, что это за дурацкая идея вызывать ProgressDialog с помощью publishProgress()? Чем вам onPreExecute() не угодил?

        И почему не инициализировать диалог с помощью onCreateDialog()?
          0
          И что тут по вашему дурацкого?
          publishProgress просто вызывает onProgressUpdate() в котором крутяшка.
            +1
            Да плохо тут то, что вы отводите начинающих разработчиков от стандартов. onPreExecute() предназначен для выполнения операций связанных с UI до начала выполнения Async Task, а onProgressUpdate() для отображения прогресса выполнения.

            Та же ерунда и с onCreateDialog().
              0
              Суть статьи — работа с потоками и понимание в каких методах AsyncTask можно использовать UI поток, а в каких нет. В данном случае я могу вообще всякие крутяшки убрать, потому что это лишнее, также хочу добавить, что стандартный протокол диалогов в Android это не панацея, хоть и удобная и хорошая практика.
                +2
                Ну зачем вы спорите? Вы пишите для начинающих, значит нужно придерживаться стандартов от Google. Если ваша основная цель показать «в каких методах AsyncTask можно использовать UI поток», то про onPreExecute забывать нельзя. Применение аннотаций в таком примере тоже спорно, даже если они вам очень-очень нравятся, т.к. читатели не смогут просто скопировать ваш код в редактор.

                Цель замечаний от всех — помочь вам в будущем писать более качественные статьи, поэтому воспринимать критику в штыки не надо. Удачи )
                  0
                  Ну люблю спорить, что поделаешь:)
                  Да я все учитываю и никаких штыков, спасибо за мнение:)

                  По поводу аннотаций: да, пожалуй тут они лишни.

                  Кроме того моя цель — хорошие статьи для любого уровня разработчиков, да и хочется писать структурированно и качественно.

                  Просто не хотелось много кода, а выделить основную часть.

                  Как время позволит, подправлю статью и покажу несколько вариантов.
              +1
              Не, ну ведь это затрудняет понимание логики автора кода. Если вам нужна только «крутяшка», логично засунуть её в onPreExecute(). publishProgress() для других целей предназначен.
            0
            Это Вы о чем сейчас?
              0
              Объекта еще нет, а он используется. Вопрос корректный :-)
                0
                Строки поменять местами нужно.
                  0
                  Извиняюсь, в хабраредакторе быстро вносил правки…
            • НЛО прилетело и опубликовало эту надпись здесь
                –7
                не нашел ничего полезного для себя, избитая тема.
                  +3
                  Не нашел? Проходи дальше. Зачем обсырать то?
                  0
                  а где я обсырал можно узнать?
                    +4
                    С одной стороны вы пишите, что статья для новичков, а сдругой зачем то:
                    > Да, я снова применил библиотеку android-annotations, так что не пугайтесь аннотациям.

                    Вот это совершенно не нужно, было делать. Мало того то пользователи не смогут проверить у себя ваш код, так еще эти анотации не используются ни где кроме подделок.

                    Вопрос по аннотациям:
                    @ViewById
                    Button runButt;

                    @Click(R.id.runButt)
                    void runClick(){
                    new DownloadImageTask().execute(pictUrl);
                    }

                    Это значит что кнопку будут два раза по иерархии объектов искать?
                      0
                      Скоро будет апдейт статьи, где уберу всё лишнее.

                      По аннотациям: тут нужно почитать доку, либо исходники либы, вполне вероятно, что Вы правы. Но в данном случае, я лишь хотел показать, что к аннотациям можно добавлять идентификаторы(если их не указывать, то либа будет искать вьюшку по названию переменной). Как в первом случае.
                      +1
                      А что будет в такой ситуации: запускаем долгий AsyncTask, который получает данные из сети (например картинку) и устанавливает её в ImageView. Во время загрузки переходим на другую activity (например home). После этого наша активити уже не foreground. Что будет происходить с тредом, порождённым AsyncTask'ом (остановится ли он как-то или нет)?.. Установится ли картинка в ImageView или вообще какой то эксепшн вылетит? Давно хотел попробовать, но руки как-то не доходили.
                        0
                        при описанной выше реализации все накроется медным тазом даже нa cменe ориентейшнa
                          0
                          Интересный момент, который легко может возникнуть и у пользователя.

                          По интуиции: эксепшена может и не быть, но картинка установится и когда вернется снова в данной активити юзер, то он не увидит ее. Загрузка то у нас в фоновом процессе.
                            0
                            Если активити не в foregroгnd, для неё вполне может вызваться onDestroy. Что тогда будет после этого с кодом в AsyncTask, который выставляет в загруженный bitmap в ImageView. Интересно, в этом случае не вызовится onDestroy, прекратится AsyncTask или вылетит эксепшн?
                              0
                              Если приложение разрушится, то возможно и вылетит, ну а так то главный поток(UI-поток) останется работающим если активити было несколько. Но все этого догадки, проверьте на практике и расскажите что получилось. Будет полезно.
                          0
                          Кстати, в версиях до Honeycomb в главном потоке только не рекомендовалось делать «networking». В новых же версиях android'a политика ужесточилась и при попытке выполнить HttpClient execute бросится NetworkOnMainThreadException.

                          Еще на официальном сайте есть небольшая статья с советами, как избежать ANR (Application Not Responding).
                            0
                            > Каждый тег компонента(,, и )

                            Кажется, теги пропали.
                            0
                            Почему-то никто не упомянул про получение результата через get(). Однажды с этим столкнулся, это блокировало основной поток. Не делайте так, найдите любой другой способ передать результат, но не через этот зловещий метод ;)

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое