Компания
57,31
рейтинг
9 сентября 2013 в 10:15

Разработка → Отзывчивое Android-приложение или 1001 способ загрузить картинку

О реализации многопоточности при разработки Android-приложений уже написано немало. В этой же статье хочется провести сравнение нескольких распространенных на сегодня способов скачать/прочитать/сохранить/посчитать, при этом не дав пользователю повода для раздражения. Постараться понять, когда то или иное решение будет уместным, а чего лучше не делать вовсе. Попытаемся показать, почему привычных вещей, таких как класс Thread и пакет java.util.concurrent оказывается недостаточно, когда речь заходит об Android-приложении.

У статьи нет задачи осветить все подробности реализации каждого подхода, но сравнить их, не рассказав основы, невозможно. А посему…

Thread

Мигрировавший из Java в Android API класс Thread, пожалуй, самый простой способ запустить новый поток. Вот пара примеров, как это делается: можно создать наследника от Thread или передать в экземпляр класса Thread объект, реализующий интерфейс Runnable.

Пример 1. Расширение Thread.
class WorkingThread extends Thread{
    @Override
    public void run() {
        //Фоновая работа
    }
}





Пример 2. Runnable.
class WorkingClass implements Runnable{
@Override
    public void run() {
        //Фоновая работа
    }
}

WorkingClass workingClass = new WorkingClass();
Thread thread = new Thread(workingClass);
thread.start();


Как правило, после выполнения требуемых операций появляется потребность предоставить результат пользователю. Но нельзя просто взять и получить доступ к элементам UI из другого потока. В силу модели многопоточности Android, изменять состояние элементов интерфейса разрешается только из того потока, в котором эти элементы были созданы, иначе будет вызвано исключение CalledFromWrongThreadException. На этот случай Android API предоставляет сразу несколько решений.

Пример 1. View#post(Runnable action).
public class MainActivity extends Activity {
    
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        textView = (TextView)findViewById(R.id.hello);
                
        WorkingClass workingClass = new WorkingClass();
        Thread thread = new Thread(workingClass);
        thread.start();
    }
    
    class WorkingClass implements Runnable{
        @Override
        public void run() {
            //Фоновая работа
            
            //Отправить в UI поток новый Runnable
            textView.post(new Runnable() {
                @Override
                public void run() {
                    textView.setText("The job is done!");
                }
            });
        }
    }   
}



Пример 2. Activity#runOnUiThread(Runnable action).
public class MainActivity extends Activity {
    
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        textView = (TextView)findViewById(R.id.hello);
                
        WorkingClass workingClass = new WorkingClass();
        Thread thread = new Thread(workingClass);
        thread.start();
    }
    
    class WorkingClass implements Runnable{
        @Override
        public void run() {
            //Фоновая работа
            
            //Отправить в UI поток новый Runnable
            MainActivity.this.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    textView.setText("The job is done!");
                }
            });
        }
    }   
}



Пример 3. Handler.
public class MainActivity extends Activity {
    
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        textView = (TextView)findViewById(R.id.hello);
                
        WorkingClass workingClass = new WorkingClass(true);
        Thread thread = new Thread(workingClass);
        thread.start();
    }
    
    class WorkingClass implements Runnable{
        
        public static final int SUCCESS = 1;
        public static final int FAIL = 2;
        
        private boolean dummyResult;
        
        public WorkingClass(boolean dummyResult){
            this.dummyResult = dummyResult;
        }       
        
        @Override
        public void run() {
            //Фоновая работа
            
            //Отправить в хэндлеру сообщение
            if (dummyResult){
                //Можно отправить пустое сообщение со статусом
                uiHandler.sendEmptyMessage(SUCCESS);
            } else {
                //Или передать в месте с сообщением данные
                Message msg = Message.obtain();
                msg.what = FAIL;
                msg.obj = "An error occurred";
                uiHandler.sendMessage(msg);
            }
        }
    } 

    Handler uiHandler = new Handler(new Handler.Callback() {
        
        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
            case WorkingClass.SUCCESS:
                textView.setText("Success");
                return true;
            case WorkingClass.FAIL:
                textView.setText((String)msg.obj);
                return true;
            }
            return false;
        }
    });  
}



В целом просто, но когда дело доходит до активного взаимодействия с элементами интерфейса, код может превратиться в нагромождение Runnable-интерфейсов или немалого размера Handler-класс. Для упрощения работы по синхронизации главного и фоновых потоков уже в версии Android 1.5 был предложен класс AsyncTask

