Volley源码分析(2)----ImageLoader

时间:2022-05-09 06:54:14

一:imageLoader

先来看看如何使用imageloader:

public void showImg(View view){
ImageView imageView = (ImageView)this.findViewById(R.id.image_view);
RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext()); ImageLoader imageLoader = new ImageLoader(mQueue, new BitmapCache());
ImageListener listener = ImageLoader.getImageListener(imageView,R.drawable.default_image, R.drawable.default_image);
imageLoader.get("http://developer.android.com/images/home/aw_dac.png", listener);
//指定图片允许的最大宽度和高度
//imageLoader.get("http://developer.android.com/images/home/aw_dac.png",listener, 200, 200);
}

ImageLoader操作挺繁琐,但是关键的就一句:

imageLoader.get("http://developer.android.com/images/home/aw_dac.png", listener);

下载图片。

所以我们来分析这句,至于BitmapCache,RequestQueue的作用和目的,在

Volley源码分析(1)----Volley 队列 中已经说明。

/**
* Issues a bitmap request with the given URL if that image is not available
* in the cache, and returns a bitmap container that contains all of the data
* relating to the request (as well as the default image if the requested
* image is not available).
* @param requestUrl The url of the remote image
* @param imageListener The listener to call when the remote image is loaded
* @param maxWidth The maximum width of the returned image.
* @param maxHeight The maximum height of the returned image.
* @param scaleType The ImageViews ScaleType used to calculate the needed image size.
* @return A container object that contains all of the properties of the request, as well as
* the currently available image (default if remote is not loaded).
*/
public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight, ScaleType scaleType) { // only fulfill requests that were initiated from the main thread.
throwIfNotOnMainThread(); final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); // Try to look up the request in the cache of remote images.
Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
if (cachedBitmap != null) {
// Return the cached bitmap.
ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
imageListener.onResponse(container, true);
return container;
} // The bitmap did not exist in the cache, fetch it!
ImageContainer imageContainer =
new ImageContainer(null, requestUrl, cacheKey, imageListener); // Update the caller to let them know that they should use the default bitmap.
imageListener.onResponse(imageContainer, true); // Check to see if a request is already in-flight.
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
if (request != null) {
// If it is, add this request to the list of listeners.
request.addContainer(imageContainer);
return imageContainer;
} // The request is not already in flight. Send the new request to the network and
// track it.
Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
cacheKey); mRequestQueue.add(newRequest);
mInFlightRequests.put(cacheKey,
new BatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}
throwIfNotOnMainThread();

确保该申请是在主线程进行的。

如果有cache,就直接返回结果:

        Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
if (cachedBitmap != null) {
// Return the cached bitmap.
ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
imageListener.onResponse(container, true);
return container;
}

首先使用默认图片:

        // The bitmap did not exist in the cache, fetch it!
ImageContainer imageContainer =
new ImageContainer(null, requestUrl, cacheKey, imageListener); // Update the caller to let them know that they should use the default bitmap.
imageListener.onResponse(imageContainer, true);
        // Check to see if a request is already in-flight.
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
if (request != null) {
// If it is, add this request to the list of listeners.
request.addContainer(imageContainer);
return imageContainer;
}

判断是否有相同的请求正在队列里面,这里和前面volley的缓存十分类似!

最后添加一个imageRequest,放入队列里面。

关于cache的部分.

首先,

imageloader需要传入一个cache的对象,用以存放图片cache,我们称为image cache。

而网络请求队列也放置一个cache,放置网络返回的数据,我们称为request cache.

二:NetworkImageView

public class NetworkImageView extends ImageView 

可以直接使用的基于URL的imageView。

        networkImageView = (NetworkImageView) findViewById(R.id.nivTestView);

        mQueue = Volley.newRequestQueue(this);

        LruImageCache lruImageCache = LruImageCache.instance();

        ImageLoader imageLoader = new ImageLoader(mQueue,lruImageCache);

        networkImageView.setDefaultImageResId(R.drawable.ic_launcher);
