Android Volley图片加载功能详解
Gituhb项目
Volley源码中文注释项目我已经上传到github,欢迎大家fork和start.
为什么写这篇博客
本来文章是维护在github上的,但是我在分析ImageLoader源码过程中与到了一个问题,希望大家能帮助解答.
Volley获取网络图片
本来想分析UniversalImageLoader的源码,但是发现Volley已经实现了网络图片的加载功能.其实,网络图片的加载也是分几个步骤:
1.获取网络图片的url.
2.判断该url对应的图片是否有本地缓存.
3.有本地缓存,直接使用本地缓存图片,通过异步回调给ImageView进行设置.
4.无本地缓存,就先从网络拉取,保存在本地后,再通过异步回调给ImageView进行设置.
我们通过Volley源码,看一下Volley是否是按照这个步骤实现网络图片加载的.
ImageRequest.java
按照Volley的架构,我们首先需要构造一个网络图片请求,Volley帮我们封装了ImageRequest类,我们来看一下它的具体实现:
/**网络图片请求类.*/
@SuppressWarnings("unused")
publicclassImageRequestextendsRequest<Bitmap>{
/**默认图片获取的超时时间(单位:毫秒)*/
publicstaticfinalintDEFAULT_IMAGE_REQUEST_MS=1000;
/**默认图片获取的重试次数.*/
publicstaticfinalintDEFAULT_IMAGE_MAX_RETRIES=2;
privatefinalResponse.Listener<Bitmap>mListener;
privatefinalBitmap.ConfigmDecodeConfig;
privatefinalintmMaxWidth;
privatefinalintmMaxHeight;
privateImageView.ScaleTypemScaleType;
/**Bitmap解析同步锁,保证同一时间只有一个Bitmap被load到内存进行解析,防止OOM.*/
privatestaticfinalObjectsDecodeLock=newObject();
/**
*构造一个网络图片请求.
*@paramurl图片的url地址.
*@paramlistener请求成功用户设置的回调接口.
*@parammaxWidth图片的最大宽度.
*@parammaxHeight图片的最大高度.
*@paramscaleType图片缩放类型.
*@paramdecodeConfig解析bitmap的配置.
*@paramerrorListener请求失败用户设置的回调接口.
*/
publicImageRequest(Stringurl,Response.Listener<Bitmap>listener,intmaxWidth,intmaxHeight,
ImageView.ScaleTypescaleType,Bitmap.ConfigdecodeConfig,
Response.ErrorListenererrorListener){
super(Method.GET,url,errorListener);
mListener=listener;
mDecodeConfig=decodeConfig;
mMaxWidth=maxWidth;
mMaxHeight=maxHeight;
mScaleType=scaleType;
}
/**设置网络图片请求的优先级.*/
@Override
publicPrioritygetPriority(){
returnPriority.LOW;
}
@Override
protectedResponse<Bitmap>parseNetworkResponse(NetworkResponseresponse){
synchronized(sDecodeLock){
try{
returndoParse(response);
}catch(OutOfMemoryErrore){
returnResponse.error(newVolleyError(e));
}
}
}
privateResponse<Bitmap>doParse(NetworkResponseresponse){
byte[]data=response.data;
BitmapFactory.OptionsdecodeOptions=newBitmapFactory.Options();
Bitmapbitmap;
if(mMaxWidth==0&&mMaxHeight==0){
decodeOptions.inPreferredConfig=mDecodeConfig;
bitmap=BitmapFactory.decodeByteArray(data,0,data.length,decodeOptions);
}else{
//获取网络图片的真实尺寸.
decodeOptions.inJustDecodeBounds=true;
BitmapFactory.decodeByteArray(data,0,data.length,decodeOptions);
intactualWidth=decodeOptions.outWidth;
intactualHeight=decodeOptions.outHeight;
intdesiredWidth=getResizedDimension(mMaxWidth,mMaxHeight,
actualWidth,actualHeight,mScaleType);
intdesireHeight=getResizedDimension(mMaxWidth,mMaxHeight,
actualWidth,actualHeight,mScaleType);
decodeOptions.inJustDecodeBounds=false;
decodeOptions.inSampleSize=
findBestSampleSize(actualWidth,actualHeight,desiredWidth,desireHeight);
BitmaptempBitmap=BitmapFactory.decodeByteArray(data,0,data.length,decodeOptions);
if(tempBitmap!=null&&(tempBitmap.getWidth()>desiredWidth||
tempBitmap.getHeight()>desireHeight)){
bitmap=Bitmap.createScaledBitmap(tempBitmap,desiredWidth,desireHeight,true);
tempBitmap.recycle();
}else{
bitmap=tempBitmap;
}
}
if(bitmap==null){
returnResponse.error(newVolleyError(response));
}else{
returnResponse.success(bitmap,HttpHeaderParser.parseCacheHeaders(response));
}
}
staticintfindBestSampleSize(
intactualWidth,intactualHeight,intdesiredWidth,intdesireHeight){
doublewr=(double)actualWidth/desiredWidth;
doublehr=(double)actualHeight/desireHeight;
doubleratio=Math.min(wr,hr);
floatn=1.0f;
while((n*2)<=ratio){
n*=2;
}
return(int)n;
}
/**根据ImageView的ScaleType设置图片的大小.*/
privatestaticintgetResizedDimension(intmaxPrimary,intmaxSecondary,intactualPrimary,
intactualSecondary,ImageView.ScaleTypescaleType){
//如果没有设置ImageView的最大值,则直接返回网络图片的真实大小.
if((maxPrimary==0)&&(maxSecondary==0)){
returnactualPrimary;
}
//如果ImageView的ScaleType为FIX_XY,则将其设置为图片最值.
if(scaleType==ImageView.ScaleType.FIT_XY){
if(maxPrimary==0){
returnactualPrimary;
}
returnmaxPrimary;
}
if(maxPrimary==0){
doubleratio=(double)maxSecondary/(double)actualSecondary;
return(int)(actualPrimary*ratio);
}
if(maxSecondary==0){
returnmaxPrimary;
}
doubleratio=(double)actualSecondary/(double)actualPrimary;
intresized=maxPrimary;
if(scaleType==ImageView.ScaleType.CENTER_CROP){
if((resized*ratio)<maxSecondary){
resized=(int)(maxSecondary/ratio);
}
returnresized;
}
if((resized*ratio)>maxSecondary){
resized=(int)(maxSecondary/ratio);
}
returnresized;
}
@Override
protectedvoiddeliverResponse(Bitmapresponse){
mListener.onResponse(response);
}
}
因为Volley本身框架已经实现了对网络请求的本地缓存,所以ImageRequest做的主要事情就是解析字节流为Bitmap,再解析过程中,通过静态变量保证每次只解析一个Bitmap防止OOM,使用ScaleType和用户设置的MaxWidth和MaxHeight来设置图片大小.
总体来说,ImageRequest的实现非常简单,这里不做过多的讲解.ImageRequest的缺陷在于:
1.需要用户进行过多的设置,包括图片的大小的最大值.
2.没有图片的内存缓存,因为Volley的缓存是基于Disk的缓存,有对象反序列化的过程.
ImageLoader.java
鉴于以上两个缺点,Volley又提供了一个更牛逼的ImageLoader类.其中,最关键的就是增加了内存缓存.
再讲解ImageLoader的源码之前,需要先介绍一下ImageLoader的使用方法.和之前的Request请求不同,ImageLoader并不是new出来直接扔给RequestQueue进行调度,它的使用方法大体分为4步:
•创建一个RequestQueue对象.
RequestQueuequeue=Volley.newRequestQueue(context);
•创建一个ImageLoader对象.
ImageLoader构造函数接收两个参数,第一个是RequestQueue对象,第二个是ImageCache对象(也就是内存缓存类,我们先不给出具体实现,讲解完ImageLoader源码之后,我会提供一个利用LRU算法的ImageCache实现类)
ImageLoaderimageLoader=newImageLoader(queue,newImageCache(){
@Override
publicvoidputBitmap(Stringurl,Bitmapbitmap){}
@Override
publicBitmapgetBitmap(Stringurl){returnnull;}
});
•获取一个ImageListener对象.
ImageListenerlistener=ImageLoader.getImageListener(imageView,R.drawable.default_imgage,R.drawable.failed_image);
•调用ImageLoader的get方法加载网络图片.
imageLoader.get(mImageUrl,listener,maxWidth,maxHeight,scaleType);
有了ImageLoader的使用方法,我们结合使用方法来看一下ImageLoader的源码:
@SuppressWarnings({"unused","StringBufferReplaceableByString"})
publicclassImageLoader{
/**
*关联用来调用ImageLoader的RequestQueue.
*/
privatefinalRequestQueuemRequestQueue;
/**图片内存缓存接口实现类.*/
privatefinalImageCachemCache;
/**存储同一时间执行的相同CacheKey的BatchedImageRequest集合.*/
privatefinalHashMap<String,BatchedImageRequest>mInFlightRequests=
newHashMap<String,BatchedImageRequest>();
privatefinalHashMap<String,BatchedImageRequest>mBatchedResponses=
newHashMap<String,BatchedImageRequest>();
/**获取主线程的Handler.*/
privatefinalHandlermHandler=newHandler(Looper.getMainLooper());
privateRunnablemRunnable;
/**定义图片K1缓存接口,即将图片的内存缓存工作交给用户来实现.*/
publicinterfaceImageCache{
BitmapgetBitmap(Stringurl);
voidputBitmap(Stringurl,Bitmapbitmap);
}
/**构造一个ImageLoader.*/
publicImageLoader(RequestQueuequeue,ImageCacheimageCache){
mRequestQueue=queue;
mCache=imageCache;
}
/**构造网络图片请求成功和失败的回调接口.*/
publicstaticImageListenergetImageListener(finalImageViewview,finalintdefaultImageResId,
finalinterrorImageResId){
returnnewImageListener(){
@Override
publicvoidonResponse(ImageContainerresponse,booleanisImmediate){
if(response.getBitmap()!=null){
view.setImageBitmap(response.getBitmap());
}elseif(defaultImageResId!=0){
view.setImageResource(defaultImageResId);
}
}
@Override
publicvoidonErrorResponse(VolleyErrorerror){
if(errorImageResId!=0){
view.setImageResource(errorImageResId);
}
}
};
}
publicImageContainerget(StringrequestUrl,ImageListenerimageListener,
intmaxWidth,intmaxHeight,ScaleTypescaleType){
//判断当前方法是否在UI线程中执行.如果不是,则抛出异常.
throwIfNotOnMainThread();
finalStringcacheKey=getCacheKey(requestUrl,maxWidth,maxHeight,scaleType);
//从L1级缓存中根据key获取对应的Bitmap.
BitmapcacheBitmap=mCache.getBitmap(cacheKey);
if(cacheBitmap!=null){
//L1缓存命中,通过缓存命中的Bitmap构造ImageContainer,并调用imageListener的响应成功接口.
ImageContainercontainer=newImageContainer(cacheBitmap,requestUrl,null,null);
//注意:因为目前是在UI线程中,因此这里是调用onResponse方法,并非回调.
imageListener.onResponse(container,true);
returncontainer;
}
ImageContainerimageContainer=
newImageContainer(null,requestUrl,cacheKey,imageListener);
//L1缓存命中失败,则先需要对ImageView设置默认图片.然后通过子线程拉取网络图片,进行显示.
imageListener.onResponse(imageContainer,true);
//检查cacheKey对应的ImageRequest请求是否正在运行.
BatchedImageRequestrequest=mInFlightRequests.get(cacheKey);
if(request!=null){
//相同的ImageRequest正在运行,不需要同时运行相同的ImageRequest.
//只需要将其对应的ImageContainer加入到BatchedImageRequest的mContainers集合中.
//当正在执行的ImageRequest结束后,会查看当前有多少正在阻塞的ImageRequest,
//然后对其mContainers集合进行回调.
request.addContainer(imageContainer);
returnimageContainer;
}
//L1缓存没命中,还是需要构造ImageRequest,通过RequestQueue的调度来获取网络图片
//获取方法可能是:L2缓存(ps:Disk缓存)或者HTTP网络请求.
Request<Bitmap>newRequest=
makeImageRequest(requestUrl,maxWidth,maxHeight,scaleType,cacheKey);
mRequestQueue.add(newRequest);
mInFlightRequests.put(cacheKey,newBatchedImageRequest(newRequest,imageContainer));
returnimageContainer;
}
/**构造L1缓存的key值.*/
privateStringgetCacheKey(Stringurl,intmaxWidth,intmaxHeight,ScaleTypescaleType){
returnnewStringBuilder(url.length()+12).append("#W").append(maxWidth)
.append("#H").append(maxHeight).append("#S").append(scaleType.ordinal()).append(url)
.toString();
}
publicbooleanisCached(StringrequestUrl,intmaxWidth,intmaxHeight){
returnisCached(requestUrl,maxWidth,maxHeight,ScaleType.CENTER_INSIDE);
}
privatebooleanisCached(StringrequestUrl,intmaxWidth,intmaxHeight,ScaleTypescaleType){
throwIfNotOnMainThread();
StringcacheKey=getCacheKey(requestUrl,maxWidth,maxHeight,scaleType);
returnmCache.getBitmap(cacheKey)!=null;
}
/**当L1缓存没有命中时,构造ImageRequest,通过ImageRequest和RequestQueue获取图片.*/
protectedRequest<Bitmap>makeImageRequest(finalStringrequestUrl,intmaxWidth,intmaxHeight,
ScaleTypescaleType,finalStringcacheKey){
returnnewImageRequest(requestUrl,newResponse.Listener<Bitmap>(){
@Override
publicvoidonResponse(Bitmapresponse){
onGetImageSuccess(cacheKey,response);
}
},maxWidth,maxHeight,scaleType,Bitmap.Config.RGB_565,newResponse.ErrorListener(){
@Override
publicvoidonErrorResponse(VolleyErrorerror){
onGetImageError(cacheKey,error);
}
});
}
/**图片请求失败回调.运行在UI线程中.*/
privatevoidonGetImageError(StringcacheKey,VolleyErrorerror){
BatchedImageRequestrequest=mInFlightRequests.remove(cacheKey);
if(request!=null){
request.setError(error);
batchResponse(cacheKey,request);
}
}
/**图片请求成功回调.运行在UI线程中.*/
protectedvoidonGetImageSuccess(StringcacheKey,Bitmapresponse){
//增加L1缓存的键值对.
mCache.putBitmap(cacheKey,response);
//同一时间内最初的ImageRequest执行成功后,回调这段时间阻塞的相同ImageRequest对应的成功回调接口.
BatchedImageRequestrequest=mInFlightRequests.remove(cacheKey);
if(request!=null){
request.mResponseBitmap=response;
//将阻塞的ImageRequest进行结果分发.
batchResponse(cacheKey,request);
}
}
privatevoidbatchResponse(StringcacheKey,BatchedImageRequestrequest){
mBatchedResponses.put(cacheKey,request);
if(mRunnable==null){
mRunnable=newRunnable(){
@Override
publicvoidrun(){
for(BatchedImageRequestbir:mBatchedResponses.values()){
for(ImageContainercontainer:bir.mContainers){
if(container.mListener==null){
continue;
}
if(bir.getError()==null){
container.mBitmap=bir.mResponseBitmap;
container.mListener.onResponse(container,false);
}else{
container.mListener.onErrorResponse(bir.getError());
}
}
}
mBatchedResponses.clear();
mRunnable=null;
}
};
//Posttherunnable
mHandler.postDelayed(mRunnable,100);
}
}
privatevoidthrowIfNotOnMainThread(){
if(Looper.myLooper()!=Looper.getMainLooper()){
thrownewIllegalStateException("ImageLoadermustbeinvokedfromthemainthread.");
}
}
/**抽象出请求成功和失败的回调接口.默认可以使用Volley提供的ImageListener.*/
publicinterfaceImageListenerextendsResponse.ErrorListener{
voidonResponse(ImageContainerresponse,booleanisImmediate);
}
/**网络图片请求的承载对象.*/
publicclassImageContainer{
/**ImageView需要加载的Bitmap.*/
privateBitmapmBitmap;
/**L1缓存的key*/
privatefinalStringmCacheKey;
/**ImageRequest请求的url.*/
privatefinalStringmRequestUrl;
/**图片请求成功或失败的回调接口类.*/
privatefinalImageListenermListener;
publicImageContainer(Bitmapbitmap,StringrequestUrl,StringcacheKey,
ImageListenerlistener){
mBitmap=bitmap;
mRequestUrl=requestUrl;
mCacheKey=cacheKey;
mListener=listener;
}
publicvoidcancelRequest(){
if(mListener==null){
return;
}
BatchedImageRequestrequest=mInFlightRequests.get(mCacheKey);
if(request!=null){
booleancanceled=request.removeContainerAndCancelIfNecessary(this);
if(canceled){
mInFlightRequests.remove(mCacheKey);
}
}else{
request=mBatchedResponses.get(mCacheKey);
if(request!=null){
request.removeContainerAndCancelIfNecessary(this);
if(request.mContainers.size()==0){
mBatchedResponses.remove(mCacheKey);
}
}
}
}
publicBitmapgetBitmap(){
returnmBitmap;
}
publicStringgetRequestUrl(){
returnmRequestUrl;
}
}
/**
*CacheKey相同的ImageRequest请求抽象类.
*判定两个ImageRequest相同包括:
*1.url相同.
*2.maxWidth和maxHeight相同.
*3.显示的scaleType相同.
*同一时间可能有多个相同CacheKey的ImageRequest请求,由于需要返回的Bitmap都一样,所以用BatchedImageRequest
*来实现该功能.同一时间相同CacheKey的ImageRequest只能有一个.
*为什么不使用RequestQueue的mWaitingRequestQueue来实现该功能?
*答:是因为仅靠URL是没法判断两个ImageRequest相等的.
*/
privateclassBatchedImageRequest{
/**对应的ImageRequest请求.*/
privatefinalRequest<?>mRequest;
/**请求结果的Bitmap对象.*/
privateBitmapmResponseBitmap;
/**ImageRequest的错误.*/
privateVolleyErrormError;
/**所有相同ImageRequest请求结果的封装集合.*/
privatefinalLinkedList<ImageContainer>mContainers=newLinkedList<ImageContainer>();
publicBatchedImageRequest(Request<?>request,ImageContainercontainer){
mRequest=request;
mContainers.add(container);
}
publicVolleyErrorgetError(){
returnmError;
}
publicvoidsetError(VolleyErrorerror){
mError=error;
}
publicvoidaddContainer(ImageContainercontainer){
mContainers.add(container);
}
publicbooleanremoveContainerAndCancelIfNecessary(ImageContainercontainer){
mContainers.remove(container);
if(mContainers.size()==0){
mRequest.cancel();
returntrue;
}
returnfalse;
}
}
}
重大疑问
个人对Imageloader的源码有两个重大疑问?
•batchResponse方法的实现.
我很奇怪,为什么ImageLoader类里面要有一个HashMap来保存BatchedImageRequest集合呢?
privatefinalHashMap<String,BatchedImageRequest>mBatchedResponses= newHashMap<String,BatchedImageRequest>();
毕竟batchResponse是在特定的ImageRequest执行成功的回调中被调用的,调用代码如下:
protectedvoidonGetImageSuccess(StringcacheKey,Bitmapresponse){
//增加L1缓存的键值对.
mCache.putBitmap(cacheKey,response);
//同一时间内最初的ImageRequest执行成功后,回调这段时间阻塞的相同ImageRequest对应的成功回调接口.
BatchedImageRequestrequest=mInFlightRequests.remove(cacheKey);
if(request!=null){
request.mResponseBitmap=response;
//将阻塞的ImageRequest进行结果分发.
batchResponse(cacheKey,request);
}
}
从上述代码可以看出,ImageRequest请求成功后,已经从mInFlightRequests中获取了对应的BatchedImageRequest对象.而同一时间被阻塞的相同的ImageRequest对应的ImageContainer都在BatchedImageRequest的mContainers集合中.
那我认为,batchResponse方法只需要遍历对应BatchedImageRequest的mContainers集合即可.
但是,ImageLoader源码中,我认为多余的构造了一个HashMap对象mBatchedResponses来保存BatchedImageRequest集合,然后在batchResponse方法中又对集合进行两层for循环各种遍历,实在是非常诡异,求指导.
诡异代码如下:
privatevoidbatchResponse(StringcacheKey,BatchedImageRequestrequest){
mBatchedResponses.put(cacheKey,request);
if(mRunnable==null){
mRunnable=newRunnable(){
@Override
publicvoidrun(){
for(BatchedImageRequestbir:mBatchedResponses.values()){
for(ImageContainercontainer:bir.mContainers){
if(container.mListener==null){
continue;
}
if(bir.getError()==null){
container.mBitmap=bir.mResponseBitmap;
container.mListener.onResponse(container,false);
}else{
container.mListener.onErrorResponse(bir.getError());
}
}
}
mBatchedResponses.clear();
mRunnable=null;
}
};
//Posttherunnable
mHandler.postDelayed(mRunnable,100);
}
}
我认为的代码实现应该是:
privatevoidbatchResponse(StringcacheKey,BatchedImageRequestrequest){
if(mRunnable==null){
mRunnable=newRunnable(){
@Override
publicvoidrun(){
for(ImageContainercontainer:request.mContainers){
if(container.mListener==null){
continue;
}
if(request.getError()==null){
container.mBitmap=request.mResponseBitmap;
container.mListener.onResponse(container,false);
}else{
container.mListener.onErrorResponse(request.getError());
}
}
mRunnable=null;
}
};
//Posttherunnable
mHandler.postDelayed(mRunnable,100);
}
}
•使用ImageLoader默认提供的ImageListener,我认为存在一个缺陷,即图片闪现问题.当为ListView的item设置图片时,需要增加TAG判断.因为对应的ImageView可能已经被回收利用了.
自定义L1缓存类
首先说明一下,所谓的L1和L2缓存分别指的是内存缓存和硬盘缓存.
实现L1缓存,我们可以使用Android提供的Lru缓存类,示例代码如下:
importandroid.graphics.Bitmap;
importandroid.support.v4.util.LruCache;
/**Lru算法的L1缓存实现类.*/
@SuppressWarnings("unused")
publicclassImageLruCacheimplementsImageLoader.ImageCache{
privateLruCache<String,Bitmap>mLruCache;
publicImageLruCache(){
this((int)Runtime.getRuntime().maxMemory()/8);
}
publicImageLruCache(finalintcacheSize){
createLruCache(cacheSize);
}
privatevoidcreateLruCache(finalintcacheSize){
mLruCache=newLruCache<String,Bitmap>(cacheSize){
@Override
protectedintsizeOf(Stringkey,Bitmapvalue){
returnvalue.getRowBytes()*value.getHeight();
}
};
}
@Override
publicBitmapgetBitmap(Stringurl){
returnmLruCache.get(url);
}
@Override
publicvoidputBitmap(Stringurl,Bitmapbitmap){
mLruCache.put(url,bitmap);
}
}
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。