21 июля 2010

Многопоточность — как средство повышения эффективности

Разработка под Android
Перевод
Автор оригинала: Gill Debunne
Хорошей практикой в создании быстро реагирующего приложения является уверенность, что ваш пользовательский интерфейс требует минимального времени для обработки. Каждое потенциально долгое действие, которое может повесить Ваше приложение, нужно вывести в отдельный поток. Типичными примерами таких действий являются сетевые операции, которые несут в себе непредсказуемые задержки. Пользователи могут мириться с небольшими паузами, особенно, если вы информируете их о прогрессе, а вот застывшее на месте приложение не дает им выбора, кроме как закрыть его.

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


Загрузчик картинок



Загрузка изображений из Интернета очень проста и делается с помощью родственного HTTP класса из фреймворка. Вот одна из реализаций:

static Bitmap downloadBitmap(String url) {
final AndroidHttpClient client = AndroidHttpClient.newInstance("Android");
final HttpGet getRequest = new HttpGet(url);

try {
HttpResponse response = client.execute(getRequest);
final int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
Log.w("ImageDownloader", "Error " + statusCode + " while retrieving bitmap from " + url);
return null;
}

final HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream inputStream = null;
try {
inputStream = entity.getContent();
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
return bitmap;
} finally {
if (inputStream != null) {
inputStream.close();
}
entity.consumeContent();
}
}
} catch (Exception e) {
// Может обеспечить более подробное сообщение об ошибке при IOException или IllegalStateException
getRequest.abort();
Log.w("ImageDownloader", "Error while retrieving bitmap from " + url, e.toString());
} finally {
if (client != null) {
client.close();
}
}
return null;
}


* This source code was highlighted with Source Code Highlighter.


Клиент и HTTP-запрос созданы. Если запрос успешен, то ответный поток будет содержать в себе картинку, которая затем будет превращена в итоговую пиктограмму. Для нормальной работы вашего приложения, его манифест должен требовать INTERNET.
Осторожно: баг в предыдущей версии BitmapFactory.decodeStream может помешать выполнению Вашего кода при медленном соединении. Во избежание данной проблемы используйте FlushedInputStream(inputStream). Вот пример этого вспомогательного класса:

static class FlushedInputStream extends FilterInputStream {
public FlushedInputStream(InputStream inputStream) {
super(inputStream);
}

@Override
public long skip(long n) throws IOException {
long totalBytesSkipped = 0L;
while (totalBytesSkipped < n) {
long bytesSkipped = in.skip(n - totalBytesSkipped);
if (bytesSkipped == 0L) {
int byte = read();
if (byte < 0) {
}
totalBytesSkipped += bytesSkipped;
}
return totalBytesSkipped;
}
}




Это грантирует, что skip() действительно пропустит именно столько байт когда мы достигнем конца файла.

Если вы вдруг используете этот способ напрямую в методе ListView из ListAdapter, то в результате Вы получите невероятно прерывистый скроллинг.

Более того, это очень плохая идея, т.к. AndroidHttpClient не может быть запущен вместе с основным потоком. Вместо этого данный код будет отображать ошибку «This thread forbids HTTP requests» («Этот поток теряет НТТР запросы»). Лучше используйте DefaultHttpClient, если вы действительно желаете вставить себе палку в колеса.

Об асинхронных задачах



Класс AsyncTask предоставляет один из простейших путей запустить новую задачу прямо из потока пользовательского интерфейса. Давайте создадим класс ImageDownloader, который будет отвечать за создание этих задач. Он предоставит метод download, который присвоит загруженное изображение с его URL в ImageView:

public class ImageDownloader {

public void download(String url, ImageView imageView) {
BitmapDownloaderTask task = new BitmapDownloaderTask(imageView);
task.execute(url);
}
}

/* класс BitmapDownloaderTask, смотри ниже */
}


* This source code was highlighted with Source Code Highlighter.