networkImageView.setErrorImageResId(R.drawable.ic_launcher);
networkImageView.setImageUrl(URLS[1], imageLoader);

使用非常简单,setImageUrl。

networkImageView极有可能时使用在listView 或者GridView等有大量使用的地方。

这里就有2个问题:

1.networkImageView需要在UI线程显示图片,图片的获取无疑是在后台线程的。

2.imageLoader是否可以重用?

我们先看第一个问题:

图片的加载更新在如下方法中,

void loadImageIfNecessary(final boolean isInLayoutPass)

如果URL为空,显示默认图片。

        // if the URL to be loaded in this view is empty, cancel any old requests and clear the
// currently loaded image.
if (TextUtils.isEmpty(mUrl)) {
if (mImageContainer != null) {
mImageContainer.cancelRequest();
mImageContainer = null;
}
setDefaultImageOrNull();
return;
}
        // if there was an old request in this view, check if it needs to be canceled.
if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
if (mImageContainer.getRequestUrl().equals(mUrl)) {
// if the request is from the same URL, return.
return;
} else {
// if there is a pre-existing request, cancel it if it's fetching a different URL.
mImageContainer.cancelRequest();
setDefaultImageOrNull();
}
}

查看是否有old request在container中。

这个处理非常有用,在adapterlistView中,往往只是更新局部的内容,而这样就不需要处理,没有必要更新的imageView。

下面的过程就是调用imageloader的过程。

/**
* Loads the image for the view if it isn't already loaded.
* @param isInLayoutPass True if this was invoked from a layout pass, false otherwise.
*/
void loadImageIfNecessary(final boolean isInLayoutPass) {
int width = getWidth();
int height = getHeight();
ScaleType scaleType = getScaleType(); boolean wrapWidth = false, wrapHeight = false;
if (getLayoutParams() != null) {
wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT;
wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT;
} // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
// view, hold off on loading the image.
boolean isFullyWrapContent = wrapWidth && wrapHeight;
if (width == 0 && height == 0 && !isFullyWrapContent) {
return;
} // if the URL to be loaded in this view is empty, cancel any old requests and clear the
// currently loaded image.
if (TextUtils.isEmpty(mUrl)) {
if (mImageContainer != null) {
mImageContainer.cancelRequest();
mImageContainer = null;
}
setDefaultImageOrNull();
return;
} // if there was an old request in this view, check if it needs to be canceled.
if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
if (mImageContainer.getRequestUrl().equals(mUrl)) {
// if the request is from the same URL, return.
return;
} else {
// if there is a pre-existing request, cancel it if it's fetching a different URL.
mImageContainer.cancelRequest();
setDefaultImageOrNull();
}
} // Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens.
int maxWidth = wrapWidth ? 0 : width;
int maxHeight = wrapHeight ? 0 : height; // The pre-existing content of this view didn't match the current URL. Load the new image
// from the network.
ImageContainer newContainer = mImageLoader.get(mUrl,
new ImageListener() {
@Override
public void onErrorResponse(VolleyError error) {
if (mErrorImageId != 0) {
setImageResource(mErrorImageId);
}
} @Override
public void onResponse(final ImageContainer response, boolean isImmediate) {
// If this was an immediate response that was delivered inside of a layout
// pass do not set the image immediately as it will trigger a requestLayout
// inside of a layout. Instead, defer setting the image by posting back to
// the main thread.
if (isImmediate && isInLayoutPass) {
post(new Runnable() {
@Override
public void run() {
onResponse(response, false);
}
});
return;
} if (response.getBitmap() != null) {
setImageBitmap(response.getBitmap());
} else if (mDefaultImageId != 0) {
setImageResource(mDefaultImageId);
}
}
}, maxWidth, maxHeight, scaleType); // update the ImageContainer to be the new bitmap container.
mImageContainer = newContainer;
}

loadImageIfNecessary

