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

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

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

由於您使用的記憶體有限,因此建議您在記憶體中載入解析度較低的版本。低解析度的版本應與顯示該使用者介面的 UI 元件大小相符。解析度較高的圖片沒有明顯的優勢,但仍會佔用寶貴的記憶體,且會因為即時擴充而產生額外的效能負擔。

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

讀取點陣圖維度和類型

BitmapFactory 類別提供數種解碼方法 (decodeByteArray()decodeFile()decodeResource() 等),以便從多種不同的來源建立 Bitmap。請根據圖片資料來源選擇最合適的解碼方法。這些方法會嘗試分配已建構點陣圖的記憶體,因此很容易導致發生 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,而非 12MB (假設點陣圖設定為 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* 方法,從其他來源解碼點陣圖。