Кэширование растровых изображений

Загрузка единственного растрового изображения в ваш пользовательский интерфейс является простой задачей, однако все становится более сложным, если вам необходимо загрузить большой набор изображений одновременно. Во многих случаях (как например с компонентами, такими как ListView, GridView или ViewPager), общее количество изображений на экране в сочетании с изображениями, которые могут в скором времени появятся на экране в результате прокрутки, по существу неограниченно.

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

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

Использование кэша памяти

Кэш-память обеспечивает быстрый доступ к растровым изображениям за счет использования ценной памяти приложения. LruCache класс (также доступен в Библиотеке Поддержки для использования начиная c API Уровень 4) особенно хорошо подходит для задачи кэширования растровых изображений, удерживая недавно загруженные объекты с помощью сильных ссылок в LinkedHashMap , удаляя элементы, которые не использовались дольше всего, если кэш превышает отведенный размер.

Примечание: В прошлом, популярная реализация кэш-памяти растрового кэша была на основе SoftReference или WeakReference , однако сейчас не рекомендуется использовать такую реализацию. Начиная с Android 2.3 (API Уровень 9) сборщик мусора стал более агрессивной к сбору мягких и слабых ссылок, что делает их довольно неэффективным. Кроме того, до Android 3.0 (API Уровень 11), внутренние данные растрового изображения хранились непосредственно в оперативной памяти, которая не всегда освобождалась предсказуемым образом, что могло привести к быстрому превышению своего предела памяти приложением, и как следствие к краху приложения.

Чтобы выбрать подходящий размер для LruCache, необходимо принять во внимание ряд факторов, например:

  • Какой объем памяти необходим для остальной часть вашей деятельности и/или приложения?
  • Сколько изображений будет на экране сразу? Сколько должно быть доступно и готово для появления на экране?
  • Каков размер экрана и плотность устройства? Устройствам с дисплеями сверхвысокой плотности (xhdpi), таким как Galaxy Nexus понадобится больший кэш для хранения такого же количества изображений в памяти по сравнению с устройством, таким как Nexus S (hdpi).
  • Какого размера и конфигурации растровые изображения, и, следовательно, сколько памяти будет каждый из них занимать?
  • Как часто изображения будут необходимы ? Будут ли некоторые необходимы чаще, чем другие? Если это так, может быть, вы хотите хранить некоторые элементы в памяти всегда или даже иметь несколько LruCache объектов для различных групп растровых изображений.
  • Можете ли вы сбалансировать качество и количество? Иногда может быть более целесообразным хранить большое количество растровых изображений более низкого качества, потенциально загружая версию с более высоким качеством в другом фоновом потоке.

Здесь нет конкретного размера или формулы, которая подходит для всех приложений, вы должны анализировать то, как вы будете использовать кэш, и придумать подходящее решение. Кэш, который слишком мал, приводит к дополнительным накладным расходам без какой-либо выгоды, кэш, который является слишком большой может вновь привести к исключению java.lang.OutOfMemory и оставить для работы остальной части вашего приложении маленький объем памяти.

Вот пример настройки LruCache для растровых изображений:

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Get max available VM memory, exceeding this amount will throw an
    // OutOfMemory exception. Stored in kilobytes as LruCache takes an
    // int in its constructor.
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // Use 1/8th of the available memory for this memory cache.
    final int cacheSize = maxMemory / 8;

    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in kilobytes rather than
            // number of items.
            return bitmap.getByteCount() / 1024;
        }
    };
    ...
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}

public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}

Примечание: В этом примере, одна восьмая памяти приложения выделяется для нашего кэша. На нормальном/hdpi устройстве это минимум около 4 Мб (32/8). Полноэкранный GridView заполнен изображениями на устройстве с разрешением 800x480 будет использовать около 1,5 Мб (800*480*4 байта), так что будет кэшироваться как минимум около 2,5 страниц изображений в памяти.

При загрузке растрового изображения в ImageView LruCache проверяется первым. Если запись найдена, она сразу же используется для обновления ImageView, в противном случае создается фоновый поток для обработки изображения:

public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);

    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        mImageView.setImageBitmap(bitmap);
    } else {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}