2.imageLoader是否可以重用?

imageloader作为一个功能处理类,显然时不会跟一个imageview绑定在一起的。

我们更可以从上述分析的get方法中确定,每次get方法,如果需要都会生成一个imageRequest,所以并不会冲突。

三:Picasso分析

Picasso不仅实现了图片异步加载的功能,还解决了android中加载图片时需要解决的一些常见问题:

1.在adapter中需要取消已经不在视野范围的ImageView图片资源的加载,否则会导致图片错位,Picasso已经解决了这个问题。

2.使用复杂的图片压缩转换来尽可能的减少内存消耗

3.自带内存和硬盘二级缓存功能

我们首先来看看Picasso如何加载一个图片。

首先它有几个加载的方法:

  /**
* Start an image request using the specified URI.
* <p>
* Passing {@code null} as a {@code uri} will not trigger any request but will set a placeholder,
* if one is specified.
*
* @see #load(File)
* @see #load(String)
* @see #load(int)
*/
public RequestCreator load(Uri uri) {
return new RequestCreator(this, uri, 0);
}

可以看到,它可以加载 文件,文件路径,资源id,当然还有url。

Picasso.with(context).load("http://i.imgur.com/DvpvklR.png").into(imageView);

我们看看into方法:

com/squareup/picasso/RequestCreator.java

 /**
* Asynchronously fulfills the request into the specified {@link ImageView} and invokes the
* target {@link Callback} if it's not {@code null}.
* <p>
* <em>Note:</em> The {@link Callback} param is a strong reference and will prevent your
* {@link android.app.Activity} or {@link android.app.Fragment} from being garbage collected. If
* you use this method, it is <b>strongly</b> recommended you invoke an adjacent
* {@link Picasso#cancelRequest(android.widget.ImageView)} call to prevent temporary leaking.
*/
public void into(ImageView target, Callback callback) {
long started = System.nanoTime();
checkMain(); if (target == null) {
throw new IllegalArgumentException("Target must not be null.");
} if (!data.hasImage()) {
picasso.cancelRequest(target);
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
return;
} if (deferred) {
if (data.hasSize()) {
throw new IllegalStateException("Fit cannot be used with resize.");
}
int width = target.getWidth();
int height = target.getHeight();
if (width == 0 || height == 0) {
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
picasso.defer(target, new DeferredRequestCreator(this, target, callback));
return;
}
data.resize(width, height);
} Request request = createRequest(started);
String requestKey = createKey(request); if (!skipMemoryCache) {
Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
if (bitmap != null) {
picasso.cancelRequest(target);
setBitmap(target, picasso.context, bitmap, MEMORY, noFade, picasso.indicatorsEnabled);
if (picasso.loggingEnabled) {
log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + MEMORY);
}
if (callback != null) {
callback.onSuccess();
}
return;
}
} if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
} Action action =
new ImageViewAction(picasso, target, request, skipMemoryCache, noFade, errorResId,
errorDrawable, requestKey, tag, callback); picasso.enqueueAndSubmit(action);
}

into

一开始也是判断主线程,和Volley的networkImageView相同。

判断传入的uri,或者resourceid是否有问题。

不对,就取消这次请求。

下面就是调整图片请求的大小。

    if (deferred) {
if (data.hasSize()) {
throw new IllegalStateException("Fit cannot be used with resize.");
}
int width = target.getWidth();
int height = target.getHeight();
if (width == 0 || height == 0) {
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
picasso.defer(target, new DeferredRequestCreator(this, target, callback));
return;
}
data.resize(width, height);
}

这段不重要,我们继续看下去:

    Request request = createRequest(started);
String requestKey = createKey(request);
 /** Create the request optionally passing it through the request transformer. */