BitmapDownloaderTask это асинхронная задача, которая на самом деле и качает картинку. Она стартует используя execute, которая возвращает значение немедленно, делая этот метод реально быстрым. Вот пример реализации данного класса:

class BitmapDownloaderTask extends AsyncTask<String, Void, Bitmap> {
private String url;
private final WeakReference<ImageView> imageViewReference;
public BitmapDownloaderTask(ImageView imageView) {
imageViewReference = new WeakReference<ImageView>(imageView);
}
@Override
// Actual download method, run in the task thread
protected Bitmap doInBackground(String... params) {
// params comes from the execute() call: params[0] is the url.
return downloadBitmap(params[0]);
}
@Override
// Once the image is downloaded, associates it to the imageView
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) {
bitmap = null;

}
if (imageViewReference != null) {
ImageView imageView = imageViewReference.get();
if (imageView != null</fonthttp://habrahabr.ru/edit/topic/99631/#>) {
imageView.setImageBitmap(bitmap);
}}}}


Метод doInBackground фактически работает над задачей в своем собственном процессе. Он будет просто использовать метод downloadBitmap, который мы ввели вначале данной статьи.

onPostExecute запускается в вызванном потоке пользовательского интерфейса когда задача выполнена. Он берет изображение, получившееся в результате, как параметр, связанный с imageView из download и хранящийся в BitmapDownloaderTask. Помните, что imageView хранится как WeakReference, так что в процессе загрузки не помешает очистить его от собранного мусора. Это объясняет почему мы должны убедиться, что и ссылки, и imageView имеют ненулевое значение(т.е. не были собраны), прежде чем использовать их в onPostExecute.

Этот пример показывает упрощенное использование AsyncTask, и если вы испопробуете этот способ, то увидите, что эти несколько строк кода значительно увеличивают производительность в ListView, которая теперь будет прокручиваться плавно.

Тем не менее, специфическое поведение ListView вызывает проблему в нашей реализации. Более того, по соображениям эффективности, ListView перерабатывает изображения, показывающиеся на экране во время скроллинга. Если один объект будет пропущен, то он будет использоваться imageView неоднократно. Другой раз пиктограмма отображается правильно во время загрузки, и заменяется изображением после загрузки. Но в чем же проблема? Как и в большинстве параллельных приложений вся проблема в упорядочении. В нашем случае нет гарантии, что загрузка задач закончится в том же порядке, в котором началась загрузка. Результат может быть таким, что показанное изображение будет относиться к предыдущему объекту в списке, т.к. оно потребовало больше времени для загрузки. Это не вопрос, если скачанные вами изображения связаны единожды, и для всех заданы ImageViews, но давайте исправим это в общем случае, когда они используются в виде списка.

Управление параллельностью



Чтобы решить эту проблему мы должны запомнить порядок начала загрузок, т. к. последняя начатая загрузка — самая эффективная для показа(?). Действительно, достаточно для каждого ImageView запомнить его последнюю загрузку. Мы будем задавать эту дополнительную информацию используя специальный Drawable подкласс, который будет связываться с ImageView пока идет загрузка. Вот код нашего DownloadedDrawable класса:

static class DownloadedDrawable extends ColorDrawable {
private final WeakReference<BitmapDownloaderTask> bitmapDownloaderTaskReference;
public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) {
super(Color.BLACK);
bitmapDownloaderTaskReference =
new WeakReference<BitmapDownloaderTask>(bitmapDownloaderTask);
}
public BitmapDownloaderTask getBitmapDownloaderTask() {
return bitmapDownloaderTaskReference.get();
}
}


Это реализация с помощью ColorDrawable, результатом которой в ImageView будет черный экран при загрузке. Можно использовать изображение «загрузка в процессе» для информирования пользователя. Еще раз обратите внимание на использование WeakReference для сокращения зависимостей.

Давайте изменим наш код чтобы принять во внимание новый класс. Во-первых, download метод должен создать экземпляр этого класса, и ассоциировать его с imageView.