AsyncTask

Для использования AsyncTask необходимо создать его класс-наследник с указанием параметризованных типов и переопределить нужные методы. После запуска AsyncTask вызовет свои методы в следующем порядке: onPreExecute(), doInBackground(Params...), onPostExecute(Result), причем первый и последний из них будут вызваны в UI потоке, а второй, как легко догадаться, в отдельном. Более того, класс AsyncTask позволяет во время выполнения фонового процесса информировать UI поток о ходе его выполнения с помощью метода publishProgress(Progress...), который в свою очередь вызовет в UI потоке onProgressUpdate(Progress...).

Пример AsyncTask
public class MainActivity extends Activity {
    
    private static final String IMAGE_URL = "http://eastbancgroup.com/images/ebtLogo.gif";
    
    TextView textView;
    ImageView imageView;
    ProgressDialog progressDialog;
    DownloadTask downloadTask;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        textView = (TextView)findViewById(R.id.hello);
        imageView = (ImageView)findViewById(R.id.imageView);
                
        downloadTask = new DownloadTask();
        //Запускаем задачу, передавая ей ссылку на картинку
        downloadTask.execute(IMAGE_URL);
    }
    
    @Override
    protected void onStop() {
        //Завершить загрузку картинки сразу,
        //как закроется Activity
        downloadTask.cancel(true);
        super.onStop();
    }
    
    /*
     * При расширении класса  AsyncTask<Params, Progress, Result>
     * необходимо указать, какими типами будут его generic-параметры.
     * Params - тип входных данных. В нашем случае будет String, т.к. 
     * передаваться будет url картинки
     * Progress - тип данных, которые будут переданы для обновления прогресса.
     * В нашем случае Integer. 
     * Result - тип результата. В нашем случае Drawable.
     */
    class DownloadTask extends AsyncTask<String, Integer, Drawable>{
        
        @Override
        protected void onPreExecute() {
            //Отображаем системный диалог загрузки
            progressDialog = new ProgressDialog(MainActivity.this);
            progressDialog.setIndeterminate(false);
            progressDialog.setMax(100);
            progressDialog.setProgress(0);
            progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
            progressDialog.setMessage("Downloading Image");
            progressDialog.show();
        }

        @Override
        protected Drawable doInBackground(String... params) {
            //В этом методе происходит загрузка картинки через
            //стандартный класс URLConnection
            int count;
            try {
                URL url = new URL(params[0]);
                URLConnection conection = url.openConnection();
                conection.connect();
                int lenghtOfFile = conection.getContentLength();
                InputStream input = new BufferedInputStream(url.openStream(), 8192);
                OutputStream output = new FileOutputStream("/sdcard/downloadedfile.jpg");
                byte data[] = new byte[256];     
                long total = 0;
     
                while ((count = input.read(data)) != -1) {
                    
                    //Проверяем, актуальна ли еще задача
                    if (isCancelled()){
                        return null;
                    }
                    total += count;  
                    output.write(data, 0, count);
                    
                    //Информирование о закачке.
                    //Передаем число, отражающее процент загрузки файла
                    //После вызова этого метода автоматически будет вызван
                    //onProgressUpdate в главном потоке
                    publishProgress((int)((total*100)/lenghtOfFile));
                }
                output.flush(); 
                output.close();
                input.close();
     
            } catch (Exception e) {
                Log.e("Error: ", e.getMessage());
            }
     
            String imagePath = Environment.getExternalStorageDirectory().toString() + "/downloadedfile.jpg";
            return Drawable.createFromPath(imagePath);
        }
        
        @Override
        protected void onProgressUpdate(Integer... progress) {
            progressDialog.setProgress(progress[0]);
        }
        
        //Скроем диалог и покажем картинку
        @Override
        protected void onPostExecute(Drawable result) {
            imageView.setImageDrawable(result);
            progressDialog.dismiss();
        }
        
        //Этот метод будет вызван вместо onPostExecute,
        //если мы остановили выполнение задачи методом 
        //AsyncTask#cancel(boolean mayInterruptIfRunning)
        @Override
        protected void onCancelled() {

        }
    }
}




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