private Request createRequest(long started) {
int id = getRequestId(); Request request = data.build();
request.id = id;
request.started = started; boolean loggingEnabled = picasso.loggingEnabled;
if (loggingEnabled) {
log(OWNER_MAIN, VERB_CREATED, request.plainId(), request.toString());
} Request transformed = picasso.transformRequest(request);
if (transformed != request) {
// If the request was changed, copy over the id and timestamp from the original.
transformed.id = id;
transformed.started = started; if (loggingEnabled) {
log(OWNER_MAIN, VERB_CHANGED, transformed.logId(), "into " + transformed);
}
} return transformed;
}

createRequest

首先是配置id,和starttime,这个可以定位request。

requestKey其实就是一套方法用于区分request,更重要的是,获取缓存。

默认使用的memory cache 为LruCache。

此处可以imageLoader(指Volley里面的imageloader,下同)做比较,imageLoader一般使用BitmapCache

作为内存缓存,文件缓存有vollley requestQueue做处理。所以从结构上来说,Picasso和imageLoader是相似的。

接下来就是从LruCache中获取图片。

    if (!skipMemoryCache) {
Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
if (bitmap != null) {
picasso.cancelRequest(target);
target.onBitmapLoaded(bitmap, MEMORY);
return;
}
}

不需考虑,Picasso肯定也有请求队列,他是通过android的handle-thread的方式来驱动的。

  void submit(Action action) {
dispatcher.dispatchSubmit(action);
}
  void dispatchSubmit(Action action) {
handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
}
    @Override public void handleMessage(final Message msg) {
switch (msg.what) {
case REQUEST_SUBMIT: {
Action action = (Action) msg.obj;
dispatcher.performSubmit(action);
break;
}

该handler是在异步线程里面处理消息。

然后dispatcher.performSubmit(action);会在线程池中PicassoExecutorService启动task。

接着我们看com/squareup/picasso/BitmapHunter.java:

网络请求的,应该在这里处理。

Bitmap hunt() throws IOException {
Bitmap bitmap = null; if (!skipMemoryCache) {
bitmap = cache.get(key);
if (bitmap != null) {
stats.dispatchCacheHit();
loadedFrom = MEMORY;
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_DECODED, data.logId(), "from cache");
}
return bitmap;
}
} data.loadFromLocalCacheOnly = (retryCount == 0);
RequestHandler.Result result = requestHandler.load(data);
if (result != null) {
bitmap = result.getBitmap();
loadedFrom = result.getLoadedFrom();
exifRotation = result.getExifOrientation();
} if (bitmap != null) {
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_DECODED, data.logId());
}
stats.dispatchBitmapDecoded(bitmap);
if (data.needsTransformation() || exifRotation != 0) {
synchronized (DECODE_LOCK) {
if (data.needsMatrixTransform() || exifRotation != 0) {
bitmap = transformResult(data, bitmap, exifRotation);
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId());
}
}
if (data.hasCustomTransformations()) {
bitmap = applyCustomTransformations(data.transformations, bitmap);
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId(), "from custom transformations");
}
}
}
if (bitmap != null) {
stats.dispatchBitmapTransformed(bitmap);
}
}
} return bitmap;
}

开始还是从memory cache中获取。

在hunt方法中,

    RequestHandler.Result result = requestHandler.load(data);

这句就是图片获取的过程。

但是这个requestHandler究竟是什么?

从头看,在Picasso构造函数里面有:

    allRequestHandlers.add(new ContactsPhotoRequestHandler(context));
allRequestHandlers.add(new MediaStoreRequestHandler(context));
allRequestHandlers.add(new ContentStreamRequestHandler(context));
allRequestHandlers.add(new AssetRequestHandler(context));
allRequestHandlers.add(new FileRequestHandler(context));
allRequestHandlers.add(new NetworkRequestHandler(dispatcher.downloader, stats));

所以,requestHandler可能就是上面一个里面的一个。

我们就看FileRequestHandler。

  @Override public boolean canHandleRequest(Request data) {
return SCHEME_FILE.equals(data.uri.getScheme());
}

也就是,FileRequestHandler只处理file类型的请求。

