Android 高效加载大图
- 前言
- 读取位图尺寸和类型
- 将按比例缩小的版本加载到内存中
前言
图片有各种形状和大小。在很多情况下,它们的大小超过了典型应用界面的要求。例如,系统“图库”应用会显示使用 Android 设备的相机拍摄的照片,这些照片的分辨率通常远高于设备的屏幕密度。
但是手机中每个应用的内存是有限的,程序占用了过高的内存就容易出现OOM(OutOfMemory)异常。理想情况下只希望在内存中加载较低分辨率的版本。分辨率较低的版本应与显示该版本的界面组件的大小相匹配。分辨率更高的图片不会带来任何明显的好处,但仍会占用宝贵的内存,并且会因为额外的动态缩放而产生额外的性能开销。
读取位图尺寸和类型
BitmapFactory
类提供了几种用于从各种来源创建 Bitmap
的解码方法(decodeByteArray()
、decodeFile()
、decodeResource()
等)。根据图片数据源选择最合适的解码方法。这些方法尝试为构造的位图分配内存,因此很容易导致 OutOfMemory
异常。每种类型的解码方法都有额外的签名,允许你通过 BitmapFactory.Options
类指定解码选项。在解码时将 inJustDecodeBounds
属性设置为 true
可避免内存分配,为位图对象返回 null
,但设置 outWidth
、outHeight
和 outMimeType
。此方法可让你在构造位图并为其分配内存之前读取图片数据的尺寸和类型。
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
异常,请先检查位图的尺寸,然后再对其进行解码,除非你绝对信任该来源可为你提供大小可预测的图片数据,以轻松适应可用的内存。
将按比例缩小的版本加载到内存中
既然图片尺寸已知,便可用于确定应将完整图片加载到内存中,还是应改为加载下采样版本。以下是需要考虑的一些因素:
- 在内存中加载完整图片的估计内存使用量
- 根据应用的其他内存要求,你愿意分配用于加载此图片的内存量
- 用于展示这张图片的控件的实际大小
- 当前设备的屏幕尺寸和分辨率
例如,如果 1024x768 像素的图片最终会在 ImageView
中显示为 128x96 像素缩略图,则不值得将其加载到内存中。
如需让解码器对图片进行下采样,以将较小版本加载到内存中,请在 BitmapFactory.Options
对象中将 inSampleSize
设置为 true
。例如,分辨率为 2048x1536 且以 4 作为 inSampleSize
进行解码的图片会生成大约 512x384 的位图。将此图片加载到内存中需使用 0.75MB,而不是完整图片所需的 12MB(假设位图配置为 ARGB_8888)。下面的方法用于计算样本大小值,即基于目标宽度和高度的 2 的幂:
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// 源图片的高度和宽度
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;
// 计算最大的 inSampleSize 值,该值应是 2 的幂次,
// 且高度和宽度均大于所要求的高度和宽度。
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
inSampleSize: 如果设置为 > 1 的值,则请求解码器对原始图像进行二次采样,返回较小的图像以节省内存。样本大小是任一维度中与解码位图中的单个像素相对应的像素数。例如,inSampleSize == 4 返回图像的宽度/高度为原始图像的 1/4,像素数为 1/16。任何<= 1的值都被视为与1相同。
注意: 解码器使用基于2的幂的最终值,任何其他值将向下舍入到最接近的2的幂。
如需使用此方法,先将 inJustDecodeBounds
设为 true
进行解码,传递选项,然后使用新的 inSampleSize
值并将 inJustDecodeBounds
设为 false
再次进行解码:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 计算inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 使用计算的inSampleSize值再次解析图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
采用此方法,可以轻松地将任意大尺寸的位图加载到显示 100x100 像素缩略图的 ImageView
中,如以下示例代码所示:
imageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.screen_capture, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
Log.i(TAG, "onCreate: bitmap == null ? " + (null == bitmap));
Log.i(TAG, "onCreate: width: " + imageWidth);
Log.i(TAG, "onCreate: height: " + imageHeight);
Log.i(TAG, "onCreate: type: " + imageType);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
bitmap = BitmapFactory.decodeResource(
getResources(), R.mipmap.screen_capture, options);
Log.i(TAG, "onCreate: size: " + (bitmap.getByteCount() / 1024 / 1024) + " MB");
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, 100, 100);
Log.i(TAG, "onCreate: inSampleSize: " + options.inSampleSize);
bitmap = BitmapFactory.decodeResource(
getResources(), R.mipmap.screen_capture, options);
imageHeight = options.outHeight;
imageWidth = options.outWidth;
imageType = options.outMimeType;
Log.i(TAG, "onCreate: bitmap == null ? " + (null == bitmap));
Log.i(TAG, "onCreate: width: " + imageWidth);
Log.i(TAG, "onCreate: height: " + imageHeight);
Log.i(TAG, "onCreate: type: " + imageType);
Log.i(TAG, "onCreate: size: " + (bitmap.getByteCount() / 1024) + " KB");