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

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

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

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

Thread

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

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

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

AsyncTask

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

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

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

Примечание: на самом деле можно запустить несколько 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.

Итого:
  • Операции, требующие установки приоритета выполнения. Операции, активно расходующие ресурсы CPU.
  • Выполнение операции множество раз, через какой-либо интервал времени.
  • Параллельное выполнение нескольких фоновых потоков.
  • Операции, на выполнение которых ожидается потратить не больше нескольких секунд. Загрузка небольшого количества данных, простые операции с файловой системой.
  • Активное управление элементами интерфейса из фоновых потоков.
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 можно отобразить список контактов телефона.

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

Итого:

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

Service и IntentService

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

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

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

Итого:

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

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

Пример DownloadManager.

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

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

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

Итого:

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

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

📎📎📎📎📎📎📎📎📎📎