Обработка растровых изображений вне потока пользовательского интерфейса

BitmapFactory.decode* методы, обсуждаемые в Эффективная загрузка больших растровых изображений , не должны выполняться в основном потоке пользовательского интерфейса, если исходные данные загружаются с диска и по сети (или любого другого источника, кроме памяти). Время, требуемое для загрузки эти данных, непредсказуемо и зависит от множества факторов (скорости чтения с диска или сети, размер изображения, мощность процессора и т.д.). Если одна из этих задач блокирует поток пользовательского интерфейса, система помечает ваше приложение как не отвечающее и пользователь имеет возможность закрыть его (см. Дизайн для быстроты реагирования для получения дополнительной информации).

Этот урок проведет вас через обработку растровых изображений в фоновом потоке, используя AsyncTask и покажет вам, как обрабатывать проблемы параллелизма.

Использование AsyncTask

AsyncTask класс предоставляет простой способ выполнения некоторой работы в фоновом потоке и передавать результаты обратно в поток пользовательского интерфейса. Чтобы его использовать, создайте подкласс и перегрузите предусмотренные методы. Вот пример загрузки большого изображения в ImageView используя AsyncTask и decodeSampledBitmapFromResource():

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    private final WeakReference<ImageView> imageViewReference;
    private int data = 0;

    public BitmapWorkerTask(ImageView imageView) {
        // Use a WeakReference to ensure the ImageView can be garbage collected
        imageViewReference = new WeakReference<ImageView>(imageView);
    }

    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
    }

    // Once complete, see if ImageView is still around and set bitmap.
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

WeakReference на ImageView гарантирует, что AsyncTask не предотвращает ImageView и все, на что он ссылается от освобождения сборщиком мусора. Нет никакой гарантии, что ImageView по-прежнему существует, когда завершается задача, поэтому вы должны также проверить ссылку в onPostExecute() ImageView , возможно, больше не существует, например, если пользователь уходит из вашей деятельности или если изменилась конфигураций до завершения задания.

Для начала загрузки растрового изображения в асинхронном режиме, просто создать новую задачу и выполнить её:

public void loadBitmap(int resId, ImageView imageView) {
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
}

Обработка параллелизма

Общие компоненты представления, такие как ListView и GridView вводят еще одну проблему, когда они используются в сочетании с AsyncTask как показано в предыдущем разделе. Для того чтобы эффективно использовать память, эти компоненты разрушают дочерние представления, когда пользователь выполняет прокрутку. Если каждое дочернее представление запускает AsyncTask, нет никакой гарантии, что, когда он завершится, связанное с ним представление не было переработано для использования для другого дочернего представления. Кроме того, нет никакой гарантии, что асинхронные задачи завершатся в том же порядке, в котором они запускались.

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

Создайте отдельный Drawable подкласс для хранения обратной ссылки на выполняемую задачу. В этом случае, BitmapDrawable будет использоваться, что позволит отобразить изображение в ImageView , когда задача будет выполнена:

static class AsyncDrawable extends BitmapDrawable {
    private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

    public AsyncDrawable(Resources res, Bitmap bitmap,
            BitmapWorkerTask bitmapWorkerTask) {
        super(res, bitmap);
        bitmapWorkerTaskReference =
            new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
    }

    public BitmapWorkerTask getBitmapWorkerTask() {
        return bitmapWorkerTaskReference.get();
    }
}

Перед выполнением BitmapWorkerTask, вы создаёте AsyncDrawable и привязываете его к целевому ImageView:

public void loadBitmap(int resId, ImageView imageView) {
    if (cancelPotentialWork(resId, imageView)) {
        final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
        final AsyncDrawable asyncDrawable =
                new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
        imageView.setImageDrawable(asyncDrawable);
        task.execute(resId);
    }
}

cancelPotentialWork метод, указанный в примере кода выше, проверяет не запущенно ли уже другое задание связанное с ImageView. Если это так, то он пытается отменить предыдущую задачу с помощью вызова cancel(). В небольшом числе случаев, новые данные задачи соответствуют данным существующей задачи и ничего больше не должно произойти. Вот реализация cancelPotentialWork:

public static boolean cancelPotentialWork(int data, ImageView imageView) {
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        // If bitmapData is not yet set or it differs from the new data
        if (bitmapData == 0 || bitmapData != data) {
            // Cancel previous task
            bitmapWorkerTask.cancel(true);
        } else {
            // The same work is already in progress
            return false;
        }
    }
    // No task associated with the ImageView, or an existing task was cancelled
    return true;
}

Вспомогательный метод, getBitmapWorkerTask(), используется для извлечения задачи, связанной с конкретным ImageView:

private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
   if (imageView != null) {
       final Drawable drawable = imageView.getDrawable();
       if (drawable instanceof AsyncDrawable) {
           final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
           return asyncDrawable.getBitmapWorkerTask();
       }
    }
    return null;
}

Последний этап это обновления onPostExecute() в BitmapWorkerTask , таким образом, чтобы он проверял не отменена ли задача и соответствует ли данное задание одному из связанных с ImageView:

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (isCancelled()) {
            bitmap = null;
        }

        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            final BitmapWorkerTask bitmapWorkerTask =
                    getBitmapWorkerTask(imageView);
            if (this == bitmapWorkerTask && imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

Эта реализация теперь пригодена для использования в ListView и GridView компонентах, а также в любых других компонентах, которые перерабатывают свои дочерние представления. Просто вызовите loadBitmap где вы обычно устанавливаете изображение для своего ImageView. Например, в GridView реализации это было бы в getView() методе адаптера обратного вызова.