Wydajne ładowanie dużych map bitowych

Uwaga: jest kilka bibliotek, które korzystają ze sprawdzonych metod wczytywania obrazów. Możesz używać tych bibliotek w swojej aplikacji, aby wczytywać obrazy w najbardziej zoptymalizowany sposób. Zalecamy korzystanie z biblioteki Glide, która ładuje oraz wyświetla obrazy tak szybko i płynnie, jak to tylko możliwe. Inne popularne biblioteki wczytywania obrazów to Picasso od Square, Coil od Instacart i Fresco z Facebooka. Biblioteki te upraszczają większość złożonych zadań związanych z mapami bitowymi i innymi typami obrazów na Androidzie.

Obrazy mają różne kształty i rozmiary. W wielu przypadkach są one większe niż wymagane w przypadku typowego interfejsu aplikacji. Na przykład systemowa aplikacja Galeria wyświetla zdjęcia zrobione aparatem urządzenia z Androidem, które mają zwykle znacznie większą rozdzielczość niż gęstość ekranu urządzenia.

Ponieważ pracujesz z ograniczoną pamięcią, najlepiej by było wczytywać w pamięci tylko wersję o niższej rozdzielczości. Wersja w niższej rozdzielczości powinna pasować do rozmiaru komponentu UI, który ją wyświetla. Obraz o wyższej rozdzielczości nie zapewnia żadnych widocznych korzyści, ale nadal zajmuje cenną pamięć i generuje dodatkową wydajność ze względu na dodatkowe skalowanie w locie.

W tej lekcji zaprezentujemy, jak dekodować duże mapy bitowe bez przekraczania limitu pamięci poszczególnych aplikacji przez wczytywanie do pamięci mniejszej wersji podpróbkowanej.

Odczytywanie wymiarów i typu mapy bitowej

Klasa BitmapFactory udostępnia kilka metod dekodowania (decodeByteArray(), decodeFile(), decodeResource() itd.) służących do tworzenia elementów Bitmap z różnych źródeł. Na podstawie źródła danych obrazu wybierz najbardziej odpowiednią metodę dekodowania. Te metody próbują przydzielać pamięć na potrzeby skonstruowanej bitmapy, dlatego mogą łatwo skutkować wyjątkiem OutOfMemory. Każdy typ metody dekodowania ma dodatkowe podpisy, które pozwalają określić opcje dekodowania za pomocą klasy BitmapFactory.Options. Ustawienie właściwości inJustDecodeBounds na true podczas dekodowania unika alokacji pamięci, ponieważ zwraca wartość null dla obiektu bitmapy, ale ustawia się outWidth, outHeight i outMimeType. Ta metoda pozwala odczytać wymiary i typ danych obrazu przed utworzeniem (i przydziałem pamięci) mapy bitowej.

Kotlin

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

Java

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;

Aby uniknąć wyjątków java.lang.OutOfMemory, sprawdź wymiary bitmapy przed jej zdekodowaniem, chyba że masz absolutnie pewność, że źródło udostępnia dane obrazu o przewidywalnych rozmiarach, które wygodnie mieszczą się w dostępnej pamięci.

Wczytywanie do pamięci wersji przeskalowanej w dół

Znane już wymiary obrazu pozwalają zdecydować, czy w pamięci ma zostać wczytany cały obraz, czy też powinna zostać wczytana jego wersja podpróbkowana. Weź pod uwagę te kwestie:

  • Szacunkowe wykorzystanie pamięci podczas wczytywania całego obrazu w pamięci.
  • Ilość pamięci, jaką chcesz załadować do wczytywania tego obrazu, biorąc pod uwagę inne wymagania dotyczące pamięci aplikacji.
  • Wymiary docelowego elementu ImageView lub komponentu UI, do którego ma być wczytywany obraz.
  • Rozmiar ekranu i gęstość obecnego urządzenia.

Na przykład nie warto wczytywać do pamięci obrazu o rozdzielczości 1024 × 768 pikseli, jeśli w zasadzie ImageView zostanie on wyświetlony jako miniatura o wymiarach 128 × 96 pikseli.

Aby nakazać dekoderowi podpróbkowanie obrazu przez wczytanie mniejszej wersji w pamięci, ustaw inSampleSize na true w obiekcie BitmapFactory.Options. Na przykład obraz o rozdzielczości 2048 x 1536 po zdekodowaniu z użyciem parametru inSampleSize o wartości 4 powoduje utworzenie mapy bitowej o wymiarach około 512 x 384. Wczytanie tego do pamięci zajmuje 0,75 MB, a nie 12 MB dla pełnego obrazu (przy założeniu, że bitmapa ma konfigurację ARGB_8888). Oto sposób obliczania wartości próbki, która jest potęgą 2 na podstawie docelowej szerokości i wysokości:

Kotlin

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
}

Java

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;
}

Uwaga: potęga równa 2 jest obliczana, ponieważ dekoder wykorzystuje wartość końcową, zaokrąglając w dół do najbliższej potęgi dwa, zgodnie z dokumentacją inSampleSize.

Aby użyć tej metody, najpierw dekoduj z parametrem inJustDecodeBounds ustawionym na true, przekaż opcje, a następnie ponownie zdekoduj, używając nowej wartości inSampleSize i inJustDecodeBounds ustawionej na false:

Kotlin

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)
    }
}

Java

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);
}

Ta metoda ułatwia wczytanie bitmapy o dowolnym rozmiarze do elementu ImageView z miniaturą o wymiarach 100 × 100 pikseli, jak w tym przykładzie:

Kotlin

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

Java

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

W podobny sposób możesz dekodować mapy bitowe z innych źródeł, zastępując w razie potrzeby odpowiednią metodę BitmapFactory.decode*.