图片有不同的形状与大小。在大多数情况下它们的实际大小都比需要呈现的尺寸大很多。例如,系统的图库应用会显示我们使用相机拍摄的照片,但这些图片的分辨率通常都比设备屏幕的分辨率要高很多。
考虑到应用是在有限的内存下工作的,理想情况下,我们只需要在内存中加载一个低分辨率的照片即可。为了有更好的显示效果,这个低分辨率的照片应该与展示它的UI组件大小相匹配。加载高分辨率的图片不仅没有任何显而易见的好处,还会占用宝贵的内存资源,并且由于图片的即时缩放,还会增加额外的性能开销。
下面将会介绍如何解码大型位图,加载缩小版图片,从而避免超出应用程序的内存限制。
1.读取位图的尺寸与类型
BitmapFactory 提供了多种解码方法(decodeByteArray(), decodeFile(),decodeResource() 等),用于从各种来源创建 Bitmap。我们应该根据图片的数据源选择最合适的解码方法。这些方法在构造位图的时候会尝试分配内存,因此很容易导致 OutOfMemory 异常。每一种解码方法都可以通过 BitmapFactory.Options 设置一些附加的标记,以此来指定解码选项。设置 inJustDecodeBounds 属性为 true 可以在解码时避免内存的分配,返回的 Bitmap 为 null,但是可以获取到 outWidth,outHeight 与 outMimeType。该技术可以允许你在构造 Bitmap 之前优先读图片的尺寸与类型(以及内存分配)。
1 | BitmapFactory.Options options = new BitmapFactory.Options(); |
为了避免 java.lang.OutOfMemory 的异常,我们需要在真正解析图片之前检查它的尺寸,除非你能确定这个数据源提供了准确无误的图片且不会导致过多内存的占用。
2.将缩小版本加载到内存中
通过上面的步骤我们已经获取到了图片的尺寸,这些数据可以用来帮助我们决定应该加载整个图片还是加载一个缩小的版本到内存中。有下面一些因素需要考虑:
- 评估加载完整图片需要占用的内存。
- 程序在加载这张图片时可能涉及到的其他内存需求。
- 展示这张图片的控件的尺寸大小。
- 当前设备的屏幕大小和屏幕密度。
例如,如果把一个大小为 1024x768 像素的图片显示到大小为 128x96 像素的 ImageView 上,就没有必要把整张原图都加载到内存中。
为了告诉解码器去加载一个缩小版本的图片到内存中,需要在 BitmapFactory.Options 中设置 inSampleSize 的值。例如, 一个分辨率为 2048x1536 的图片,如果设置 inSampleSize 为 4,那么会产出一个大约 512x384 大小的 Bitmap。加载这张缩小的图片仅仅使用大概 0.75MB 的内存,如果是加载完整尺寸的图片,那么大概需要花费 12MB(前提都是Bitmap的配置是 ARGB_8888)。下面有一段根据目标图片大小来计算 Sample 图片大小的代码示例:
1 | public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { |
注意:设置 inSampleSize 为 2 的幂是因为解码器最终还是会对非 2 的幂的数进行向下处理,获取到最靠近 2 的幂的数。
要使用该方法,首先需要设置 inJustDecodeBounds 为 true 进行解码,传递 options 参数获取新的 inSampleSize,然后使用新的 inSampleSize 值并设置 inJustDecodeBounds 为 false 进行再次解码:
1 | public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { |
此方法可以轻松地将任意大小的位图加载到显示 100x100 像素缩略图的 ImageView 中,如以下示例代码所示:
1 | mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100)); |
我们可以根据需要替换相应的 BitmapFactory.decode * 方法,按照类似的流程来解码来自其他来源的位图。