Android 中的缓存策略
缓存策略在andriod开发的过程中有着广泛的使用场景,尤其是在图片加载的场景下,缓存策略变得尤为重要.通常很多时候,我们需要浏览大量的图片,如果是在PC端,这种根本不算是问题,直接加载就好了,但是在移动端,无论是andriod或者是IOS流量对于用户来说都是非常宝贵的资源,由于流量是收费的,因此在应用开发的过程中,我们应该尽可能避免过多的消耗用户的流量.
如何避免过多的消耗流量呢?这就是我们要介绍的主题:缓存.当程序第一次从网络加载好图片后,我们就把图片缓存到储存的设备上,这样下次使用这张图片就不用重新从网络上获取,这样就为用户节省了流量.很多时候为了提高用户的体验,我们往往还会把图片在内存中缓存一份,这样,应用在请求一张图片的时候,首先去内存中获取,如果内存中没有,那就去存储的设备中去取,如果存储的设备中也没有的时候,我们才会去从网络上获取.这样的缓存策略不仅仅适用于图片,还适用于一些其他的文件类型.
说到缓存策略,这里并没有一个统一的标准,缓存的策略主要包括缓存的添加,获取和删除等.添加和获取这个都比较好理解,但是删除又是怎么一回事呢?其实,我们细想一下这个也并不难理解.因为无论是设备存储或是内存的资源都是有限的,尤其是内存,它的资源更是稀缺,所以有时候,我们不可能分配大多的内存去存储图片.当分配的内存满了之后,我们想要存储其他的图片,此时就需要用到删除,当然也不是随便删除的,我们通常用的算法是最近最少算法(LRU),LRU是近期最少使用的算法,根据算法的思想,当缓存满的时候,会优先淘汰掉那些近期最少使用的缓存对象,采用LRU算法的缓存有两种:LruCache和DiskLruCache,前者实现内存缓存,后者充当了存储设备的缓存.通过这二者完美的结合,就可以很方便的实现一个具有很高实用价值的ImgageLoader.
ImageLoader的实现
一般来说,一个优秀的ImageLoader应该具有如下的功能:
- 图片的同步加载
- 图片的异步加载
- 图片压缩
- 内存缓存
- 磁盘缓存
- 网络拉取
图片的同步加载是指能够以同步的方式向调用者所提供所加载的图片,这个图片可能是从内存中读取到的,也可能是从磁盘缓存中读取的,还可能是从网络拉取的.图片的异步加载是一个非常有用的功能,很多时候调用者不想在单独的线程中以同步的方式获取图片,这时候ImageLoader内部需要自己在线程中加载图片,并将图片设置给所需的ImageView.图片压缩的作用更加毋庸置疑了,这是降低OOM概率的有效手段,ImageLoader必须合适的处理图片的压缩问题.
内存缓存和磁盘缓存是ImageLoader的核心,也是ImageLoader的意义所在,通过这两级的缓存极大的提高了程序的效率并且有效的降低了对用户所造成的流量消耗,只有这两级缓存都不可用的时候才会到网络上拉取图片.
除此之外,ImageLoader还需要处理一些特殊的情况,比如ListView或者Gridview中,View的复用既是他们的优点也是他们的缺点,优点想必大家都知道了,但是缺点可能还不是特别清楚,有时候listView或者gridview出现图片错位的情况的时候就是由于View的复用引起的,因此,ImageLoader需要正确的处理这样的特殊情况.
图片压缩功能的实现
图片压缩在Bitmap加载的时候我们已经做了相应的介绍,为了有一个良好的设计风格,这里单独抽出了一个类似于用于完成图片压缩的功能,这个类叫做ImageResizer,它的实现如下:
public class ImageResizer{
private static final String TAG="ImageResizer";
public ImageResizer(){
}
public Bitmap decodeSampledBitmapFromResource(Resource res,int resId,int reqWdith,int reqHeight){
final BitmapFactory.Options options=new BitmapFactory.options();
Options.inJustDecodeBounds=true;
BitmapFactory.decodeResource(res,resId,options);
options.inSampleSize=calculateImSampleSize(options,reqWidth,reqHight);
options.inJustDecodeBounds=false;
return BitmapFactory.decodeResource(res,resId.options);
}
public Bitmap decodeSampledBitmapFromDescriptor(FileDescriptor fd,int reqWidth,int feqHeight){
final BitmapFactory.Options options=new BitmapFactory.Options();
options.inJustDecodeBounds=true;
BitmapFactory.decodeFileDescriptor(fd,null,options);
options.inSampleSize=calculateInSampleSize(options,reqWidth,reqHight);
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;
final 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;
}
}
内存缓存和磁盘缓存的实现
这里选择LruCache和DiskLruCache来分别完成内存缓存和磁盘缓存的工作.在ImageLoader初始化的时候,会创建LruCache和DiskLruCache,如下所示:
private LruCache<String Bitmap> mMemoryCache;
private DiskLruCache mDiskLruCache;
private ImageLoader(Context context){
mContext=context.getApplicationContext();
int maxMemory=(int)(Runtime.getRuntime().maxMemory()/1024);
int cacheSize=maxMemory/8;
mMemoeyCache=new LruCache<String,Bitmap>(cacheSize){
@override
protected int sizeOf(String key,Bitmap bitmap){
return bitmap.getRowBytes()*bitmap.getHeight()/1024;
}
};
File diskCacheDir=getDiskCacheDir(mContext,"Bitmap");
if(!diskCacheDir.exists){
diskCacheDir.mkdirs();
}
if(getUsableSpace(diskCacheDir)>DISK_CACHE_SIZE){
try{
mDiskLruCache=DiskLruCache.open(diskCacheDir,1,1,DISK_CACHE_SIZE);
mIsDiskLruCacheCreated=true;
}catch(IOException e){
e.printStackTrace();
}
}
}
内存缓存和磁盘缓存创建完毕之后,还需要提供方法来完成缓存的添加和获取功能,缓存的的添加和读取的过程比较简单如下:
private void addBitmapToMemoryCache(String key,Bitmap bitmap){
if(getBitmapFromMemCache(key)==null){
mMemoryCache.put(key,bitmap);
}
private Bitmap getBitmapFrommMemoryCache(String key){
return mMemoryCache.get(key);
}
}
磁盘的读取就会比较复杂一些,磁盘缓存的添加需要Editor来完成,Editor提供了commit和abort方法来提交和撤销对文件系统的写操作.而磁盘的读取则通过Snapshot来完成,通过Snapshot可以得到磁盘缓存对象对应的FileInputStream,但是FileInputStream无法完成便捷的压缩,所以还要通过FileDescriptor来加载压缩的图片,最后将加载的图片缓存到内存中.
同步加载和异步加载接口的设计
同步加载接口需要外部在线程中调用,这是因为同步加载很可能比较耗时.
public Bitmap loadBitmap(String uri,int reqWidth,int reqHeight){
Bitmap bitmap=loadBitmapFrommMemoryCache(uri);
if(bitmap!=null){
return bitmap;
}
try{
bitmap=loadBitmapFromDiskCache(uri,reqWidth,reqHeight);
if(bitmap!=null){
return bitmap;
}
bitmap=loadBitmapFromHttp(uri,reqWidth,reqHeight);
}catch(IOException e){
e.printStackTrace();
}
if(bitmap==null && !mIsDiskLruCacheCreated){
bitmap=downloadBitmapFromUrl(uri);
}
return bitmap;
}
从loadBitmap的实现可以看的出,它的工作原理遵循如下几部:
- 首先尝试从内存缓存中获取图片
- 接着尝试从磁盘缓存中获取图片
- 最后从网络拉取图片
- 这个方法不能在主线程中执行,否者就会抛出异常
注意:判断是否为主线程的方法,就是通过检查当前线程的Looper是否为主线程的的looper从而判断当前线程是否为主线程,如果不是主线程就直接抛出异常终止程序.
if(Looper.myLooper()==Looper.getMainLooper){
throw new RuntimeException("can not visit network from UI Thread.");
}
接着我们看一下异步加载接口的设计:
public void bindBitmap(final String uri,final ImageView,fianl int reqWidth,final int reqHeight){
imageView.setTag(TAG_KEY_URI,uri);
Bitmap bitmap=loadBitmapFrommMemoryCache(uri);
if(bitmap!=null){
imageView.setImageBitmap(bitmap);
return;
}
Runnable loadBitmapTask=new Runnable(){
@override
public void run(){
Bitmap bitamp=loadBitmap(uri,reqWidth,reqHeight);
if(bitmap!=null){
LoaderResult result=new LoadResult(imageView,reqHeight);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT,result);
sendToTarget();
}
}
};
THREAD_POOLEXECUTOR.execute(loadBitmapTask);
}
从上面的代码我们可以大概看的出来,它的工作流程,bindBitmap方法尝试从缓存中读取图片,如果读取成功就会直接返回结果,如果获取不成功,就会在线程池调取loadBitmap的方法,当图片加载成功后再将图片,图片地址以及需要绑定的imageView封装成一个LoadResult的对象,然后通过mMainHander向主线程发送消息,这样就可以在主线程给imageView设置图片了.