由于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方法。
其实核心思想也很简单,那就是采用BitmapFactory.Options来加载所需尺寸的图片。这里假设通过ImageView来显示图片,很多时候ImageView并没有图片的原始尺寸那么大,这个时候把整个图片加载进来后再设给ImageView,这显然是没必要的,因为ImageView并没有办法显示原始的图片。通过BitmapFactory.Options就可以按一定的采样率来加载缩小后的图片,将缩小后的图片在ImageView中显示,这样就会降低内存占用从而在一定程度上避免OOM,提高了Bitmap加载时的性能。
BitmapFactory提供的加载图片的四类方法都支持BitmapFactory.Options参数,通过它们就可以很方便地对一个图片进行采样缩放。
通过采样率即可有效地加载图片,那么到底如何获取采样率呢?获取采样率也很简单,遵循如下流程:
将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片。
有了上面的两个方法,实际使用的时候就很简单了,比如ImageView所期望的图片大小为100*100像素,这个时候就可以通过如下方式高校地加载并显示图片:
mImageView.setImageBitmap(decodeSampleBitmapFromResource(getResources(),R.id.myimage,100,100))
除了BitmapFactory的decodeResource方法,其他三个decode系列的方法也是支持采样加载的,并且处理方式也是类似的,但是decodeStream方法稍微有点特殊。
12.2Android中的缓存策略
如何避免过多的流量消耗呢?
缓存
当程序第一次从网络加载图片后,就将其缓存到存储设备上,这样下次使用这张图片就不用再从网络上获取了,这样就为用户节省了流量。很多时候为了提高应用的用户体验,往往还会把图片在内存中再缓存一份,这样当应用打算从网络上请求一张图片时,程序会首先从内存中去获取,如果内存中没有那就从存储设备中去获取,如果存储设备中也没有,那就从网络上下载这张图片。因为从内存中加载图片比从存储设备中加载图片要快,所以这样既提高了程序的效率又为用户节约了不必要的流量开销。
LruCache是一个泛型类,它内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,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
public static DiskLruCache open(File directory,int appVersion,int valueCount,long maxSize)
网络拉取;
上面对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开启硬件加速。