Android 培训

高效加载大图

编写:kesenhoo - 原文:http://developer.android.com/training/displaying-bitmaps/load-bitmap.html

图片有不同的形状与大小。在大多数情况下它们的实际大小都比需要呈现出来的要大很多。例如,系统Gallery程序会显示那些你使用Camera拍摄好的照片,但是那些图片的分辨率通常都比你的设备屏幕分辨率要高很多。

考虑到程序是在有限的内存下工作,理想情况是你只需要在内存中加载一个低分辨率的照片即可。这个低分辨率的照片应该是与你的UI大小所匹配的,这样更便于显示。加载一个超过屏幕分辨率的高分辨率照片不仅没有任何显著的可见好处,还会占用宝贵的内存资源,另外在快速滑动图片时容易导致额外的效率问题。

这一课会介绍如何通过加载一个缩小版本的图片,从而避免超出程序的内存限制。

读取位图的尺寸与类型(Read Bitmap Dimensions and Type)

BitmapFactory提供了一些decode的方法 (decodeByteArray(), decodeFile(), decodeResource(), etc.) 用来从不同的资源中创建一个Bitmap。 根据你的图片数据源来选择合适的decode方法。 那些方法在构造位图的时候会尝试分配内存,因此会容易导致OutOfMemory的异常。每一种decode方法都提供了通过BitmapFactory.Options 来设置一些附加的标记来指定decode的选项。设置 inJustDecodeBounds 属性为true可以在decoding的时候避免内存的分配,它会返回一个null的bitmap,但是可以获取到 outWidth, outHeight 与 outMimeType。这个技术可以允许你在构造bitmap之前优先读图片的尺寸与类型。

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 的异常,我们需要在真正decode图片之前检查它的尺寸,除非你确定这个数据源提供了准确无误的图片且不会导致占用过多的内存。

加载一个按比例缩小的版本到内存中(Load a Scaled Down Version into Memory)

通过上面的步骤我们已经知道了图片的尺寸,那些数据可以用来决定是应该加载整个图片到内存中还是加载一个缩小的版本。有下面一些因素需要考虑:

  • 评估加载完整图片所需要耗费的内存。
  • 程序在加载这张图片时会涉及到其他内存需求。
  • 呈现这张图片的组件的尺寸大小。
  • 屏幕大小与当前设备的屏幕密度。

例如,如果把一个原图是1024*768 pixel的图片显示到ImageView为128*96 pixel的缩略图上就没有必要把整张图片都加载到内存中。

为了告诉decoder去加载一个低版本的图片到内存,需要在你的BitmapFactory.Options 中设置 inSampleSize 的值。例如, 一个分辨率为2048x1536的图片,如果设置 inSampleSize 为4,那么会产出一个大概为512x384的bitmap。加载这张小的图片仅仅使用大概0.75MB的内存,如果是加载完整尺寸的图片,那么大概需要花费12MB(前提都是bitmap的配置是 ARGB_8888). 下面有一段根据目标图片大小来计算Sample图片大小的代码示例:

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

Note: 设置inSampleSize为2的幂是因为decoder最终还是会对非2的幂的数进行向下处理,获取到最靠近2的幂的数。详情参考inSampleSize的文档。

为了使用这个方法,首先需要设置 inJustDecodeBoundstrue, 把options的值传递过来,然后设置 inSampleSize 的值并设置 inJustDecodeBounds 为 false 来重新Decode一遍。

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

使用上面这个方法可以简单的加载一张任意大小的图片并最终显示为一个接近100*100 pixel样式的缩略图。像下面演示的一样:

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

你可以通过替换合适的BitmapFactory.decode* 方法来写一个类似的方法从其他的数据源进行decode bitmap。