Примечание. В большинстве случаев мы рекомендуем использовать библиотеку Glide для получения, декодирования и отображения растровых изображений в приложении. Glide абстрагирует большую часть сложности при обработке этих и других задач, связанных с работой с растровыми и другими изображениями на Android. Информацию о том, как использовать и скачать Glide, можно найти в хранилище Glide на GitHub.
Загрузить одно растровое изображение в пользовательский интерфейс несложно, но если вам нужно загрузить сразу несколько изображений, все становится сложнее. Во многих случаях (например, при использовании компонентов ListView, GridView или ViewPager) общее количество изображений на экране и тех, которые могут появиться на нем в ближайшее время, практически не ограничено.
Использование памяти снижается за счет таких компонентов, как этот, путем повторного использования дочерних представлений по мере их перемещения за пределы экрана. Сборщик мусора также освобождает загруженные растровые изображения, если вы не сохраняете долгоживущие ссылки. Это все хорошо, но чтобы сохранить плавный и быстро загружаемый интерфейс, вы должны избегать постоянной обработки этих изображений каждый раз, когда они возвращаются на экран. В этом случае может помочь кеш памяти и кеш диска, позволяющий компонентам быстро перезагружать обработанные изображения.
В этом уроке вы узнаете, как использовать кэш битовых карт в памяти и на диске, чтобы повысить скорость отклика и плавность пользовательского интерфейса при загрузке нескольких битовых карт.
Используйте кеш памяти
Кеш памяти обеспечивает быстрый доступ к битовым картам, но занимает ценную память приложения. Класс LruCache (также доступен в библиотеке поддержки для использования с уровнем API 4) особенно хорошо подходит для кеширования растровых изображений, сохранения объектов, на которые недавно ссылались, в LinkedHashMap со строгой ссылкой и удаления наименее часто используемого элемента до того, как кеш превысит назначенный размер.
Примечание. Раньше для кеша памяти часто использовался кеш битовых карт SoftReference или WeakReference, но сейчас мы не рекомендуем его. Начиная с Android 2.3 (уровень API 9) сборщик мусора более агрессивно собирает мягкие/слабые ссылки, что делает их довольно неэффективными. Кроме того, до выхода Android 3.0 (уровень API 11) резервные данные растрового изображения хранились в нативной памяти, которая не освобождается предсказуемым образом, что могло привести к кратковременному превышению лимитов памяти приложения и его сбою.
Чтобы выбрать подходящий размер для LruCache, необходимо учесть ряд факторов, например:
- Насколько требовательны к памяти другие действия и/или приложения?
- Сколько изображений будет на экране одновременно? Сколько из них должны быть готовы к показу на экране?
- Каковы размер и плотность экрана устройства? Устройству с экраном сверхвысокой плотности (xhdpi), например Galaxy Nexus, потребуется кеш большего размера, чтобы хранить в памяти то же количество изображений, что и устройство с экраном высокой плотности (hdpi), например Nexus S.
- Каковы размеры и конфигурация растровых изображений и, следовательно, сколько памяти займет каждое из них?
- Как часто будет осуществляться доступ к изображениям? Будут ли некоторые из них использоваться чаще других?
Если да, то, возможно, вы захотите постоянно хранить определенные элементы в памяти или даже иметь несколько объектов
LruCacheдля разных групп растровых изображений. - Можете ли вы найти баланс между качеством и количеством? Иногда может быть полезнее хранить большее количество растровых изображений более низкого качества, потенциально загружая версию более высокого качества в другой фоновой задаче.
Универсального решения нет, поэтому вам нужно проанализировать, как используется приложение, и выбрать подходящий вариант. Слишком маленький кеш приведет к дополнительным накладным расходам без какой-либо пользы, а слишком большой – к исключениям java.lang.OutOfMemory и нехватке памяти для работы остальных компонентов приложения.
Вот пример настройки LruCache для битовых карт:
Kotlin
private lateinit var memoryCache: LruCache<String, Bitmap> override fun onCreate(savedInstanceState: Bundle?) { ... // Get max available VM memory, exceeding this amount will throw an // OutOfMemory exception. Stored in kilobytes as LruCache takes an // int in its constructor. val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt() // Use 1/8th of the available memory for this memory cache. val cacheSize = maxMemory / 8 memoryCache = object : LruCache<String, Bitmap>(cacheSize) { override fun sizeOf(key: String, bitmap: Bitmap): Int { // The cache size will be measured in kilobytes rather than // number of items. return bitmap.byteCount / 1024 } } ... }
Java
private LruCache<String, Bitmap> memoryCache; @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; memoryCache = 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) { memoryCache.put(key, bitmap); } } public Bitmap getBitmapFromMemCache(String key) { return memoryCache.get(key); }
Примечание. В этом примере для кеша выделена одна восьмая часть памяти приложения. На устройстве с обычным разрешением или HDPI это значение составляет около 4 МБ (32/8). Для заполнения изображениями экрана GridView с разрешением 800 x 480 пикселей требуется около 1,5 МБ (800 x 480 x 4 байта). Таким образом, в памяти будет кэшироваться не менее 2,5 страниц с изображениями.
При загрузке битовой карты в ImageView сначала проверяется LruCache. Если запись найдена, она сразу используется для обновления ImageView. В противном случае для обработки изображения создается фоновый поток:
Kotlin
fun loadBitmap(resId: Int, imageView: ImageView) { val imageKey: String = resId.toString() val bitmap: Bitmap? = getBitmapFromMemCache(imageKey)?.also { mImageView.setImageBitmap(it) } ?: run { mImageView.setImageResource(R.drawable.image_placeholder) val task = BitmapWorkerTask() task.execute(resId) null } }
Java
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:
Kotlin
private inner class BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() { ... // Decode image in background. override fun doInBackground(vararg params: Int?): Bitmap? { return params[0]?.let { imageId -> decodeSampledBitmapFromResource(resources, imageId, 100, 100)?.also { bitmap -> addBitmapToMemoryCache(imageId.toString(), bitmap) } } } ... }
Java
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.
Вот обновленный пример кода, в котором помимо кеша памяти добавляется кеш диска:
Kotlin
private const val DISK_CACHE_SIZE = 1024 * 1024 * 10 // 10MB private const val DISK_CACHE_SUBDIR = "thumbnails" ... private var diskLruCache: DiskLruCache? = null private val diskCacheLock = ReentrantLock() private val diskCacheLockCondition: Condition = diskCacheLock.newCondition() private var diskCacheStarting = true override fun onCreate(savedInstanceState: Bundle?) { ... // Initialize memory cache ... // Initialize disk cache on background thread val cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR) InitDiskCacheTask().execute(cacheDir) ... } internal inner class InitDiskCacheTask : AsyncTask<File, Void, Void>() { override fun doInBackground(vararg params: File): Void? { diskCacheLock.withLock { val cacheDir = params[0] diskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE) diskCacheStarting = false // Finished initialization diskCacheLockCondition.signalAll() // Wake any waiting threads } return null } } internal inner class BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() { ... // Decode image in background. override fun doInBackground(vararg params: Int?): Bitmap? { val imageKey = params[0].toString() // Check disk cache in background thread return getBitmapFromDiskCache(imageKey) ?: // Not found in disk cache decodeSampledBitmapFromResource(resources, params[0], 100, 100) ?.also { // Add final bitmap to caches addBitmapToCache(imageKey, it) } } } fun addBitmapToCache(key: String, bitmap: Bitmap) { // Add to memory cache as before if (getBitmapFromMemCache(key) == null) { memoryCache.put(key, bitmap) } // Also add to disk cache synchronized(diskCacheLock) { diskLruCache?.apply { if (!containsKey(key)) { put(key, bitmap) } } } } fun getBitmapFromDiskCache(key: String): Bitmap? = diskCacheLock.withLock { // Wait while disk cache is started from background thread while (diskCacheStarting) { try { diskCacheLockCondition.await() } catch (e: InterruptedException) { } } return diskLruCache?.get(key) } // Creates a unique subdirectory of the designated app cache directory. Tries to use external // but if not mounted, falls back on internal storage. fun getDiskCacheDir(context: Context, uniqueName: String): File { // Check if media is mounted or storage is built-in, if so, try and use external cache dir // otherwise use internal cache dir val cachePath = if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState() || !isExternalStorageRemovable()) { context.externalCacheDir.path } else { context.cacheDir.path } return File(cachePath + File.separator + uniqueName) }
Java
private DiskLruCache diskLruCache; private final Object diskCacheLock = new Object(); private boolean diskCacheStarting = 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 (diskCacheLock) { File cacheDir = params[0]; diskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE); diskCacheStarting = false; // Finished initialization diskCacheLock.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) { memoryCache.put(key, bitmap); } // Also add to disk cache synchronized (diskCacheLock) { if (diskLruCache != null && diskLruCache.get(key) == null) { diskLruCache.put(key, bitmap); } } } public Bitmap getBitmapFromDiskCache(String key) { synchronized (diskCacheLock) { // Wait while disk cache is started from background thread while (diskCacheStarting) { try { diskCacheLock.wait(); } catch (InterruptedException e) {} } if (diskLruCache != null) { return diskLruCache.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); }
Примечание. Даже инициализация кеша диска требует операций с диском, поэтому ее не следует выполнять в основном потоке. Однако в этом случае есть вероятность, что к кешу обратятся до инициализации. Чтобы решить эту проблему, в приведенной выше реализации объект блокировки гарантирует, что приложение не будет считывать данные из кеша диска до тех пор, пока кеш не будет инициализирован.
Кеш памяти проверяется в потоке UI, а кеш диска – в фоновом потоке. Операции с диском никогда не должны выполняться в потоке UI. После завершения обработки изображений окончательное растровое изображение добавляется в кеш памяти и диска для дальнейшего использования.
Обработка изменений конфигурации
Изменения конфигурации во время выполнения, например изменение ориентации экрана, приводят к тому, что Android уничтожает и перезапускает текущее действие с новой конфигурацией. Подробнее об обработке изменений во время выполнения… Вы хотите избежать повторной обработки всех изображений, чтобы пользователь не заметил задержек при изменении конфигурации.
К счастью, у вас есть кеш памяти для битовых карт, который вы создали в разделе Использование кеша памяти. Этот кеш можно передать новому экземпляру действия с помощью Fragment, который сохраняется при вызове setRetainInstance(true). После повторного создания действия сохраненный объект Fragment снова прикрепляется, и вы получаете доступ к существующему объекту кеша, что позволяет быстро извлекать изображения и повторно заполнять объекты ImageView.
Ниже приведен пример сохранения объекта LruCache при изменении конфигурации с помощью Fragment.
Kotlin
private const val TAG = "RetainFragment" ... private lateinit var mMemoryCache: LruCache<String, Bitmap> override fun onCreate(savedInstanceState: Bundle?) { ... val retainFragment = RetainFragment.findOrCreateRetainFragment(supportFragmentManager) mMemoryCache = retainFragment.retainedCache ?: run { LruCache<String, Bitmap>(cacheSize).also { memoryCache -> ... // Initialize cache here as usual retainFragment.retainedCache = memoryCache } } ... } class RetainFragment : Fragment() { var retainedCache: LruCache<String, Bitmap>? = null companion object { fun findOrCreateRetainFragment(fm: FragmentManager): RetainFragment { return (fm.findFragmentByTag(TAG) as? RetainFragment) ?: run { RetainFragment().also { fm.beginTransaction().add(it, TAG).commit() } } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) retainInstance = true } }
Java
private LruCache<String, Bitmap> memoryCache; @Override protected void onCreate(Bundle savedInstanceState) { ... RetainFragment retainFragment = RetainFragment.findOrCreateRetainFragment(getFragmentManager()); memoryCache = retainFragment.retainedCache; if (memoryCache == null) { memoryCache = new LruCache<String, Bitmap>(cacheSize) { ... // Initialize cache here as usual } retainFragment.retainedCache = memoryCache; } ... } class RetainFragment extends Fragment { private static final String TAG = "RetainFragment"; public LruCache<String, Bitmap> retainedCache; 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. Вы должны заметить небольшую задержку или ее отсутствие, поскольку изображения заполняют активность почти мгновенно из памяти, когда вы сохраняете кэш. Если изображения не найдены в кеше памяти, они могут быть доступны в кеше диска. Если нет, они обрабатываются как обычно.