所以从网络获取图片com/squareup/picasso/NetworkRequestHandler.java:

直接看load:

@Override public Result load(Request data) throws IOException {
Response response = downloader.load(data.uri, data.loadFromLocalCacheOnly);
if (response == null) {
return null;
} Picasso.LoadedFrom loadedFrom = response.cached ? DISK : NETWORK; Bitmap bitmap = response.getBitmap();
if (bitmap != null) {
return new Result(bitmap, loadedFrom);
} InputStream is = response.getInputStream();
if (is == null) {
return null;
}
// Sometimes response content length is zero when requests are being replayed. Haven't found
// root cause to this but retrying the request seems safe to do so.
if (response.getContentLength() == 0) {
Utils.closeQuietly(is);
throw new IOException("Received response with 0 content-length header.");
}
if (loadedFrom == NETWORK && response.getContentLength() > 0) {
stats.dispatchDownloadFinished(response.getContentLength());
}
try {
return new Result(decodeStream(is, data), loadedFrom);
} finally {
Utils.closeQuietly(is);
}
}

load

      if (downloader == null) {
downloader = Utils.createDefaultDownloader(context);
}

download是在build里面创建的,其实很多内容都是在这里面创建的。

 public Picasso build()

最终获取的downloader对象是HttpURLConnection来进行网络通信。

我们看到response的结果是2种,DISK : NETWORK。

所以我们分析如何得到这2种结果,整个picasso的图片加载过程也就结束了。

1.UrlConnectionDownloader:

 @Override public Response load(Uri uri, boolean localCacheOnly) throws IOException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
installCacheIfNeeded(context);
} HttpURLConnection connection = openConnection(uri);
connection.setUseCaches(true);
if (localCacheOnly) {
connection.setRequestProperty("Cache-Control", "only-if-cached,max-age=" + Integer.MAX_VALUE);
} int responseCode = connection.getResponseCode();
if (responseCode >= 300) {
connection.disconnect();
throw new ResponseException(responseCode + " " + connection.getResponseMessage());
} long contentLength = connection.getHeaderFieldInt("Content-Length", -1);
boolean fromCache = parseResponseSourceHeader(connection.getHeaderField(RESPONSE_SOURCE)); return new Response(connection.getInputStream(), fromCache, contentLength);
}

通过HttpUrlConnection的cache,来得到结果,以及fromCache。

2.OkHttpDownloader

@Override public Response load(Uri uri, boolean localCacheOnly) throws IOException {
HttpURLConnection connection = openConnection(uri);
connection.setUseCaches(true);
if (localCacheOnly) {
connection.setRequestProperty("Cache-Control", "only-if-cached,max-age=" + Integer.MAX_VALUE);
} int responseCode = connection.getResponseCode();
if (responseCode >= 300) {
connection.disconnect();
throw new ResponseException(responseCode + " " + connection.getResponseMessage());
} String responseSource = connection.getHeaderField(RESPONSE_SOURCE_OKHTTP);
if (responseSource == null) {
responseSource = connection.getHeaderField(RESPONSE_SOURCE_ANDROID);
} long contentLength = connection.getHeaderFieldInt("Content-Length", -1);
boolean fromCache = parseResponseSourceHeader(responseSource); return new Response(connection.getInputStream(), fromCache, contentLength);
}

过程类似。

至此,一个完整的获取图片的流程就可以看到了。

1.每一条图片请求将会包装成一个request。

2.每一个request将会submit到一个DispatcherThread的线程做分发操作。

3.找到request对应的RequestHandler。把bitmaphunter放入PicassoExecutorService线程池里面。

4.由不同的RequestHandler来处理各种情况,包括网络request。

5.剩下的就是结果的处理,和delivier response的过程了。

由于本文只关心网络图片的加载过程,所以只涉及了Picasso的2级缓存功能。

参考:

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/0731/1639.html

http://square.github.io/picasso/

本系列第一篇:Volley源码分析(1)----Volley 队列