在Android的开发中,我们经常回去处理一些图片相关的问题,比如当加载图片到内存中产生的OOM(OutOfMemory)异常、图片加载到内存中占多大内存的问题、jpg png两种常见的图片的原理及区别。
图片加载到内存所占内存大小的问题
在讲OOM异常前需要对图片的加载有所了解,所以在这里就先介绍图片加载的问题。
图片加载到内存中的大小,不是直接由图片的存储大小来决定的。比如一个10k大小的png格式的图片加载到内存可能就不止10k了。那应该怎么计算呢?
图片加载到内存中的大小=图片的宽×图片的高×该图片一个像素所占的位数/8
举个例子:一个1024*1024像素的图片,每个像素是32位,那么他的大小就是1024×1024×32÷8=4M。通常图片保存成jpg、png格式是经过压缩处理的,它的存储大小可能就只有几k。这就是为什么我们在加载一个10多k的图片是会出现OOM异常的原因。
加载较大的图片
在展示高分辨率图片的时候,最好先将图片进行压缩。压缩后的图片大小应该和用来展示它的控件大小相近,在一个很小的ImageView上显示一张超大的图片不会带来任何视觉上的好处,但却会占用很多的内存,而且在性能上还可能会带来负面影响。下面我们就来看一看,如何对一张大图片进行适当的压缩,让它能够以最佳大小显示的同时,还能防止OOM的出现。
BitmapFactory这个类提供了多个解析方法(decodeByteArray, decodeFile, decodeResource等)用于创建Bitmap对象,我们应该根据图片的来源选择合适的方法。比如SD卡中的图片可以使用decodeFile方法,网络上的图片可以使用decodeStream方法,资源文件中的图片可以使用decodeResource方法。这些方法会尝试为已经构建的bitmap分配内存,这时就会很容易导致OOM出现。为此每一种解析方法都提供了一个可选的BitmapFactory.Options参数,将这个参数的inJustDecodeBounds属性设置为true就可以让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。虽然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。如下代码所示:
1 BitmapFactory.Options options = new BitmapFactory.Options(); 2 options.inJustDecodeBounds = true; 3 BitmapFactory.decodeResource(getResources(), R.id.myimage, options); 4 int imageHeight = options.outHeight; 5 int imageWidth = options.outWidth; 6 String imageType = options.outMimeType;
在加载图片时,最好每次都先检查一下图片的大小,除非你能确保这个图片不会导致OOM异常。
通过上面的代码我们能得到图片的大小,下面我们来对图片进行压缩处理。通过设置BitmapFactory.Options中inSampleSize的值就可以实现。比如我们有一张2048*1536像素的图片,将inSampleSize的值设置为4,就可以把这张图片压缩成512*384像素。原本加载这张图片需要占用13M的内存,压缩后就只需要占用0.75M了(假设图片是ARGB_8888类型,即每个像素点占用4个字节)。下面的方法可以根据传入的宽和高,计算出合适的inSampleSize值:
1 public static int calculateInSampleSize(BitmapFactory.Options options, 2 int reqWidth, int reqHeight) { 3 // 源图片的高度和宽度 4 final int height = options.outHeight; 5 final int width = options.outWidth; 6 int inSampleSize = 1; 7 if (height > reqHeight || width > reqWidth) { 8 // 计算出实际宽高和目标宽高的比率 9 final int heightRatio = Math.round((float) height / (float) reqHeight); 10 final int widthRatio = Math.round((float) width / (float) reqWidth); 11 // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高 12 // 一定都会大于等于目标的宽和高。 13 inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; 14 } 15 return inSampleSize; 16 }
使用这个方法,首先你要将BitmapFactory.Options的inJustDecodeBounds属性设置为true,解析一次图片。然后将BitmapFactory.Options连同期望的宽度和高度一起传递到到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。之后再解析一次图片,使用新获取到的inSampleSize值,并把inJustDecodeBounds设置为false,就可以得到压缩后的图片了。
大量图片的缓存处理
在你应用程序的UI界面加载一张图片是一件很简单的事情,但是当你需要在界面上加载一大堆图片的时候,情况就变得复杂起来。在很多情况下,(比如使用ListView, GridView 或者 ViewPager 这样的组件),屏幕上显示的图片可以通过滑动屏幕等事件不断地增加,最终导致OOM。
为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行GC操作。用这种思路来解决问题是非常好的,可是为了能让程序快速运行,在界面上迅速地加载图片,你又必须要考虑到某些图片被回收之后,用户又将它重新滑入屏幕这种情况。这时重新去加载一遍刚刚加载过的图片无疑是性能的瓶颈,你需要想办法去避免这个情况的发生。
这个时候,使用内存缓存技术可以很好的解决这个问题,它可以让组件快速地重新加载和处理图片。下面我们就来看一看如何使用内存缓存技术来对图片进行缓存,从而让你的应用程序在加载很多图片的时候可以提高响应速度和流畅性。
我们可以使用LruCache来处理这个问题,例子如下:
1 private LruCache<String, Bitmap> mMemoryCache; 2 3 @Override 4 protected void onCreate(Bundle savedInstanceState) { 5 // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。 6 // LruCache通过构造函数传入缓存值,以KB为单位。 7 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); 8 // 使用最大可用内存值的1/8作为缓存的大小。 9 int cacheSize = maxMemory / 8; 10 mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { 11 @Override 12 protected int sizeOf(String key, Bitmap bitmap) { 13 // 重写此方法来衡量每张图片的大小,默认返回图片数量。 14 return bitmap.getByteCount() / 1024; 15 } 16 }; 17 } 18 19 public void addBitmapToMemoryCache(String key, Bitmap bitmap) { 20 if (getBitmapFromMemCache(key) == null) { 21 mMemoryCache.put(key, bitmap); 22 } 23 } 24 25 public Bitmap getBitmapFromMemCache(String key) { 26 return mMemoryCache.get(key); 27 }
另外,在github上有个很好用的解决大量图片缓存的框架—xUtils,其中BitmapUtils模块就是用来解决这个问题的。