Große Bitmaps effizient laden

Hinweis:Es gibt mehrere Bibliotheken, die Best Practices zum Laden von Bildern berücksichtigen. Sie können diese Bibliotheken in Ihrer App verwenden, um Bilder optimal zu laden. Wir empfehlen die Glide-Bibliothek, mit der Bilder so schnell und reibungslos wie möglich geladen und angezeigt werden. Weitere beliebte Bibliotheken zum Laden von Bildern sind Picasso von Square, Coil von Instacart und Fresco von Facebook. Diese Bibliotheken vereinfachen die meisten komplexen Aufgaben im Zusammenhang mit Bitmaps und anderen Bildtypen unter Android.

Bilder gibt es in allen Formen und Größen. In vielen Fällen sind sie größer als für eine typische Anwendungs-UI erforderlich. Die Systemgalerie-App zeigt beispielsweise Fotos an, die mit der Kamera Ihres Android-Geräts aufgenommen wurden und in der Regel eine viel höhere Auflösung haben als die Bildschirmdichte Ihres Geräts.

Da Sie mit begrenztem Speicherplatz arbeiten, möchten Sie idealerweise nur eine Version mit geringerer Auflösung in den Speicher laden. Die Version mit der niedrigeren Auflösung sollte der Größe der UI-Komponente entsprechen, in der sie angezeigt wird. Ein Bild mit einer höheren Auflösung bietet keinen sichtbaren Vorteil, verbraucht aber dennoch wertvollen Arbeitsspeicher und verursacht aufgrund zusätzlicher schneller Skalierung zusätzlichen Leistungsaufwand.

In dieser Lektion erfahren Sie, wie Sie große Bitmaps decodieren, ohne das Arbeitsspeicherlimit pro Anwendung zu überschreiten. Dazu wird eine kleinere Subsamped-Version in den Arbeitsspeicher geladen.

Bitmapdimensionen und -typ lesen

Die BitmapFactory-Klasse bietet mehrere Decodierungsmethoden (decodeByteArray(), decodeFile(), decodeResource() usw.) zum Erstellen einer Bitmap aus verschiedenen Quellen. Wählen Sie für Ihre Bilddatenquelle die am besten geeignete Decodierungsmethode aus. Diese Methoden versuchen, Arbeitsspeicher für die erstellte Bitmap zuzuweisen, und können daher leicht zu einer OutOfMemory-Ausnahme führen. Jede Art von Decodierungsmethode hat zusätzliche Signaturen, mit denen Sie über die Klasse BitmapFactory.Options Decodierungsoptionen angeben können. Wenn Sie das Attribut inJustDecodeBounds beim Decodieren auf true festlegen, wird die Speicherzuweisung vermieden. Dabei wird null für das Bitmapobjekt zurückgegeben, aber outWidth, outHeight und outMimeType festgelegt. Mit dieser Technik können Sie die Abmessungen und den Typ der Bilddaten vor der Erstellung (und Speicherzuweisung) der Bitmap lesen.

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;

Um java.lang.OutOfMemory-Ausnahmen zu vermeiden, prüfen Sie die Abmessungen einer Bitmap, bevor Sie sie decodieren, es sei denn, Sie vertrauen darauf, dass die Quelle Ihnen Bilddaten von vorhersehbarer Größe liefert, die problemlos in den verfügbaren Arbeitsspeicher passen.

Herunterskalierte Version in den Arbeitsspeicher laden

Da die Bildabmessungen nun bekannt sind, können sie verwendet werden, um zu entscheiden, ob das vollständige Bild in den Speicher geladen werden soll oder ob stattdessen eine Subsamped-Version geladen werden soll. Hier sind einige Faktoren, die zu berücksichtigen sind:

  • Geschätzte Arbeitsspeichernutzung beim Laden des vollständigen Bildes in den Arbeitsspeicher.
  • Arbeitsspeichermenge, die Sie bereit sind, sich zum Laden dieses Bildes zu verpflichten, wenn andere Speicheranforderungen Ihrer Anwendung vorliegen.
  • Abmessungen des Ziel-ImageView oder der UI-Komponente, in die das Bild geladen werden soll.
  • Bildschirmgröße und -dichte des aktuellen Geräts.

Es lohnt sich beispielsweise nicht, ein Bild mit 1024 × 768 Pixel in den Speicher zu laden, wenn es schließlich in einer Miniaturansicht mit 128 × 96 Pixel in einem ImageView angezeigt wird.

Wenn Sie den Decoder anweisen möchten, eine kleinere Version des Bildes in den Arbeitsspeicher zu laden, setzen Sie inSampleSize in Ihrem BitmapFactory.Options-Objekt auf true. Beispiel: Ein Bild mit einer Auflösung von 2048 × 1536, das mit einem inSampleSize von 4 decodiert wird, erzeugt eine Bitmap von etwa 512 × 384. Das Laden in den Arbeitsspeicher belegt 0,75 MB statt 12 MB für das gesamte Bild (bei angenommener Bitmapkonfiguration von ARGB_8888). Hier ist eine Methode zur Berechnung eines Stichprobengrößenwerts, der auf Basis einer Zielbreite und ‐höhe eine Zweierpotenz ist:

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

Hinweis:Eine Potenz von zwei Wert wird berechnet, da der Decoder gemäß der inSampleSize-Dokumentation einen endgültigen Wert verwendet, indem er auf die nächste Potenz von zwei abgerundet wird.

Wenn Sie diese Methode verwenden möchten, decodieren Sie zuerst die Dekodierung, wobei inJustDecodeBounds auf true gesetzt ist. Übergeben Sie dann die Optionen und decodieren Sie sie dann noch einmal mit dem neuen Wert inSampleSize und inJustDecodeBounds auf 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);
}

Mit dieser Methode lässt sich eine Bitmap beliebiger Größe einfach in ein ImageView laden, das eine Miniaturansicht mit 100 × 100 Pixeln anzeigt, wie im folgenden Beispielcode gezeigt:

Kotlin

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

Java

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

Sie können einen ähnlichen Prozess zum Decodieren von Bitmaps aus anderen Quellen anwenden, indem Sie bei Bedarf die entsprechende BitmapFactory.decode*-Methode ersetzen.