高效加载大型位图

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

图片的形状和大小各不相同。在许多情况下,它们比典型的应用用户界面 (UI) 所需的尺寸要大。例如,系统图库应用会显示使用您的 Android 设备相机拍摄的照片,这些照片的分辨率通常远高于您设备的屏幕密度。

鉴于您使用的是有限的内存,理想情况下,您只想在内存中加载分辨率较低的版本。较低分辨率的版本应与显示该图片的界面组件的大小相匹配。分辨率较高的图片没有任何可见优势,但仍然占用宝贵的内存,并且由于额外的即时缩放会产生额外的性能开销。

本课程将引导您如何在不超出每个应用的内存限制的情况下解码大型位图,方法是在内存中加载尺寸较小的采样版本。

读取位图尺寸和类型

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 或界面组件的尺寸。
  • 当前设备的屏幕尺寸和密度。

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

要告诉解码器对图片进行采样,从而将尺寸较小的版本加载到内存中,请在您的 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 的幂,因为解码器会根据 inSampleSize 文档,通过向下舍入到最接近的 2 的幂来使用最终值。

要使用此方法,请先在 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* 方法。