Хотя AsyncTask зачастую удобнее создания потоков классом Thread, бывают случаи, в которых первый способ реализации многопоточности окажется более выигрышным. Вот важные, на наш взгляд, отличия, принятие во внимание которых может помочь при выборе способа реализации многопоточности:
  • Используя AsyncTask невозможно задать приоритет новому потоку, как это можно было бы сделать методом Thread#setPriority(int priority) https://developer.android.com/reference/java/lang/Thread.html#setPriority(int)
  • Начиная с Android HONEYCOMB по умолчанию для всех background операций экземпляров AsyncTask отводится только один поток.

Второй пункт особенно важен. Дело в том, что из-за такой модели работы задач на их основе нельзя сделать долгоживущий фоновый процесс (как, например, таймер). Такой экземпляр AsyncTask забьет собой очередь и не даст быть запущенными любые последующие задачи. Зато используя связку Thread и Handler, наоборот, достаточно просто добиться выполнения кода через промежутки времени.

Пример таймера.
public class MainActivity extends Activity {
        
    TextView textView;
    private int counter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        counter = 0;
        textView = (TextView)findViewById(R.id.hello);
        
        //старт таймера
        new Thread(new WorkingClass()).start();
    }
    
    class WorkingClass implements Runnable{
        
        public static final int RELAUNCH = 1;
        private static final int DELAY = 1000;

        @Override
        public void run() {
            //фоновая операция
            
            //отправим сообщение хендлеру с задержкой в 1000ms
            uiHandler.sendEmptyMessageDelayed(RELAUNCH, DELAY);
        }
        
    }
    
    Handler uiHandler = new Handler(new Handler.Callback() {
        
        @Override
        public boolean handleMessage(Message msg) {
            //перезапустим поток
            if (msg.what == WorkingClass.RELAUNCH){
                textView.setText("Times: "+counter);
                counter++;
                new Thread(new WorkingClass()).start();
                return true;
            }
            return false;
        }
    });
}



Примечание: на самом деле можно запустить несколько AsyncTask параллельно, при помощи метода AsyncTask#executeOnExecutor(Executor exec, Params… params) https://developer.android.com/reference/android/os/AsyncTask.html, если вам это действительно нужно.

В предыдущих примерах и Thread, и AsyncTask используются в контексте некой Activity. В большинстве случаев это нормально, однако такая модель может принести определенные проблемы. Нужно понимать, что работающие, хоть и в background’е, AsyncTask или Thread не позволят сборщику мусора удалить экземпляр нашей Activity, когда он будет больше не нужен. А случиться это может очень просто, например, при повороте экрана девайса. При каждой смене ориентации экрана будет создаваться новая Activity, и каждый раз будет вызываться AsyncTask. Чем больше будет размер загружаемой картинки, тем быстрее приложение закроется с ошибкой OutOfMemoryError. Хуже такого примера может быть, разве что, использование анонимных классов, как это показывается во многих учебных статьях. Не сохраняя ссылку на новую задачу или поток вы лишаете себя возможности контролировать ход процесса, например, остановить его выполнение при закрытии той же Activity.

Итого:

Из сравнения классов Thread и AsyncTask можно сделать несколько выводов.
Задачи, при решении которых оправдано использование Thread:
  • Операции, требующие установки приоритета выполнения. Операции, активно расходующие ресурсы CPU.
  • Выполнение операции множество раз, через какой-либо интервал времени.
  • Параллельное выполнение нескольких фоновых потоков.

Задачи, при решении которых оправдано использование AsyncTask:
  • Операции, на выполнение которых ожидается потратить не больше нескольких секунд. Загрузка небольшого количества данных, простые операции с файловой системой.
  • Активное управление элементами интерфейса из фоновых потоков.

Главное условие, накладываемое на работу с Thread и AsyncTask: если работа была запущена в контексте Activity/Fragment, то и закончиться она должна по возможности сразу, после остановки Activity/Fragment.

Loaders

Существуют виды операций с данными, выполнение которых хоть и позволительно в главном потоке приложения, но может заметно затормозить интерфейс или даже вызвать ANR сообщение. Показательный пример такой операции — чтение из базы данных/файлов. До недавнего времени хорошей практикой работы с БД было использование уже рассмотренных Thread и AsyncTask, но в Android 3.0 были добавлены такие классы как Loader и LoaderManager, цель которых упростить асинхронную загрузку данных в Activity или Fragment. Для платформ старых версий эти же классы доступны в android support library.

Принцип работы с Loader’ами таков:

