以有效率的方式載入大型點陣圖

注意:許多程式庫都會採用最佳做法載入圖片。您可以在應用程式中使用這些程式庫,以最適合的方式載入圖片。建議您使用 Glide 程式庫,這可以快速且順暢地載入及顯示圖片。其他常用的圖片載入程式庫包括 Square 的 Picasso、Instacart 的 Coil,以及 Facebook 的 Fresco。這些程式庫可簡化與 Android 點陣圖和其他類型圖片相關的大多數複雜工作。

圖片的形狀和大小各不相同。比起一般的應用程式使用者介面 (UI),圖片在多數情況下所佔空間會更大。舉例來說,系統 Gallery 應用程式會顯示使用 Android 裝置相機拍攝的相片,這些相片的解析度通常遠高於裝置的畫面密度。

由於可用的記憶體有限,因此建議您僅在記憶體中載入解析度較低的版本。此版本的大小應與顯示該版本的 UI 元件相符。解析度較高的圖片不僅沒有顯著優勢,還會佔用寶貴的記憶體,且會在執行其他縮放操作時產生額外的效能負擔。

本課程會逐步說明如何在記憶體中載入較小的向下取樣版本,讓您既可以解碼大型點陣圖,又不會超過個別應用程式的記憶體限制。

讀取點陣圖維度和類型

BitmapFactory 類別提供可從多種來源建立 Bitmap 的數種解碼方法 (decodeByteArray()decodeFile()decodeResource() 等)。請根據圖片資料來源選擇最合適的解碼方法。這些方法會試圖為已建構的點陣圖分配記憶體,因此很容易導致發生 OutOfMemory 例外狀況。每種解碼方法都有額外的簽名,可讓您透過 BitmapFactory.Options 類別指定解碼選項。在解碼時將 inJustDecodeBounds 屬性設為 true,可省去記憶體配置程序,也就是能為點陣圖物件傳回 null,但設定 outWidthoutHeightoutMimeType。您可以運用這項技巧,在建構點陣圖 (和記憶體配置) 前讀取圖片資料的尺寸和類型。

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;

為避免發生 java.lang.OutOfMemory 例外狀況,除非您絕對信任來源能為您提供可預測大小的圖片資料,且此資料絕對不會超過可用記憶體的大小,否則請在解碼前先檢查點陣圖的尺寸。

在記憶體中載入縮減版本

既然您已經知道圖片尺寸,不妨根據這些尺寸決定是應在記憶體中載入完整圖片,還是應改為載入向下取樣版本。以下列出幾個需要考量的因素:

  • 在記憶體中載入完整圖片,預計需要使用多少記憶體。
  • 計算應用程式的任何其他記憶體需求後,您想將多少記憶體用於載入這張圖片。
  • 要將圖片載入到什麼尺寸的目標 ImageView 或 UI 元件中。
  • 目前裝置的螢幕大小和密度。

舉例來說,如果 1024x768 像素的圖片最終會在 ImageView 中顯示為 128x96 像素的縮圖,那就不值得將其載入記憶體中。

如要讓解碼器對圖片進行向下取樣,請將 BitmapFactory.Options 物件中的 inSampleSize 設為 true,在記憶體中載入較小的版本。舉例來說,如果圖片的解析度是 2048x1536,且解碼時的 inSampleSize 為 4,就會產生大約 512x384 的點陣圖。將此載入記憶體會用掉 0.75 MB,而非完整圖片所需的 12 MB (假設點陣圖設定為 ARGB_8888)。以下是根據目標寬度和高度計算樣本大小值的方法,該值為二的次方:

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

注意:根據 inSampleSize 說明文件所述,解碼器使用最終值時會四捨五入至最接近的二次方,因此計算的是二次方值。

如要使用此方法,請先將 inJustDecodeBounds 設為 true 來進行解碼,然後傳遞選項,再使用新的 inSampleSize 值再次解碼,並將 inJustDecodeBounds 設為 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);
}

這個方法可讓您輕鬆將任意尺寸的點陣圖載入至顯示 100x100 像素縮圖的 ImageView 中,如以下範例程式碼所示:

Kotlin

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

Java

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

您可以按照類似的程序解碼其他來源的點陣圖,只要視需要替換適當的 BitmapFactory.decode* 方法即可。