由于Bitmap的特殊以及Android对单个应用所施加的内存限制,比如16MB,这导致加载Bitmap的时候很容易出现内存溢出。下面这个异常信息在开发中应该时常遇到:
java.lang.OutofMemoryError:bitmap size exceeds VM budget
因此如何高效地加载Bitmap是一个很重要也很容易被开发者忽视的问题。
12.1 Bitmap的高效加载
首先如何加载Bitmap:
Bitmap在Android中指的是一张图片,可以是png格式也可以是jpg等其他常见的图片格式。那么如何加载一个图片呢?BitmapFactory类提供了四类方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分别用于支持从文件系统、资源、输入流以及字节数组中加载出一个Bitmap对象,其中decodeFile和decodeResource又间接调用了decodeStream方法,这四类方法最终是在Android的底层实现的,对应着BitmapFactory类的几个native方法。
- 如何高效地加载Bitmap呢?
其实核心思想也很简单,那就是采用BitmapFactory.Options来加载所需尺寸的图片。这里假设通过ImageView来显示图片,很多时候ImageView并没有图片的原始尺寸那么大,这个时候把整个图片加载进来后再设给ImageView,这显然是没必要的,因为ImageView并没有办法显示原始的图片。通过BitmapFactory.Options就可以按一定的采样率来加载缩小后的图片,将缩小后的图片在ImageView中显示,这样就会降低内存占用从而在一定程度上避免OOM,提高了Bitmap加载时的性能。
BitmapFactory提供的加载图片的四类方法都支持BitmapFactory.Options参数,通过它们就可以很方便地对一个图片进行采样缩放。
- 根据BitmapFactory.Options中的inSampleSize参数的大小进行像素的缩放。缩放比例【1/(inSampleSize的2次方)】
- 官方文档指出:inSampleSize的取值应该总是为2的指数,比如1、2、4、8、16,等等。如果外界传递给系统的inSampleSize不为2的指数,那么系统会向下取整并选择一个最接近2的指数来代替,比如3,系统会选择2来代替.
通过采样率即可有效地加载图片,那么到底如何获取采样率呢?获取采样率也很简单,遵循如下流程:
- 将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片。
- 从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数。
- 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize。
将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片。
有了上面的两个方法,实际使用的时候就很简单了,比如ImageView所期望的图片大小为100*100像素,这个时候就可以通过如下方式高校地加载并显示图片:
mImageView.setImageBitmap(decodeSampleBitmapFromResource(getResources(),R.id.myimage,100,100))
除了BitmapFactory的decodeResource方法,其他三个decode系列的方法也是支持采样加载的,并且处理方式也是类似的,但是decodeStream方法稍微有点特殊。
12.2Android中的缓存策略
如何避免过多的流量消耗呢?
缓存
当程序第一次从网络加载图片后,就将其缓存到存储设备上,这样下次使用这张图片就不用再从网络上获取了,这样就为用户节省了流量。很多时候为了提高应用的用户体验,往往还会把图片在内存中再缓存一份,这样当应用打算从网络上请求一张图片时,程序会首先从内存中去获取,如果内存中没有那就从存储设备中去获取,如果存储设备中也没有,那就从网络上下载这张图片。因为从内存中加载图片比从存储设备中加载图片要快,所以这样既提高了程序的效率又为用户节约了不必要的流量开销。
- 目前常用的一种缓存算法是LRU(Least Recently Used),LRU是近期最少使用的算法,:
- 核心思想是当缓存满时,会有线淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LruCache和DiskLruCache,LruCache用于实现内存缓存,而DiskLruCache则充当了存储设备缓存,通过这二者的完美结合,就可以很方便地实现一个具有很高实用价值的ImageLoader.
LruCache是一个泛型类,它内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象。这里读者要明白强引用、软引用和弱引用的区别,如下所示:
- 强引用:直接的对象引用;
- 软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收;
- 弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收。
另外LruCache是线程安全的,下面是LruCache的定义:
public class LruCache<K,V>{
private final LinkedHashMap<k,v> map;
....
}
除了LruCache的创建以外,还有缓存的获取和添加,这也很简单,从LruCache中获取一个缓存对象,如下所示:
mMemoryCache.get(key);
向LruCache中添加一个缓存对象,如下所示:
mMemoryCache.put(key,bitmap);
12.2.2 DiskLruCache
- DiskLruCache的创建
DiskLruCache并不能通过构造方法来创建,它提供了open方法用于创建自身,如下所示:
public static DiskLruCache open(File directory,int appVersion,int valueCount,long maxSize)
- DiskLruCache的缓存的创建、添加和查找
DiskLruCache的缓存添加的操作是通过Editor完成的,Editor表示一个缓存对象的编程对象。
12.2.3 ImageLoader的实现
一般来说,一个优秀的ImageLoader应该具备如下功能: - 图片的同步加载;
- 图片的异步加载;
- 图片压缩;
- 内存缓存;
- 磁盘缓存;
网络拉取;
上面对ImageLoader的功能做了一个全面的分析,下面就可以一步步实现一个ImageLoader了,这里主要分为如下几步。图片压缩功能的实现下面的类用于完成图片的压缩功能
public class ImageResizer{
private static final String TAG="ImageResizer";
public ImageResizer(){
}
public Bitmap decodeSampleBitmapFromResource(Resources res,int resId,int reqWidth,int reqHeight){
final BitmapFactory.Options options=new BitmapFactory.Options();
options.inJustDecodeBounds=true;
BitmapFactory.decodeResource(res,resId,options);
options.inSampleSize=calculateInSampleSize(options,reqWidth,reqHeight);
options.inJustDecodeBounds=false;
return BitmapFactory.decodeResource(res,resId,options);
}
public Bitmap decodeSampledBitmapFromFileDescriptior(FileDescriptor fd,int reqWidth,int reqHeight){
final BitmapFactory.Options options=new BitmapFactory.Options();
options.inJustDecodeBounds=true;
BitmapFactory.decodeFileDescriptor(fd,null,options);
options.SampleSize=calculateInSampleSize(options,reqWidth,reqHeight);
options.inJustDecodeBounds=false;
return BitmapFactory.decodeFileDescriptor(fd,null,options);
}
public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth,int reqHeight){
if(reqWidth==0||reqHeight==0){
return 1;
}
final int height=options.outHeight;
finla int width=options.outWidth;
int inSampleSize=1;
if(height>reqHeight||width>reqWidth){
final int halfHeight=height/2;
final int halfWidth=width/2;
while((halfHeight/inSampleSize)>=reqHeight&&(halfWidth/inSampleSize)>=reqWidth){
inSampleSize*=2;
}
}
return inSampleSize;
}
}
ImageLoader的使用
12.3.2 优化列表的卡顿现象
这个问题困扰了很多开发者,其实答案很简单,不要再主线程中做太耗时的操作即可提高滑动的流畅度,可以从三个方面来说明这个问题。
首先,不要再getView中执行耗时操作。对于上面的例子来说,如果直接在getView方法中加载图片,肯定会导致卡顿,因为加载图片是一个耗时的操作,这种操作必须通过异步的方式来处理,就像ImageLoader实现的那样。
其次,控制异步任务的执行频率。这一点也很重要,对于列表来说,仅仅在getView中采用异步操作是不够的。考虑一种情况,以照片墙来说,在getView方法中会通过ImageLoader的bindBitmap方法来异步加载图片,但是如果用户刻意地频繁上下滑动,这一瞬间产生上百个异步任务,这些异步任务会造成线程池的拥堵并随即带来大量的UI更新操作,这是没有意义的。由于一瞬间存在大量的UI更新操作,这些UI操作是运行在主线程的,这就会造成一定程度的卡顿。
如何解决呢?
可以考虑在列表滑动的时候停止加载图片,尽管这个过程是异步的,等列表停下来以后再加载图片仍然可以获得良好的用户体验。具体实现时,可以给ListView或者GridView设置setOnScrollListener,并在OnScrollListener的onScrollStateChanged方法中判断列表是否处于滑动状态,如果是的话就停止加载图片,如下所示:
public void onScrollSateChanged(AbsListView view,int scrollState){
if(scrollState==onScrollListener.SCROLL_STATE_IDLE){
mIsGridViewIdle=true;
mImageAdapter.notifyDataSetChanged();
}else{
mIsGridViewIdle=false;
}
}
然后再getView方法中,仅当列表静止时才能加载图片,如下所示:
if(mIsGridViewIdle && mCanGetBitmapFromNetWork){
imageView.setTag(uri);
mImageLoader.bindBitmap(uri,imageView,mImageWidth,mImageWidth);
}
一般来说,经过上面两个步骤,列表都不会有卡顿现象,但是在某些情况下,列表还是会有偶尔的卡顿现象,这个时候还可以开启硬件加速。绝大多数情况下,硬件加速都可以解决莫名的卡顿问题,通过设置android:hardwareAccelerated=”true”即可为Activity开启硬件加速。