1. Нужно создать собственный класс, расширяющий класс Loader или один из его стандартных наследников.
2. Реализовать в нем загрузку данных generic-типа D
3. В Activity получить ссылку на LoaderManager и инициализировать свой Loader, передав его и callback LoaderManager.LoaderCallbacks менеджеру.

Приведем пример, как при помощи стандартного класса CursorLoader можно отобразить список контактов телефона.

Пример Loader.
public class MainActivity extends ListActivity implements LoaderCallbacks<Cursor> {

	//поля из базы данных контактов
	static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
        Contacts._ID,
        Contacts.DISPLAY_NAME,
        Contacts.CONTACT_STATUS,
    };

	private static final int LOADER_ID = 1;	
	private SimpleCursorAdapter adapter;
	TextView textview;	

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		//Текст, видимый во время загрузки контактов
		textview = (TextView)findViewById(R.id.loading);

		//Скрываем список контактов, пока они не загрузятся
		getListView().setVisibility(View.GONE);

		//Адаптер для ListView
	    adapter = new SimpleCursorAdapter(this,
                android.R.layout.simple_list_item_2, null,
                new String[] { Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS },
                new int[] { android.R.id.text1, android.R.id.text2 }, 0);

	    setListAdapter(adapter);

	    //Инициализация Loader'а
	    //передаем мэнеджеру id Loader'а и callback
	    LoaderManager lm = getLoaderManager();
	    lm.initLoader(LOADER_ID, null, this);
	}

	//Здесь мы должны сконструировать Loader, который будет
	//использоваться для обращения к БД контактов
	@Override
	public Loader<Cursor> onCreateLoader(int id, Bundle args) {
		Uri baseUri = Contacts.CONTENT_URI;

        String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND ("
                + Contacts.HAS_PHONE_NUMBER + "=1) AND ("
                + Contacts.DISPLAY_NAME + " != '' ))";
        return new CursorLoader(this, baseUri,
                CONTACTS_SUMMARY_PROJECTION, select, null,
                Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
	}

	//Метод будет вызван, когда загрузка будет завершена
	//Используем готовый курсор, что бы отобразить список контактов
	@Override
	public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
		 switch (loader.getId()) {
	      case LOADER_ID:
	    	adapter.swapCursor(cursor);
	    	textview.setVisibility(View.GONE);
	    	getListView().setVisibility(View.VISIBLE);
	        break;
	    }
	}

	@Override
	public void onLoaderReset(Loader<Cursor> loader) {
		adapter.swapCursor(null);
	}	
}



Не забудьте указать соответствующее разрешение на чтение контактов в манифесте приложения.

Итого:

Использование шаблона Loaders тесно связанно с компонентами приложения, ответственными за отображение (Activity, Fragment) и потому время выполнения операций по загрузке данных должно быть сопоставимо с временем жизни этих компонентов.

Service и IntentService

Service – это один из компонентов Android приложения. Сам по себе сервис не является отдельным процессом или отдельным потоком. Однако, сервис имеет собственный жизненный цикл, и он как раз подходит для выполнения в нем длинных по времени операций. Дополнительные потоки, запущенные в контексте сервиса могут выполняться, не мешая навигации пользователя по приложению. Для общение между сервисом и другими компонентами приложения обычно используется два способа: интерфейсами ServiceConnection/IBinder или broadcast-сообщениями. Суть первого способа — получение ссылки на запущенный экземпляр сервиса. Нельзя сказать, что такой способ как-то решает проблемы многозадачности, он скорее подходит для управления сервисом. А общение с помощью broadcast-сообщений как раз потокобезопасно и потому будет рассмотрено в примере.

Пример сервиса.
public class BackgroundService extends Service {
    
    public static final String CHANNEL = BackgroundService.class.getSimpleName()+".broadcast";
    
    //Этот метод будет вызван всякий раз,
    //когда сервису будет передан новый Intent
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //здесь вы можете запустить новый поток или задачу
        
        sendResult();
        return Service.START_NOT_STICKY;
    }

    //После завершения работы информируйте об этом,
    //разослав Broadcast
    private void sendResult() {
        Intent intent = new Intent(CHANNEL);
        sendBroadcast(intent);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}



public class MainActivity extends Activity {
        
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        textView = (TextView)findViewById(R.id.hello);      
        
