В одном из наших приложений была поставлена задача загружать иконки приложений, которые имеются на устройстве. Поскольку иконок, как и приложений, может быть очень много, для повышения быстродействия эту операцию следовало разделить на параллельные процессы. Затем загруженные записи с приложениями и иконками выводятся пользователям с помощью RecyclerView.
Для выполнения этих задач используется класс AsyncTask, принимающий в качестве исходных данных ID ImageView, в который нужно загрузить иконку, и выполняющий процесс загрузки.
Сейчас код, реализующий загрузку иконок, выглядит следующим образом:
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); } } }
Однако, как оказалось, здесь имеется существенный недостаток: AsyncTask вместо того, чтобы для загрузки каждой иконки выделять отдельный поток, выполняет все операции в одном потоке, выстраивая их в очередь выполнения. То есть, при создании нового ViewHolder или его перебиндивания вызываемый AsyncTask не запускает новый поток, а ждёт, пока не будет выполнен работающий AsyncTask. Как следствие, загрузка иконок может занять много времени, и также это может вызвать дерганье интерфейса на слабеньких устройствах.
Такая проблема возникает потому, что, начиная с версии Android 3.2 Honeycomb, класс AsyncTask перестал создавать поток для каждого экземпляра, вместо этого создаётся объект Executor, который выполняет всё в одном фоновом потоке. Из-за этого сейчас крайне не рекомендуется его использовать для операций, выполняющихся не один раз, а на протяжении долгого времени.
Чтобы исправить проблему, попробуем создать собственный фоновый поток для загрузки иконок. В этом нам поможет класс HandlerThread, благодаря которому мы создадим цикл сообщений. Фоновый поток с помощью очередей будет проверять новые сообщения, а главный поток будет извлекать эти сообщения из очереди.
Старый код убираем полностью, теперь наш класс GetIconTask будет наследовать от HandlerThread, а не от AsyncTask.
public class GetIconTask<T> extends HandlerThread { private static final String TAG = "GetIconTask"; public GetIconTask() { super(TAG); } public void queueIcon(T target, String packageName) { Log.i(TAG, "Получен packageName: " + packageName); } }
Обобщённый параметр T используется для того, чтобы реализация была более гибкой, не привязанной к какому-то конкретному типу. Метод queueIcon() будет принимать имя пакета, иконку которого мы хотим загрузить в потоке. Он будет вызываться там же, где сейчас запускается AsyncTask, то есть в методе onBindViewHolder() адаптера.
Теперь нужно фоновый поток запускать. Запускаться он будет в момент старта приложения и останавливаться, если оно было закрыто. Поэтому нам нужны методы главной активности onCreate() и onDestroy() соответственно.
private GetIconTask<AppsAdapter.AppViewHolder> mGetIconTask; ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ... mGetIconTask = new GetIconTask<>(); mGetIconTask.start(); mGetIconTask.getLooper(); ... } @Override public void onDestroy() { super.onDestroy(); mGetIconTask.quit(); }
Крайне важно уничтожать потоки, если приложение их не использует в данный момент, иначе они останутся работать в системе, что будет создавать лишний мусор. Метод getLooper() вызывается после запуска потока, чтобы исключить состояние гонки (ошибка, при которой работа приложения зависит от того, в каком порядке выполняются части кода), мы должны быть уверены, что поток готов к использованию.
Теперь в методе onBindViewHolder() передаём в метод queueIcon() фонового потока сам холдер и имя пакета. Чтобы получить доступ к экземпляру GetIconTask, нужно будет передать его в адаптер через конструктор.
mGetIconTask.queueIcon(appVH, ai.packageName);
На данном этапе фоновый поток уже должен получать сообщения от адаптера, однако он их пока никак не обрабатывает. Для того, чтобы реализовать обработку, нам понадобится класс Handler, отвечающий за обработку и отправку сообщений (Message). В данной реализации сообщения будут создаваться и отправляться в конец очереди Looper, откуда уже Handler будет его обрабатывать. Для того, чтобы добавить сообщения в очередь, нам также понадобится константа, определяющая сообщение (поле what).
private static final int MESSSAGE_DOWNLOAD = 0; private Handler mRequestHandler; private ConcurrentMap<T, String> mRequestMap = new ConcurrentHashMap<>();
Для хранения имён пакетов используется безопасная разновидность HashMap – ConcurrentHashMap, принимающая в качестве ключа объект T.
Теперь сообщение нужно отправить в очередь, для этого дополним код в методе queueIcon().
public void queueIcon(T target, String packageName) { Log.i(TAG, "Получен packageName: " + packageName); if (packageName == null) { mRequestMap.remove(target); } else { mRequestMap.put(target, packageName); mRequestHandler.obtainMessage(MESSSAGE_DOWNLOAD, target).sendToTarget(); } }
Здесь сообщение отправляется на приёмник для дальнейшей обработки. Теперь нашему Handler нужно извлечь сообщение из очереди и загрузить иконку, выполнив необходимые операции. Для этого переопределим метод onLooperPrepared(), в котором Handler будет извлекать сообщения и обрабатывать их в отдельном методе.
@Override protected void onLooperPrepared() { mRequestHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == MESSSAGE_DOWNLOAD) { T target = (T) msg.obj; Log.i(TAG, "Получен запрос для пакета: " + mRequestMap.get(target)); handleRequest(target); } } }; } private void handleRequest(final T target) { final String packageName = mRequestMap.get(target); try { final Drawable icon = mContext.getPackageManager().getApplicationIcon(packageName); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } }
Фоновый поток принимает и обрабатывает сообщения, однако главный поток об этом ничего не знает. Чтобы передать сообщение в главный поток, нужно объявит в нём экземпляр Handler, который будет передаваться в конструктор фонового потока. В фоновом же потоке нужно добавить интерфейс, через который главный поток будет принимать ViewHolder и иконку приложения.
private Context mContext; private Handler mResponseHandler; private GetIconTaskListener<T> mGetIconTaskListener; public interface GetIconTaskListener<T> { void onIconDownloaded(T target, Drawable icon); } public void setIconDownloadListener(GetIconTaskListener<T> listener) { mGetIconTaskListener = listener; } public GetIconTask(Handler responseHandler, Context context) { super(TAG); mContext = context; mResponseHandler = responseHandler; } ...
Метод onIconDownloaded() в интерфейсе будет вызываться тогда, когда иконка будет загружена и готова к добавлению.
Теперь нам нужно передать Handler из главного потока, для этого нужно его объявить, также необходимо установить слушатель, который будет принимать готовые иконки и обновлять нужные холдеры.
Handler responseHandler = new Handler(); mGetIconTask = new GetIconTask<>(responseHandler, getApplicationContext()); mGetIconTask.setIconDownloadListener( new GetIconTask.GetIconTaskListener<AppsAdapter.AppViewHolder>() { @Override public void onIconDownloaded(AppsAdapter.AppViewHolder target, Drawable icon) { target.bindDrawable(icon); } }); mGetIconTask.start(); mGetIconTask.getLooper();
Связь между двумя потоками установлена, но никакие данные между ними не пересылаются. Чтобы отправить сообщение из фонового потока в главный, нужно в методе handleRequest() фонового потока добавить код, передающий сообщение в интерфейс и удаляющий его из очереди.
private void handleRequest(final T target) { final String packageName = mRequestMap.get(target); try { final Drawable icon = mContext.getPackageManager().getApplicationIcon(packageName); mResponseHandler.post(new Runnable() { @Override public void run() { if (!mRequestMap.get(target).equals(packageName)) { return; } mRequestMap.remove(target); mGetIconTaskListener.onIconDownloaded(target, icon); } }); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } }
Благодаря условию внутри run() мы можем быть уверены, что каждое приложение в списке получит свою иконку.
Чтобы обезопасить себя от возможных ошибок с утечкой памяти или удалением связи между GetIconTask и адаптером, нужно реализовать полную очистку списка. Для этого в классе GetIconTask добавляем новый метод clearQueue().
public void clearQueue() { mRequestHandler.removeMessages(MESSSAGE_DOWNLOAD); }
Затем в методе нашей активности onStop() вызываем очистку списка.
@Override protected void onStop() { super.onStop(); mGetIconTask.clearQueue(); }
В результате у нас получилась множественная загрузка иконок без каких-либо подтормаживаний и багов. Фоновый поток обрабатывает получаемые сообщения и возвращает их главному потоку, откуда уже происходит обновление записей в RecyclerView. Итоговый код класса GetIconTask выглядит следующим образом:
public class GetIconTask<T> extends HandlerThread { private static final String TAG = "GetIconTask"; private static final int MESSSAGE_DOWNLOAD = 0; private Handler mRequestHandler; private ConcurrentMap<T, String> mRequestMap = new ConcurrentHashMap<>(); private Context mContext; private Handler mResponseHandler; private GetIconTaskListener<T> mGetIconTaskListener; public interface GetIconTaskListener<T> { void onIconDownloaded(T target, Drawable icon); } public void setIconDownloadListener(GetIconTaskListener<T> listener) { mGetIconTaskListener = listener; } public GetIconTask(Handler responseHandler, Context context) { super(TAG); mContext = context; mResponseHandler = responseHandler; } @Override protected void onLooperPrepared() { mRequestHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == MESSSAGE_DOWNLOAD) { T target = (T) msg.obj; Log.i(TAG, "Получен запрос для пакета: " + mRequestMap.get(target)); handleRequest(target); } } }; } private void handleRequest(final T target) { final String packageName = mRequestMap.get(target); try { final Drawable icon = mContext.getPackageManager().getApplicationIcon(packageName); mResponseHandler.post(new Runnable() { @Override public void run() { if (!mRequestMap.get(target).equals(packageName)) { return; } mRequestMap.remove(target); mGetIconTaskListener.onIconDownloaded(target, icon); } }); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } } public void queueIcon(T target, String packageName) { Log.i(TAG, "Получен packageName: " + packageName); if (packageName == null) { mRequestMap.remove(target); } else { mRequestMap.put(target, packageName); mRequestHandler.obtainMessage(MESSSAGE_DOWNLOAD, target).sendToTarget(); } } public void clearQueue() { mRequestHandler.removeMessages(MESSSAGE_DOWNLOAD); } }
Если сравнивать качество и скорость работы с предыдущей версией, то можно наглядно заметить улучшение работы приложения.
Вот так работает загрузка иконок с использованием AsyncTask.
А вот так – после замены AsyncTask на HandlerThread.
Подводя итоги, можно сказать, что использование AsyncTask для часто повторяющихся операций не является эффективным решением, хотя в некоторых ситуациях этот класс крайне полезен, поскольку очень прост в реализации. С помощью HandlerThread же мы добились того, что приложение стало работать быстрее, чем раньше, что должно положительно сказаться на отзывах пользователей.
Скачать обновлённый Менеджер системных приложений вы можете в Google Play по следующей ссылке.
” эту операцию следовало разделить на параллельные процессы. ” – вот после этой фразы сразу закрались сомнения..
Вы просто накидываете в свой Looper сообщения в очередь, по очереди их обрабатываете и отравляете в Main Looper – где тут множетвенная загрузка и распараллеливание?
Почитайте: https://blog.mindorks.com/android-core-looper-handler-and-handlerthread-bd54d69fe91a
У вас API > 16? Чем не нравится AsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) – сам все распараллелит и не надо столько кода впустую писать… Не нравится TREAD_POOL_EXECUTOR? – можно свой написать – и то в разы проще будет.
Ну если хочется совсем хардкора – можно использовать “родные” способы распараллеливания java, например ExecutorService:
ExecutorService executionService = Executors.newFixedThreadPool(5);
executionService.submit(new Runnable(…))
…
executionService.shutdown();
и это, в принципе, 3 сточки которые заменят почти всю вашу реализацию…
Спасибо за комментарий. У нас апи 14 минимальный.
Да, и картинки у вас с AsyncTask менялись вовсе не потому, что AsyncTask выполнял все поочередно, а оптому что вы передаете ImageView в каждый таск, RecyclerView – переиспользует ваши вьюхи, и все накиданные ранее таски просто сетили свои картики по завершению во вьюху, вам надо лишь было отменять предыдущий AsyncTask при новом onBind
Сделал отмену в onViewRecycled .
Вот пример адаптера
https://gist.github.com/petrovichtim/106d89ba61fe8ff57a420fbd152c87de