Отмена задачи в AsyncTask

Пользователь Alexander в комментариях к нашей предыдущей статье заметил, что реализация загрузки иконок приложений через HandlerThread хоть и рабочая, однако слишком громоздкая, аналогичного результата можно было бы добиться изменением AsyncTask в предыдущем варианте приложения.

Основная проблема здесь кроется в асинхронности. Главное достоинство асинхронности заключается в том, что фоновый и главный потоки работают независимо друг от друга, что и позволяет выполнять самые разные операции, не блокируя основной поток и продолжая работу в нём, однако в этом же и кроется недостаток, поскольку эти потоки друг о друге ничего не знают. Поэтому, когда AsyncTask завершает свою работу и готовится вернуть результат в главный поток, может возникнуть такая ситуация, что ему этот результат уже не требуется. По этой причине в приложении возникал баг, когда после быстрого скроллинга иконки приложений вставали не на свои места. Данную проблему можно решить, если отменять выполняемый AsyncTask.

На самом деле, при использовании AsyncTask нет многопоточности, это было реализовано только в самых первых версиях Android, но показало себя не с самой лучше стороны. По этой причине реализация выглядит следующим образом: при запуске с помощью метода execute() происходит добавление задачи в очередь выполнения, причём максимальное число задач, которые могут одновременно находиться в очереди, равно 128.

Для того, чтобы удалить задачи из очереди, в API есть метод cancel(), попробуем его использовать и посмотрим поведение приложения. Для этого вернёмся к прежнему варианту с реализацией AsyncTask.

public class GetIconTask extends AsyncTask<String, Void, Drawable> {
    private ImageView ivImageView;
    private Context context;

    public GetIconTask(ImageView ivImageView) {
        this.ivImageView = ivImageView;
        this.context = ivImageView.getContext();
    }

    @Override
    protected Drawable doInBackground(String... params) {
        String packageName = params[0];
        Drawable icon = null;
        try {
            icon = context.getPackageManager().getApplicationIcon(packageName);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return icon;
    }

    @Override
    protected void onPostExecute(Drawable result) {
        super.onPostExecute(result);
        if (ivImageView != null) {
            ivImageView.setImageDrawable(result);
        }
    }
}

И теперь в классе адаптера добавим метод onViewRecycled() добавим отмену AsyncTask.

@Override public void onViewRecycled(AppViewHolder appVH) {
  super.onViewRecycled(appVH);
  appVH.git.cancel(true);
}

Сравним APK-файлы всех трёх версий (с HandlerThread, с HandlerThread и использованием LruCache и с исправленным AsyncTask). Первый вариант делался в предыдущей статье, в нём не было никаких изменений.

Второй вариант является дополнением к первому класса LruCache для того, чтобы держать загруженные иконки в кэше приложения и быстро оттуда их получать. При первом запуске приложение загружает их привычным способом и затем сохраняет в собственный кэш. Затем, во время пользования приложением, проверяется, находится ли требуемая иконка в кэше или нет, и если находится, то берётся оттуда. Чтобы это реализовать, был добавлен класс IconCache, наследующий от LruCache.

class IconCache extends LruCache<String, Drawable> {
  IconCache(int maxSize) {
    super(maxSize);
  }

  Drawable getBitmapFromMemory(String key) {
    return this.get(key);
  }

  void setBitmapToMemory(String key, Drawable drawable) {
    if (getBitmapFromMemory(key) == null) {
      this.put(key, drawable);
      Log.d("TEST", key + " добавлен в кэш");
    }
  }
}

Добавление в кэш и загрузка оттуда происходит в классе GetIconTask во время обработки сообщения и передачи в главный поток.

private void handleRequest(final T target) {
  final String packageName = mRequestMap.get(target);
  final Drawable icon;

  if (packageName != null) { // во избежание key == null
    try {
      if (iconCache.getBitmapFromMemory(packageName) == null) {
        icon = mContext.getPackageManager().getApplicationIcon(packageName);
        iconCache.setBitmapToMemory(packageName, icon);
      } else {
        icon = iconCache.getBitmapFromMemory(packageName);
      }
      mResponseHandler.post(new Runnable() {
        @Override
        public void run() {
          if (mRequestMap.get(target) == null || !mRequestMap.get(target).equals(packageName)) {
            return;
          }
          mRequestMap.remove(target);
          mGetIconTaskListener.onIconDownloaded(target, icon);
        }
      });
    } catch (PackageManager.NameNotFoundException | NullPointerException e) {
      e.printStackTrace();
    }
  }
}

В качестве ключа для кэша используется имя пакета приложения.

Третий вариант — текущая реализация из данной статьи.

Посмотрим, какой вариант наиболее оптимален в плане размера и количества классов и методов. Делать это будем с помощью APK Analyzer, утилиты Android Studio,

HandlerThread HandlerThread + LruCache AsyncTask
Размер в несжатом виде 1.1 МБ 1.1 МБ 1.1 МБ
Размер в сжатом виде 767.9 КБ 768.2 КБ 762 КБ
Сколько содержит классов 977 978 973
Сколько содержит методов 8143 8146 8130

Как можно отсюда видеть, вариант с AsyncTask выглядит более экономным, однако на некоторых устройствах проблема сохранилась — иконки изображений перескакивают с места на место при быстро скроллинге списка с приложениями.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *