高效加载大型位图

注意: 有几个库遵循加载图像的最佳实践。您可以在您的应用中使用这些库以最优化的方式加载图像。我们推荐使用 Glide 库,它可以尽可能快且流畅地加载和显示图像。其他流行的图像加载库包括来自 Square 的 Picasso、来自 Instacart 的 Coil 以及来自 Facebook 的 Fresco。这些库简化了与 Android 上的位图和其他类型的图像相关的许多复杂任务。

图像有多种形状和大小。在许多情况下,它们比典型应用用户界面 (UI) 所需的要大。例如,系统图库应用显示使用 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 组件的尺寸。
  • 当前设备的屏幕尺寸和密度。

例如,如果最终要在 ImageView 中以 128x96 像素的缩略图显示 1024x768 像素的图像,则不值得将其加载到内存中。

要告诉解码器对图像进行子采样,将较小版本加载到内存中,请在 BitmapFactory.Options 对象中将 inSampleSize 设置为 true。例如,分辨率为 2048x1536 的图像,如果使用 inSampleSize 为 4 进行解码,则会生成大约 512x384 的位图。将其加载到内存中使用 0.75MB,而不是完整图像的 12MB(假设位图配置为 ARGB_8888)。以下是一种根据目标宽度和高度计算 2 的幂的样本大小值的方法

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

注意: 计算 2 的幂值是因为解码器使用最终值向下舍入到最接近的 2 的幂,如 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* 方法。