        //Подписываемся на события нашего сервиса
        registerReceiver(receiver, new IntentFilter(BackgroundService.CHANNEL));
        
        //Запускаем сервис, передавая ему новый Intent
        Intent intent = new Intent(this, BackgroundService.class);
        startService(intent);
    }
    
    @Override
    protected void onStop() {
        //Отписываемся от событий сервиса
        unregisterReceiver(receiver);
        super.onStop();
    }
    
    
    private BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            textView.setText("Message from Service");
        }
    };
}



Не забудьте, что сервис, так же как и Activity, должен быть объявлен в манифесте проекта.

Вдобавок, Android API предоставляет класс IntentService, расширяющий стандартный Service, но выполняющий обработку переданных ему данных в отдельном потоке. При поступлении нового запроса IntentService сам создаст новый поток и вызовет в нем метод IntentService#onHandleIntent(Intent intent) https://developer.android.com/reference/android/app/IntentService.html#onHandleIntent(android.content.Intent), который вам остается только переопределить. Если при поступлении нового запроса обработка предыдущего еще не закончилась, он будет поставлен в очередь.

Пример IntentService.
public class DownloadService extends IntentService {
	
	public DownloadService() {
		super("DownloadService");
	}

	public static final String CHANNEL = DownloadService.class.getSimpleName()+".broadcast";

	private void sendResult() {
		Intent intent = new Intent(CHANNEL);
	    sendBroadcast(intent);
	}

	@Override
	public IBinder onBind(Intent intent) {
		return null;
	}

	//Этот метод вызвается автоматически и в отдельном потоке
	@Override
	protected void onHandleIntent(Intent intent) {
		//фоновая операция
		
		//отправка сообщения о завершении операции
		sendResult();
	}
}

public class MainActivity extends Activity {
		
	TextView textView;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		textView = (TextView)findViewById(R.id.hello);		
		
		//Подписываемся на события нашего сервиса
		registerReceiver(receiver, new IntentFilter(DownloadService.CHANNEL));
		
		//Запускаем сервис, передавая ему новый Intent
		Intent intent = new Intent(this, DownloadService.class);
		startService(intent);
	}
	
	@Override
	protected void onStop() {
		//Отписываемся от событий сервиса
		unregisterReceiver(receiver);
		super.onStop();
	}
	
	
	private BroadcastReceiver receiver = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
			textView.setText("Message from Service");
		}
	};
}



Итого:

Жизненный цикл сервисов, как правило, дольше, чем Activity. Стартовав однажды, сервис будет жив, пока у него не закончится работа, после чего он самостоятельно остановится. Разработчику в основном остается лишь организовать желаемую обработку входящих сообщений (интентов): сравнить, сконструировать очередь и т.п., и посылать сообщения о завершении каждой операции.
Как можно заметить из примеров, не важно, откуда будет послано broadcast-сообщение, главное, что его получение произойдет в главном потоке.

DownloadManager
Начиная с версии Android API 9 задача по загрузке и сохранению файлов через сеть становится еще проще, благодаря системному сервису DowloadManager. Все, что остается сделать, это передать этому сервису Uri, если хотите, указать текст, который будет показан в области уведомлений во время и после загрузки и подписаться на события, который DownloadManager может рассылать Этот сервис возьмет на себя установление коннекта, реагирование на ошибки, возобновление закачки, создание уведомлений в Notification bar и, конечно, саму загрузку файлов в фоновом потоке.

Пример DownloadManager.

Пример DownloadManager
public class MainActivity extends Activity {

	private static final String IMAGE_URL = "http://eastbancgroup.com/images/ebtLogo.gif";

	ImageView imageView;
	DownloadManager downloadManager;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		imageView = (ImageView)findViewById(R.id.imageView);

		//Получаем ссылку на DownloadManager сервис
		downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
        
		//Создаем новый запрос
		Request request = new Request(Uri.parse(IMAGE_URL));
        request.setTitle("Title"); //заголовок будущей нотификации
        request.setDescription("My description"); //описание будущей нотификации
        request.setMimeType("application/my-mime"); //mine type загружаемого файла
        
