关于android图片缓存开源框架,被程序猿们津津乐道的应该是,Volley , Universal-Image-Loader 和 picasso。关于他们大家问的最多的问题是,到底Volley和UIL那个好?其实我也不知道哪个好,但是我可以帮大家分析分析,大家来评判一下:
UIL之前我已近有文章详细分析过了,今天就我想带着对着两个框架的比较来从源码细节分析一下他们的差别。
首先说说他们之间关于缓存部分(Cache),用过Volley的同行应该都知道,它只是提供了接口,ImageCache 和 Cache 让大家自己去实现,接口已经定义好了,至于大家想用内存缓存,还是磁盘缓存,看各自应用场景了。而相比于UIL提供了内存缓存,还同时实现了各种内存缓存的策略,另外还实现了磁盘缓存以及磁盘缓存的各种策略。其实这部分代码并不复杂,我就算实现在这个框架里面了,也不会和UIL有多大差异吧。
接下来就按照Volley加载图片ImageRequest,ImageLoader,NetworkImageView三种方式入手,分析Volley加载图片的控制流程和UIL的Core部分有哪些差别。
Volley加载图片的第一种方式:ImageRequest
RequestQueue mQueue = Volley.newRequestQueue(context);
ImageRequestimageRequest = new ImageRequest(
"图片下载地址",
new Response.Listener<Bitmap>(){
@Override
public void onResponse(Bitmapresponse) {
imageView.setImageBitmap(response);
}
}, 0, 0, Config.RGB_565, newResponse.ErrorListener() {
@Override
public void onErrorResponse(VolleyErrorerror) {
imageView.setImageResource(R.drawable.default_image);
}
});
mQueue.add(imageRequest);
先简单分析一下这两个类。
ImageRequest:就是一个网络请求数据类,包含图片下载地址,需要下载图片的宽高,格式,已近下载成功的回调等等。
RequestQueue:这个是个核心类,下面是各个成员变量的作用。
Cache mCache; 缓存自己实现.
ResponseDelivery mDelivery; 分发请求结果。
mWaitingRequests 等待队列 重复请求处理
mCurrentRequests 当前所有请求队列,同意处理,比如取消。
mCacheQueue 需要获取图片的请求请求队列
mNetworkQueue 需要从网络获取图片的请求队列
NetworkDispatcher 网络请求处理线程,从mNetworkQueue队列中读取从网络获取图片请求。
CacheDispatcher 缓存处理线程, 从mCacheQueue 队列中获取数据, 从自定义缓存mCache中获取对应请求的数据,如果数据存在则调用mDelivery分发请求结果,如没有加入到mNetworkQueue。
然后为我们分析add函数做一下简单的铺垫。在RequestQueue构造函数中
public RequestQueue(Cache cache, Networknetwork, int threadPoolSize,
ResponseDelivery delivery) {
mCache = cache;
mNetwork = network;
mDispatchers = new NetworkDispatcher[threadPoolSize];
mDelivery = delivery;
}
我们会为缓存mCache, 网络请求mNetwork, mDispatchers图片处理线程数组,处理结果分发mDelivery。几个对象初始化。以及定义处理加载网络请求线程个数threadPoolSize
再看RequestQueue.start函数。
public void start() {
stop(); // Make sure anycurrently running dispatchers are stopped.
// Create the cache dispatcher and start it.
mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue,mCache, mDelivery);
mCacheDispatcher.start();
// Create network dispatchers (and corresponding threads) up to the poolsize.
for (int i = 0; i < mDispatchers.length; i++) {
NetworkDispatcher networkDispatcher = newNetworkDispatcher(mNetworkQueue, mNetwork,
mCache, mDelivery);
mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}
首先停止当前的线程,然后创建CacheDispatcher 线程并把mCacheQueue, mNetworkQueue, mCache, mDelivery传进入,再启动threadPoolSize个NetworkDispatcher线程启动了同样也把mNetworkQueue, mNetwork, mCache, mDelivery传进去。一切算是准备就绪了。先打住,我们回忆一下UIL中是怎样处理的?UIL中是我们自己配置的线程池 taskExecutorForCachedImages 和taskExecutor 也就是说,UIL中是用线程池的方式,和Volley就是开五个常驻线程。
铺垫准备就绪了,看看mQueue.add(imageRequest); 函数。
public<T> Request<T> add(Request<T> request) {
request.setRequestQueue(this);
synchronized (mCurrentRequests) {
mCurrentRequests.add(request);
}
request.setSequence(getSequenceNumber());
request.addMarker("add-to-queue");
if(!request.shouldCache()) {
mNetworkQueue.add(request);
return request;
}
synchronized (mWaitingRequests) {
String cacheKey =request.getCacheKey();
if (mWaitingRequests.containsKey(cacheKey)) {
Queue<Request<?>> stagedRequests =mWaitingRequests.get(cacheKey);
if (stagedRequests == null) {
stagedRequests = newLinkedList<Request<?>>();
}
stagedRequests.add(request);
mWaitingRequests.put(cacheKey, stagedRequests);
} else {
mWaitingRequests.put(cacheKey, null);
mCacheQueue.add(request);
}
return request;
}
}
这个函数比较简单主要工作如下:
这里把RequestQueue保存到request里面,同时把request加入到mCurrentRequests中。然后判断这个请求是否需要缓存,如果不需要直接加入到mNetworkQueue让NetworkDispatcher去处理,然后返回。如果需要缓存获得缓存请求key,判断当前mWaitingRequests等待队列里面是否已经存在这个请求,如果存在就保存到mWaitingRequests对应的key的请求队列中。如果不存在就往mWaitingRequests中加入KEY并对应null值,表示没有相同key的请求。最后把request加入到mCacheQueue中。这样add函数就完成了。mWaitingRequests的作用相信大家看出来了,就是保存重复请求的。
CacheDispatcher线程分析:
前面说过mCacheQueue队列中的数据是由CacheDispatcher线程来读取并处理的。现在看看CacheDispatcher的run函数。
这个函数比较长分段阅读:
final Request<?> request = mCacheQueue.take();
request.addMarker("cache-queue-take");
if (request.isCanceled()) {
request.finish("cache-discard-canceled");
continue;
}
这里是从mCacheQueue取出请求然后判断是否此请求已经被取消,如果取消则继续去下一条。
Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry == null) {
request.addMarker("cache-miss");
mNetworkQueue.put(request);
continue;
}
if (entry.isExpired()) {
request.addMarker("cache-hit-expired");
request.setCacheEntry(entry);
mNetworkQueue.put(request);
continue;
}
上面主要检查是否该请求已经缓存,或者是否过期,如果都不成立就直接把请求加入到mNetworkQueue中去取下一条。
request.addMarker("cache-hit");
Response<?> response =request.parseNetworkResponse(
newNetworkResponse(entry.data, entry.responseHeaders));
request.addMarker("cache-hit-parsed");
if (!entry.refreshNeeded()) {
mDelivery.postResponse(request, response);
} else {
request.addMarker("cache-hit-refresh-needed");
request.setCacheEntry(entry);
response.intermediate =true;
mDelivery.postResponse(request, response, new Runnable() {
@Override
public void run() {
try {
mNetworkQueue.put(request);
} catch(InterruptedException e) {
}
}
});
}
} catch (InterruptedException e) {
if (mQuit) {
return;
}
continue;
}
如果在缓存中且没有过期,我们构造一个请求结果,同时判断该请求是否需要刷新,如果不需要直接mDelivery.postResponse(request, response);分发请求结果,如果需要则,在分发请求结果的同时把这个请求加入到mNetworkQueue中重新再请求一次,拿到新的图片再刷新。
接下来看看mDelivery.postResponse做了啥?
往主线程消息队列里面发送了一个ResponseDeliveryRunnablerunnable对象。
public void run() {
if (mRequest.isCanceled()) {
mRequest.finish("canceled-at-delivery");
return;
}
调用mRequest.finish,而在mRequest.finish中又调用了mRequestQueue.finish(this);我们还记得在Add函数的第一行就把mRequestQueue保存到了request中。看看mRequestQueue.finish
void finish(Request<?> request) {
synchronized (mCurrentRequests) {
mCurrentRequests.remove(request);
}
if (request.shouldCache()) {
synchronized (mWaitingRequests) {
String cacheKey =request.getCacheKey();
Queue<Request<?>>waitingRequests = mWaitingRequests.remove(cacheKey);
if (waitingRequests != null) {
mCacheQueue.addAll(waitingRequests);
}
}
}
把当前request从mCurrentRequests移除,也就是说mCurrentRequests保存了当前所有未完成的请求。继续往下看,request.shouldCache()判断当前请求是否需要缓存,如果需要,这从waitingRequests找出所有正在等待中的同样key请求。全部加入到缓存mCacheQueue队列中。交给CacheDispatcher处理。到这里我想我们应该打住了,想想UIL是怎么处理同样的Uri请求的。在我的Android-Universal-Image-Loader简单分析-Core部分文章中有说道,每个uri创建一个 ReentrantLock对象。同时保存在ImageLoaderEngine 的uriLocks 中,之所以每一个uri对应一个,是为了防止多个imageView同时都从网络加载同一张图片的情况。它可以让其中一个task从网络下载图片,让后让后来的task等待直到第一个task下载完成
然后别的task就可以从内存中区获取该图片的bigmap.也就是说会阻塞线程池工作线程。而Volley不会。
if (mResponse.isSuccess()) {
mRequest.deliverResponse(mResponse.result);
} else {
mRequest.deliverError(mResponse.error);
}
这几行代码就是调用request分发处理结果,然后调用Response.Listener或者Response.ErrorListener做具体的请求。
if (mRunnable != null) {
mRunnable.run();
}
}
这几行代码就是执行前面传进来的Runnable对象run函数比如说前面的如果这个请求需要刷新就把当前请求加入到mNetworkQueue中。
NetworkDispatcher线程分析:
在CacheDispatcher线程中,如果该请求没有缓存,或者该请求过期,或者该请求需要刷新会把该请求加入到mNetworkQueue中,而mNetworkQueue是由NetworkDispatcher来读取数据并处理的。下面看看NetworkDispatcher.run函数。
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
while (true) {
long startTimeMs =SystemClock.elapsedRealtime();
Request<?> request;
request = mQueue.take();
try {
request.addMarker("network-queue-take");
if (request.isCanceled()) {
request.finish("network-discard-cancelled");
continue;
}
addTrafficStatsTag(request);
NetworkResponse networkResponse= mNetwork.performRequest(request);
request.addMarker("network-http-complete");
if (networkResponse.notModified&& request.hasHadResponseDelivered()) {
request.finish("not-modified");
continue;
}
Response<?> response =request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");
if (request.shouldCache()&& response.cacheEntry != null) {
mCache.put(request.getCacheKey(), response.cacheEntry);
request.addMarker("network-cache-written");
}
request.markDelivered();
mDelivery.postResponse(request,response);
} catch (VolleyError volleyError) {
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() -startTimeMs);
parseAndDeliverNetworkError(request, volleyError);
} catch (Exception e) {
VolleyLog.e(e, "Unhandledexception %s", e.toString());
VolleyError volleyError = newVolleyError(e);
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime()- startTimeMs);
mDelivery.postError(request,volleyError);
}
}
}
这个函数主要先是从mNetworkQueue取出需要发起网络请求的request,然后判断是否已经被用户取消,如果取消就结束去取下一个请求。如果未取消,这mNetwork执行这个网络请求,拿到请求后调用request.parseNetworkResponse解析这个请求,这里主要是根据之前我们对图片一些宽高,格式或者imageview大小或解析出合适的bitmap。解析完成之后判断是否需要缓存,如果需要就把他加入到mCache缓存中。然后调用mDelivery.postResponse完成对此次请求的分发工作。前面在CacheDispatcher中已经详细分析过了。
Volley加载图片的第二种方式:ImageLoader
ImageLoaderimageLoader = new ImageLoader(mQueue, new ImageCache() {
@Override
public void putBitmap(String url, Bitmapbitmap) {
}
@Override
public Bitmap getBitmap(String url) {
return null;
}
});
ImageListenerlistener = ImageLoader.getImageListener(imageView,
R.drawable.default_image,R.drawable.failed_image);
imageLoader.get("图片加载地址",listener, 200, 200);
在ImageLoader构造函数中,有我们熟悉的mQueue另外的就是ImageCache 这个是图片缓存,需要我们自己实现自己的缓存方式。ImageListener 为imageView不同状态设置不同图片加载完成时做收尾工作。现在看看imageLoader.get函数
public ImageContainer get(StringrequestUrl, ImageListener imageListener,
int maxWidth, int maxHeight) {
throwIfNotOnMainThread();
final String cacheKey =getCacheKey(requestUrl, maxWidth, maxHeight);
Bitmap cachedBitmap =mCache.getBitmap(cacheKey);
if (cachedBitmap != null) {
ImageContainer container = newImageContainer(cachedBitmap, requestUrl, null, null);
imageListener.onResponse(container,true);
return container;
}
上面的代码主要是检测是否在主线程中,然后判断当前请求是否已经在缓存中,如果在这创建一个ImageContainer 然后调用imageListener.onResponse把结果返回,为ImageView设置上对应的bitmap数据。
ImageContainer imageContainer =
new ImageContainer(null,requestUrl, cacheKey, imageListener);
imageListener.onResponse(imageContainer, true);
上面代码主要是新建一个ImageContainer对象,同时调用imageListener.onResponse为ImageView显示加载中的图片。
BatchedImageRequest request =mInFlightRequests.get(cacheKey);
if (request != null) {
request.addContainer(imageContainer);
return imageContainer;
}
Request<Bitmap> newRequest =makeImageRequest(requestUrl, maxWidth, maxHeight, cacheKey);
上面代码主要是判断当前请求是否在mInFlightRequests存在,如果存在这加入到对应key的BatchedImageRequest批处理请求中。其实mInFlightRequests的作用还是为了处理重复的uri请求。有前面知道ImageRequest两个listener 当期请求完成有mDelivery分发请求,最后由listener处理最后的显示,而在ImageLoader中在Listener的onResponse和onErrorResponse中会调用onGetImageSuccess和onGetImageError函数在这两个函数中会
BatchedImageRequest request =mInFlightRequests.remove(cacheKey);
batchResponse(cacheKey, request);
从mInFlightRequests取出相同key的请求,然后调用batchResponse函数直接把结果通过listen设置上对应的图片。
mRequestQueue.add(newRequest);
这个函数我们前面已经详细分析过了。其实ImageLoader只是对mRequestQueue封装。方便用户更简单的使用。在这个方面Volley相比UIL去做需要的ImageLoaderConfiguration 和DisplayImageOptions 要更简洁,当然,我们也可以使用UIL默认的配置。
mInFlightRequests.put(cacheKey,
newBatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}
Volley加载图片的第三种方式:NetworkImageView
networkImageView= (NetworkImageView) findViewById(R.id.network_image_view);
networkImageView.setDefaultImageResId(R.drawable.default_image);
networkImageView.setErrorImageResId(R.drawable.failed_image);
networkImageView.setImageUrl("图片下载地址",imageLoader);
其实NetworkImageView就是继承ImageView然后封装了ImageLoader。也就是说它内部还是调用了第二种加载方式中ImageLoader.get,在ImageListener的onResponse函数中直接调用Imageview 的接口根据设置加载结果。看似简单的这个NetworkImageView背后其实隐藏着让人深思的问题。什么问题呢?来看看networkImageView.setImageUrl函数。
public void setImageUrl(String url,ImageLoader imageLoader) {
mUrl = url;
mImageLoader = imageLoader;
loadImageIfNecessary(false);
}
这个函数重点在于loadImageIfNecessary(false);对这个函数做完简化处理。
void loadImageIfNecessary(final booleanisInLayoutPass) {
// if the URL to be loaded in this viewis empty, cancel any old requests and clear the
// currently loaded image.
if (TextUtils.isEmpty(mUrl)) {
if (mImageContainer != null) {
mImageContainer.cancelRequest();
mImageContainer = null;
}
setDefaultImageOrNull();
return;
}
这个就是如果当前下载Url为空,mImageContainer不为空,表示什么?这个Imageview之前有一个下载图片的请求,现在需要用新的URL而这个值有为空,那我们就取消掉之前的,然后把现在的赋值为空。
// if there was an old request in thisview, check if it needs to be canceled.
if (mImageContainer != null &&mImageContainer.getRequestUrl() != null) {
if(mImageContainer.getRequestUrl().equals(mUrl)) {
// if the request is from thesame URL, return.
return;
} else {
// if there is a pre-existingrequest, cancel it if it's fetching a different URL.
mImageContainer.cancelRequest();
setDefaultImageOrNull();
}
}
如果运行到这里表示新的URL不为空,那么我们判断一下新的URL是否和之前的一样,一样就返回了,不一样,这取消掉之前的下载请求。
// The pre-existing content of thisview didn't match the current URL. Load the new image
// from the network.
ImageContainer newContainer =mImageLoader.get(mUrl,...);
然后的然后,就调用mImageLoader.get,到这里大家就都知道在干吗了。。。。
// update the ImageContainer to be thenew bitmap container.
mImageContainer = newContainer;
最后把新的newContainer 保存起来。
}
到这里,函数已经分析完了,用过UIL的都知道,它完成了,在ListView滑动中ImageView 重用时,取消之前Task的执行,从而避免下载不需要的图片,另外在ListView快速滑动过程中,我们可以监听并设置ImageLoaderEngine.pause 让其它还未执行下载的task阻塞。其实我开始说的值得深思的问题。
回头再看看如果Imageview当前新的URL与之前要加载的图片URL不一样,我们会调用mImageContainer.cancelRequest看他是如何取消的呢?
这个函数最重要的一个调用就是BatchedImageRequest. removeContainerAndCancelIfNecessary()而在这个函数中,有调用了ImageRequest.cancel(),前面分析过ImageRequest是我们NetworkDispatcher和CacheDispatcher一个执行的单位,这两个线程会从队列mNetworkQueue和mCacheQueue取出这个请求(ImageRequest)先判断是否已经取消,如果没有取消,那么就开始执行下载,如果取消了,就返回取下一个请求。原来是这样,那快速滑动怎么解决呢?其实,还是mImageContainer.cancelReques这个函数,试想一下如果listView快速滑动,那么Imageview必然会很快重用,很快被重用带来的是之前的请求很快就会被取消。那么NetworkDispatcher线程如果正在忙,等他忙完了,从mNetworkQueue取出来的ImageRequest可能就已经被取消了。如果不忙,就让它在快速滑动的时候请求一张图片也未尝不可嘛!
到此,我对volley和UIL简单的比较分析做完了。我尽量做到看完每一个类控制部分的数据和使用部分的数据,不放过我能发现的可以比较的细节,但是我知道还有许多我没有发现的东西。如果遗漏的地方请大神们多多指点!