public void download(String url, ImageView imageView) {
if (cancelPotentialDownload(url, imageView)) {
BitmapDownloaderTask task = new BitmapDownloaderTask(imageView);
DownloadedDrawable downloadedDrawable = new DownloadedDrawable(task);
imageView.setImageDrawable(downloadedDrawable);
task.execute(url, cookie);
}
}


Метод cancelPotentialDownload останавливает возможные загрузки imageView которые скоро начнутся. Обратите внимание, что этого недостаточно, чтобы гарантировать, что новейшие загрузки всегда отобразятся, т. к. задача моет быть завершена, и ждет свой метод onPostExecute, который может быть выполнен только после загрузки.

private static boolean cancelPotentialDownload(String url, ImageView imageView) {
BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);

if (bitmapDownloaderTask != null) {
String bitmapUrl = bitmapDownloaderTask.url;
if ((bitmapUrl == null) || (!bitmapUrl.equals(url))) {
bitmapDownloaderTask.cancel(true);
} else {
// The same URL is already being downloaded.
return false;
}
}
return true;
}


cancelPotentialDownload использует метод cancel из класса AsyncTask для остановки действующих загрузок. Она возвращает true большую часть времени, так что закачка может быть начата в download. Единственный случай, при котором мы этого не хотим — это когда загрузка идет с того же URL, тогда мы даем ей закончиться. Помните, что при таком осуществлении, если ImageView собрал мусор, связанные с ним загрузки не остановятся. Для этого должен быть использован RecyclerListener.

Этот метод использует вспомогательную функцию getBitmapDownloaderTask:

private static BitmapDownloaderTask getBitmapDownloaderTask(ImageView imageView) {
if (imageView != null) {
Drawable drawable = imageView.getDrawable();
if (drawable instanceof DownloadedDrawable) {
DownloadedDrawable downloadedDrawable = (DownloadedDrawable)drawable;
return downloadedDrawable.getBitmapDownloaderTask();
}
}
return null;
}


Наконец, onPostExecute должна быть модифицирована таким образом, чтобы она связывала пиктограммы только если ImageView всё еще ассоциируется именно с этим процессом загрузки.

if (imageViewReference != null) {
ImageView imageView = imageViewReference.get();
BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);
// Change bitmap only if this process is still associated with it
if (this == bitmapDownloaderTask) {
imageView.setImageBitmap(bitmap);
}
}


С этими модификациями наш класс ImageDownloader теперь предоставляет базовые сервисы, которые мы от него ожидали. Не стесняйтесь использовать его, или асинхронный шаблон, который обеспечвает гибкость Ваших приложений.

Демонстрация



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

image

На будущее



Этот код был упрощен для того, чтобы сфокусировать Ваше внимание на параллельности и многих других функциях, отсутствующих в нашей реализации. Класс ImageDownloader извлечет выгоду из кэша, особенно если использовать его совместно с ListView, который скорее всего будет отображать одно и тоже изображение столько раз, сколько пользователь будет использовать прокручивать экран вниз-вверх. Также в случае необходимости можно добавить эскизы и ресайзинг изображений.

Ошибки загрузки и тайм-ауты корректно отображаются в нашей реализации, она просто выдаст пустую картинку в этом случае. Если хотите — можно показывать любое изображение сообщающее об ошибке.

Наш HTTP-запрос очень прост. Вы можете добавить параметры или подключить кукисы, которые требуют некоторые сайты.

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

UPD. Спасибо за замечания, исправил.
Теги:Androidмногопоточностьасинхронные задачи
Хабы: Разработка под Android
+34
4,8k 66
Комментарии 19
Разработка приложений на Kotlin
2 декабря 202020 900 ₽Нетология
Профессия Android-разработчик
14 декабря 202071 000 ₽SkillFactory
Android Developer. Basic
24 декабря 202070 000 ₽OTUS
Android Developer. Professional
24 декабря 202060 000 ₽OTUS
Vue.js Продвинутая веб-разработка
11 января 202127 000 ₽Loftschool
▇▅▄▅▅▄ ▇▄▅