У статьи нет задачи осветить все подробности реализации каждого подхода, но сравнить их, не рассказав основы, невозможно. А посему…
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)
- Начиная с 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), если вам это действительно нужно.
В предыдущих примера и Thread, и AsyncTask используются в контексте некой Activity. В большинстве случаев это нормально, однако такая модель может принести определенные проблемы. Нужно понимать, что работающие, хоть и в background’е, AsyncTask или Thread не позволят сборщику мусора удалить экземпляр нашей Activity, когда он будет больше не нужен. А случиться это может очень просто, например, при повороте экрана девайса. При каждой смене ориентации экрана будет создаваться новая Activity, и каждый раз будет вызываться AsyncTask. Чем больше будет размер загружаемой картинки, тем быстрее приложение закроется с ошибкой OutOfMemoryException. Хуже такого примера может быть, разве что, использование анонимных классов, как это показывается во многих учебных статьях. Не сохраняя ссылку на новую задачу или поток вы лишаете себя возможности контролировать ход процесса, например, остановить его выполнение при закрытии той же 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), который вам остается только переопределить. Если при поступлении нового запроса обработка предыдущего еще не закончилась, он будет поставлен в очередь.
Пример 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.
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-работу наиболее правильно и удобно.
ссылка на оригинал статьи http://habrahabr.ru/company/eastbanctech/blog/192998/
Добавить комментарий