一: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 队列