BitmapWorkerTask также должен быть обновлен, чтобы добавлять записи в кэш-память:

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}

Использование дискового кэша

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

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

Примечание: ContentProvider может быть более подходящим местом для хранения кэшированных изображений, если они нужны более чаще, как например в приложении Галерея.

Примере кода этого класса использует DiskLruCache реализацию, которая взята из исходного кода Android. Вот обновленный пример кода, который добавляет дисковый кэш в дополнение к существующей кэш-памяти:

private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Initialize memory cache
    ...
    // Initialize disk cache on background thread
    File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
    new InitDiskCacheTask().execute(cacheDir);
    ...
}

class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
    @Override
    protected Void doInBackground(File... params) {
        synchronized (mDiskCacheLock) {
            File cacheDir = params[0];
            mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
            mDiskCacheStarting = false; // Finished initialization
            mDiskCacheLock.notifyAll(); // Wake any waiting threads
        }
        return null;
    }
}

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final String imageKey = String.valueOf(params[0]);

        // Check disk cache in background thread
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);

        if (bitmap == null) { // Not found in disk cache
            // Process as normal
            final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(), params[0], 100, 100));
        }

        // Add final bitmap to caches
        addBitmapToCache(imageKey, bitmap);

        return bitmap;
    }
    ...
}

public void addBitmapToCache(String key, Bitmap bitmap) {
    // Add to memory cache as before
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }

    // Also add to disk cache
    synchronized (mDiskCacheLock) {
        if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
            mDiskLruCache.put(key, bitmap);
        }
    }
}

public Bitmap getBitmapFromDiskCache(String key) {
    synchronized (mDiskCacheLock) {
        // Wait while disk cache is started from background thread
        while (mDiskCacheStarting) {
            try {
                mDiskCacheLock.wait();
            } catch (InterruptedException e) {}
        }
        if (mDiskLruCache != null) {
            return mDiskLruCache.get(key);
        }
    }
    return null;
}

// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
public static File getDiskCacheDir(Context context, String uniqueName) {
    // Check if media is mounted or storage is built-in, if so, try and use external cache dir
    // otherwise use internal cache dir
    final String cachePath =
            Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
                    !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
                            context.getCacheDir().getPath();

    return new File(cachePath + File.separator + uniqueName);
}

Примечание: Даже инициализации дискового кэша требуется дисковых операций, и, следовательно, не должна происходить в основном потоке. Тем не менее, это не означает, что есть шанс обращения к кэшу до инициализации. Для решения этой проблемы, в приведенной выше реализации, объект блокировки гарантирует, что приложение не будет читать из дискового кэша, пока кэш не будет проинициализирован.

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

Обработка изменений в конфигурации

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

К счастью, у вас есть хорошая кэш-память растровых изображений, которую вы построили в разделе Использование кэша памяти . Этот кэш может быть передан новому экземпляру деятельности с помощью Fragment , который сохранится с помощью вызова setRetainInstance(true)). После того, как деятельность будет создана заново, этот сохранившийся Fragment будет повторного присоединен, и вы получите доступ к существующему объекту кэша, что позволяет быстро извлекать изображения и заново использовать в ImageView объектах.

Вот пример удерживания LruCache объекта при изменении конфигурации, используя Fragment:

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    RetainFragment retainFragment =
            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mMemoryCache = retainFragment.mRetainedCache;
    if (mMemoryCache == null) {
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            ... // Initialize cache here as usual
        }
        retainFragment.mRetainedCache = mMemoryCache;
    }
    ...
}

class RetainFragment extends Fragment {
    private static final String TAG = "RetainFragment";
    public LruCache<String, Bitmap> mRetainedCache;

    public RetainFragment() {}

    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
        RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
        if (fragment == null) {
            fragment = new RetainFragment();
            fm.beginTransaction().add(fragment, TAG).commit();
        }
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
}

Чтобы проверить это, попробуйте повернуть устройство, с и без удерживания Fragment. Вы должны заметить, что практически нет задержек при заполнении деятельности изображениями из памяти, когда вы удерживаете кэш. Любые изображения не найденные в кэш-памяти, будем надеяться, доступны в кэше диска, если нет, то они обрабатываются как обычно.