        //Установите следующий флаг, если хотите,
        //что-бы уведомление осталось по окончании загрузки
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
        //Добавляем запрос в очередь
        downloadManager.enqueue(request);
	}

	@Override
	protected void onResume() {
		super.onResume();
		//Подписываемся на сообщения от сервиса
		registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
		registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_NOTIFICATION_CLICKED));
	};

	@Override
	protected void onPause() {
		super.onPause();
		//Отписываемся от сообщений сервиса
		unregisterReceiver(receiver);
	};

	BroadcastReceiver receiver = new BroadcastReceiver() {

		@Override
		public void onReceive(Context context, Intent intent) {
			String action = intent.getAction();

			//Сообщение о том, что загрузка закончена
			if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)){
				long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0);
				DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
				DownloadManager.Query query = new DownloadManager.Query();			
				query.setFilterById(downloadId);
				Cursor cursor = dm.query(query);
	            if (cursor.moveToFirst()){
            		int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
                    if (DownloadManager.STATUS_SUCCESSFUL == cursor.getInt(columnIndex)) {                   	
                        String uriString = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
                        imageView.setImageURI(Uri.parse(uriString));
                    }
	            }

	        //Сообщение о клике по нотификации
			} else if (DownloadManager.ACTION_NOTIFICATION_CLICKED.equals(action)){
				DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
				//несколько параллельных загрузок могут быть объеденены в одну нотификацию,
				//по этому мы пытаемся получить список всех загрузок, связанных с 
				//выбранной нотификацией
				long[] ids = intent.getLongArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS);
				DownloadManager.Query query = new DownloadManager.Query();
	            query.setFilterById(ids);
	            Cursor cursor = dm.query(query);
	            int idIndex = cursor.getColumnIndex(DownloadManager.COLUMN_ID);
	            if (cursor.moveToFirst()){
	            	do {
	            		//здесь вы можете получить id загрузки и
	            		//реализовать свое поведение
	            		long downloadId = cursor.getLong(idIndex);

	            	} while (cursor.moveToNext());
	            }
			}
		}	
	};
}


В работе DownloadManager’а есть одна особенность. Дело в том, что при клике на нотификацию об успешной загрузке файла вопреки ожиданиям не будет разослано broadcast-сообщение типа DownloadManager.ACTION_NOTIFICATION_CLICKED. Но вместо этого, сервис попытается найти Activity, которая сможет обработать этот клик. Так что, если вы хотите реагировать на это событие, то добавьте в манифесте проекта к нужной activity новый intent-фильтр примерно такого содержания:

<intent-filter>
<action android:name="android.intent.action.VIEW" />
            <data android:mimeType="application/my-mime" /> (mime type, указанный в загрузке)
            <category android:name="android.intent.category.DEFAULT" />
</intent-filter>


В этом случае при клике на нотифиацию будет запущена ваша activity, в которую уже будет передан Intent с идентификатором загрузки. Получить его можно, например, так:

Intent intent = getIntent();
String data = intent.getDataString();

Итого:

Сервис DownloadManager удобно использовать для загрузки больших файлов, которые могут представлять интерес пользователю отдельно от вашего приложения, например, изображения, медиа-файлы, архивы и многое другое. Имейте в виду, что доступ к загруженным вами файлам могут получить и другие приложения.

Нельзя сказать, что мы осветили все шаблоны реализации фоновой работы android-приложения, но с большой степенью уверенности можем сказать, что рассмотренные способы распространены весьма широко. Надеемся эта статья поможет вам спроектировать background-работу наиболее правильно и удобно.
Автор: @eastbanctech

