Caricamento efficiente di bitmap di grandi dimensioni

Nota: esistono diverse librerie che seguono le best practice per il caricamento delle immagini. Puoi usare queste librerie nella tua app per caricare le immagini nel modo più ottimizzato. Ti consigliamo la libreria Glide, che carica e visualizza le immagini nel modo più rapido e fluido possibile. Altre librerie di caricamento immagini popolari includono Picasso da Square, Coil di Instacart e Fresco da Facebook. Queste librerie semplificano la maggior parte delle attività complesse associate ai bitmap e ad altri tipi di immagini su Android.

Le immagini sono disponibili in tutte le forme e dimensioni. In molti casi, sono più grandi del necessario per una tipica interfaccia utente (interfaccia utente). Ad esempio, l'applicazione Galleria di sistema mostra le foto scattate con la fotocamera dei dispositivi Android, che in genere hanno una risoluzione molto più elevata rispetto alla densità dello schermo del dispositivo.

Dato che stai lavorando con memoria limitata, idealmente vuoi caricare solo una versione a risoluzione più bassa in memoria. La versione con risoluzione inferiore deve corrispondere alle dimensioni del componente dell'interfaccia utente che la mostra. Un'immagine con una risoluzione più alta non offre alcun vantaggio visibile, ma occupa comunque memoria preziosa e comporta un ulteriore sovraccarico delle prestazioni dovuto a una scalabilità aggiuntiva immediata.

Questa lezione illustra la decodifica di bitmap di grandi dimensioni senza superare il limite di memoria per applicazione caricando una versione sottocampionata più piccola in memoria.

Lettura tipo e dimensioni bitmap

La classe BitmapFactory offre diversi metodi di decodifica (decodeByteArray(), decodeFile(), decodeResource() e così via) per creare un Bitmap da varie origini. Scegli il metodo di decodifica più appropriato in base all'origine dati dell'immagine. Questi metodi tentano di allocare memoria per la bitmap creata e possono pertanto facilmente comportare un'eccezione OutOfMemory. Ogni tipo di metodo di decodifica ha firme aggiuntive che ti consentono di specificare le opzioni di decodifica tramite la classe BitmapFactory.Options. L'impostazione della proprietà inJustDecodeBounds su true durante la decodifica evita l'allocazione della memoria, restituendo null per l'oggetto bitmap, ma impostando outWidth, outHeight e outMimeType. Questa tecnica consente di leggere le dimensioni e il tipo dei dati dell'immagine prima della creazione (e dell'allocazione della memoria) della bitmap.

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;

Per evitare eccezioni java.lang.OutOfMemory, controlla le dimensioni di una bitmap prima di decodificarla, a meno che tu non ti fidi assolutamente dell'origine di dati immagine di dimensioni prevedibili che rientrino comodamente nella memoria disponibile.

Carica una versione scalata in memoria

Ora che sono note, le dimensioni dell'immagine possono essere utilizzate per decidere se caricare in memoria l'immagine completa o se caricare una versione sottocampionata. Ecco alcuni fattori da considerare:

  • Utilizzo stimato della memoria per il caricamento dell'immagine intera in memoria.
  • Quantità di memoria che intendi impegnare per il caricamento di questa immagine in base a qualsiasi altro requisito di memoria della tua applicazione.
  • Dimensioni del componente di interfaccia utente o ImageView di destinazione in cui deve essere caricata l'immagine.
  • Dimensioni e densità dello schermo del dispositivo attuale.

Ad esempio, non vale la pena caricare un'immagine da 1024 x 768 pixel in memoria se alla fine verrà visualizzata in una miniatura di 128 x 96 pixel in un ImageView.

Per indicare al decoder di sottocampionare l'immagine, caricando una versione più piccola in memoria, imposta inSampleSize su true nell'oggetto BitmapFactory.Options. Ad esempio, un'immagine con risoluzione 2048 x 1536 decodificata con un valore inSampleSize di 4 produce una bitmap di circa 512 x 384. Il caricamento di questo elemento in memoria utilizza 0,75 MB anziché 12 MB per l'immagine completa (presupponendo una configurazione bitmap ARGB_8888). Ecco un metodo per calcolare un valore di dimensione campione che corrisponde alla potenza di due in base alla larghezza e all'altezza target:

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

Nota: viene calcolata una potenza di due valori perché il decoder utilizza un valore finale arrotondando per difetto al valore più vicino di due, come indicato nella documentazione su inSampleSize.

Per utilizzare questo metodo, devi prima decodificare con inJustDecodeBounds impostato su true, trasmettere le opzioni, quindi decodificare di nuovo utilizzando il nuovo valore inSampleSize e inJustDecodeBounds impostato su 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);
}

Questo metodo semplifica il caricamento di una bitmap di dimensioni arbitrarie in un ImageView che visualizza una miniatura di 100 x 100 pixel, come mostrato nel codice di esempio riportato di seguito:

Kotlin

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

Java

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

Puoi seguire una procedura simile per decodificare le bitmap da altre origini, sostituendo il metodo BitmapFactory.decode* appropriato in base alle tue esigenze.