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

Примечание. Существует несколько библиотек, которые следуют рекомендациям по загрузке изображений. Вы можете использовать эти библиотеки в своем приложении для наиболее оптимизированной загрузки изображений. Мы рекомендуем библиотеку Glide , которая максимально быстро и плавно загружает и отображает изображения. Другие популярные библиотеки загрузки изображений включают Picasso от Square, Coil от Instacart и Fresco от Facebook. Эти библиотеки упрощают большинство сложных задач, связанных с растровыми изображениями и другими типами изображений на Android.

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

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

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

Чтение размеров и типа растрового изображения

Класс BitmapFactory предоставляет несколько методов декодирования ( decodeByteArray() , decodeFile() , decodeResource() и т. д.) для создания Bitmap из различных источников. Выберите наиболее подходящий метод декодирования в зависимости от источника данных изображения. Эти методы пытаются выделить память для созданного растрового изображения и поэтому могут легко привести к исключению OutOfMemory . Каждый тип метода декодирования имеет дополнительные сигнатуры, которые позволяют указать параметры декодирования через класс BitmapFactory.Options . Установка для свойства inJustDecodeBounds значения true во время декодирования позволяет избежать выделения памяти, возвращая значение null для растрового объекта, но устанавливая outWidth , outHeight и outMimeType . Этот метод позволяет вам считывать размеры и тип данных изображения до построения (и выделения памяти) растрового изображения.

val options = BitmapFactory.Options().apply {
    inJustDecodeBounds
= true
}
BitmapFactory.decodeResource(resources, R.id.myimage, options)
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
val imageType: String = options.outMimeType
BitmapFactory.Options options = new BitmapFactory.Options();
options
.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

Чтобы избежать исключений java.lang.OutOfMemory , проверьте размеры растрового изображения перед его декодированием, если только вы не абсолютно уверены, что источник предоставит вам данные изображения предсказуемого размера, которые удобно помещаются в доступную память.

Загрузите уменьшенную версию в память

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

  • Примерное использование памяти при загрузке полного изображения в память.
  • Объем памяти, который вы готовы выделить для загрузки этого образа, учитывая любые другие требования к памяти вашего приложения.
  • Размеры целевого ImageView или компонента пользовательского интерфейса, в который должно быть загружено изображение.
  • Размер экрана и плотность текущего устройства.

Например, не стоит загружать в память изображение размером 1024x768 пикселей, если оно в конечном итоге будет отображаться в миниатюре размером 128x96 пикселей в ImageView .

Чтобы сообщить декодеру о субдискретизации изображения и загрузке уменьшенной версии в память, установите для inSampleSize значение true в объекте BitmapFactory.Options . Например, изображение с разрешением 2048x1536, декодированное с помощью inSampleSize , равного 4, создает растровое изображение размером примерно 512x384. При загрузке этого изображения в память используется 0,75 МБ, а не 12 МБ для полного изображения (при условии, что конфигурация растрового изображения ARGB_8888 ). Вот метод расчета значения размера выборки, которое представляет собой степень двойки на основе целевой ширины и высоты:

fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
   
// Raw height and width of image
   
val (height: Int, width: Int) = options.run { outHeight to outWidth }
   
var inSampleSize = 1

   
if (height > reqHeight || width > reqWidth) {

       
val halfHeight: Int = height / 2
       
val halfWidth: Int = width / 2

       
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
       
// height and width larger than the requested height and width.
       
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
            inSampleSize
*= 2
       
}
   
}

   
return inSampleSize
}
public static int calculateInSampleSize(
           
BitmapFactory.Options options, int reqWidth, int reqHeight) {
   
// Raw height and width of image
   
final int height = options.outHeight;
   
final int width = options.outWidth;
   
int inSampleSize = 1;

   
if (height > reqHeight || width > reqWidth) {

       
final int halfHeight = height / 2;
       
final int halfWidth = width / 2;

       
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
       
// height and width larger than the requested height and width.
       
while ((halfHeight / inSampleSize) >= reqHeight
               
&& (halfWidth / inSampleSize) >= reqWidth) {
            inSampleSize
*= 2;
       
}
   
}

   
return inSampleSize;
}

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

Чтобы использовать этот метод, сначала декодируйте, установив для inJustDecodeBounds значение true , передайте параметры, а затем снова декодируйте, используя новое значение inSampleSize и значение inJustDecodeBounds , установленное в false :

fun decodeSampledBitmapFromResource(
        res
: Resources,
        resId
: Int,
        reqWidth
: Int,
        reqHeight
: Int
): Bitmap {
   
// First decode with inJustDecodeBounds=true to check dimensions
   
return BitmapFactory.Options().run {
        inJustDecodeBounds
= true
       
BitmapFactory.decodeResource(res, resId, this)

       
// Calculate inSampleSize
        inSampleSize
= calculateInSampleSize(this, reqWidth, reqHeight)

       
// Decode bitmap with inSampleSize set
        inJustDecodeBounds
= false

       
BitmapFactory.decodeResource(res, resId, this)
   
}
}
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
       
int reqWidth, int reqHeight) {

   
// First decode with inJustDecodeBounds=true to check dimensions
   
final BitmapFactory.Options options = new BitmapFactory.Options();
    options
.inJustDecodeBounds = true;
   
BitmapFactory.decodeResource(res, resId, options);

   
// Calculate inSampleSize
    options
.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

   
// Decode bitmap with inSampleSize set
    options
.inJustDecodeBounds = false;
   
return BitmapFactory.decodeResource(res, resId, options);
}

Этот метод позволяет легко загрузить растровое изображение произвольно большого размера в ImageView , который отображает миниатюру размером 100x100 пикселей, как показано в следующем примере кода:

imageView.setImageBitmap(
        decodeSampledBitmapFromResource
(resources, R.id.myimage, 100, 100)
)
imageView.setImageBitmap(
    decodeSampledBitmapFromResource
(getResources(), R.id.myimage, 100, 100));

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