Комментарии (17)

  • +4
    А вообще грузите-ка картинки лучше с помощью этого, а не плодите своих костылей.
    • +5
      эта библиотека <наверно> здорово помогает в загрузке и кешировании картинок, но в статье картинки — только пример.
    • 0
      загрузка картинок — очень типовая операция в андроиде и, конечно, уже есть тысячи библиотек для загрузки картинок, но в статье помимо примеров с картинкой еще много чего про фоновые процессы :)
      • 0
        Я понимаю, просто озвучил 1002 способ =)
  • –10
    После таких статей всегда думаю «как же хорошо что в iOS есть GCD»
    • –5
      Минусуют из зависти, очевидно.
      • +2
        Минусуют наверное из-за того, что ждут конкретного примера преимуществ
  • 0
    > работающие [...] AsyncTask [...] не позволят сборщику мусора удалить экземпляр нашей Activity
    Можно держать пул AsyncTask'ов и в onStop останавливать их.
    Для конкретно этой задачи — загрузки картинок — мне в своей практике обычно хватало расширенного AsyncTask'a на пару десятков строк.
    Но в любом случае, интересное сравнение альтернатив.
  • 0
    >Начиная с Android HONEYCOMB по умолчанию для всех background операций экземпляров AsyncTask отводится только один поток.

    Не совсем точно, так как начиная со следующей версии количество увеличено до 128
    • 0
      Вы, скорее всего, говорите о максимальном размере пула у ThreadPoolExecutor, экземпляр которого использует AsyncTask. Однако, запустить задачу в этом экзекуторе можно только методом task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, <остальные параметры>);
      Если посмотреть исходники AsyncTask любой из последних версий android (например, тут grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.3_r2.1/android/os/AsyncTask.java), то можно заметить, что коммандой AsyncTask#execute(…) задача будет передана не в ThreadPoolExecutor, а в дефолтный SerialExecutor с одним потоком.
      • 0
        Надеюсь мой комментарий будет полезен.
        Разумеется, один AsyncTask = один поток, но:
        По вашей же ссылке важный участок кода в классе дефолтного SerialExecutor тут:
        protected synchronized void scheduleNext() {
                    if ((mActive = mTasks.poll()) != null) {
                        THREAD_POOL_EXECUTOR.execute(mActive);
                    }
                }

        Потому количество потоков AsyncTask (в версии API по указанной вами ссылке) будет ограничено значением:
        private static final int CORE_POOL_SIZE = 5;
        private static final int MAXIMUM_POOL_SIZE = 128;

        В API 19 же значение также отличается:
        private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
        private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
        private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
  • +1
    Вы не написали в чём же главное преимущество Loader'ов: их жизненный цикл отвязан от конкретной активности и управляется LoaderManager'ом. Это решает очень распространённую в 2.х проблему: вы начали загрузку чего-то тяжёлого в отдельном потоке (в асинктаске или руками созданном потоке, не суть), а активность пересоздалась (перевернули девайс). Асинтаска в панике — возвращать результат некуда, а вам нужно либо начинать загрузку заново либо придумывать велосипед по сохранению результатов загрузки. Loader'ы полностью решают эту проблему: вы просите у менеджера экземпляр лоадера по идентификатору. Если лоадер не был создан — менеджер его создаст, иначе — отдаст существующий экземпляр. То есть инициировать создание лоадера может одна активность, а получить результат уже совсем другая.

    А на счёт Thread и AsyncTask я слышал мнение, что всегда предпочтительно использовать асинктаски (даже если операция никак не влияет на интерфейс), поскольку асинктаски используют общий пул потоков, что сводит к минимуму риск создания нового тяжеловесного потока.
    • 0
      Есть еще такая потрясающая штука, как HandlerThread. А еще есть такая штука, как onConfigurationChanged(). Для меня пересоздание активити — бессмысленный гемор. Лучше при повороте экрана, в крайнем случае, спокойно пересоздать contentView (или изначально разбить его на несколько фрагментов, и потом просто перетасовать в новые ViewGroup, сохранив все listener'ы).
  • 0
    Ох, зачем такие велосипеды городить для запуска повторяющихся задач. Есть же замечательные методы schedule и scheduleAtFixedRate что у Timer, что у (если хочется погибче управлять потоками) ScheduledExecutorService. По крайней мере, там не надо после каждого выполнения явно заказывать выполниться ещё раз.
    • 0
      Ну и небольшой оффтопик, в ListActivity не надо управлять видимостью ListView вручную, там автоматически оно скрывается и показывается вьюха с android:id="android:id/empty", для того такая Activity и есть не в последнюю очередь.
  • 0
    До недавнего времени хорошей практикой работы с БД было использование уже рассмотренных Thread и AsyncTask, но в Android 3.0 были добавлены такие классы как Loader и LoaderManager


    Thread? AsyncTask? Правда? Ну если уж говорить о хороших практиках до Honeycomb, я бы упомянул в первую очередь как способ работы с БД, класс AsyncQueryHandler
  • 0
    К сказанному выше я бы хотел добавить, что, работая напрямую с Thread, следует иметь ввиду, что по-умолчанию новый поток создается с таким же приоритетом, что и UI-поток. И, если вы наплодите их штук 10, делающих более-менее серьёзную работу, об «отзывчивости» можете забыть. Я обычно в таких случаях руками понижаю приоритет до Process.THREAD_PRIORITY_BACKGROUND.

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

Самое читаемое Разработка