最美应用有价值的好应用 2018-02-24
在上一篇文章中,我们一起深入探究了 Volley 的缓存机制,通过源码分析对缓存的工作原理进行了了解,这篇文章将带大家一起探究「Volley 图片加载的实现」,图片加载跟缓存还是有比较紧密的联系的,建议大家先去看下:Android Volley 源码解析(二),探究缓存机制。
这是 Volley 源码解析系列的最后一篇文章,今天我们通过以基本用法和源码分析相结合的方式来进行,当然本文的源码还是建立在第一篇源码分析的基础上的,还没有看过这篇文章的朋友,建议先去阅读:Android Volley 源码解析(一),网络请求的执行流程。
在进行源码解析之前,我们先来看一下 Volley 中有关图片加载的基本用法。
ImageRequest 和 StringRequest 以及 JsonRequest 都是继承自 Request,因此他们的用法也基本是相同的,首先需要获取一个 RequestQueue 对象:
RequestQueue mQueue = Volley.newRequestQueue(context);
接着 new 出一个 ImageRequest 对象:
private static final String URL = "http://ww4.sinaimg.cn/large/610dc034gw1euxdmjl7j7j20r2180wts.jpg"; ImageRequest imageRequest = new ImageRequest(URL, new Response.Listener<Bitmap>() { @Override public void onResponse(Bitmap response) { imageView.setImageBitmap(response); } }, 0, 0, ImageView.ScaleType.CENTER_CROP, Bitmap.Config.RGB_565, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { } });
可以看到 ImageRequest 接收六个参数:
1、图片的 URL 地址
2、图片请求成功的回调,这里我们将返回的 Bitmap 设置到 ImageView 中
3、4 分别用于指定允许图片最大的宽度和高度,如果指定的网络图片的宽度或高度大于这里的值,就会对图片进行压缩,指定为 0 的话,表示不管图片有多大,都不进行压缩
5、指定图片的属性,Bitmap.Config 下的几个常量都可以使用,其中 ARGB_8888 可以展示最好的颜色属性,每个图片像素像素占 4 个字节,RGB_565 表示每个图片像素占 2 个字节
6、图片请求失败的回调
最后将这个 ImageRequest 添加到 RequestQueue 就行了
mQueue.add(imageRequest);
ImageLoader 其实是对 ImageRequest 的封装,它不仅可以帮我们对图片进行缓存,还可以过滤掉重复的链接,避免重复发送请求,因此 ImageLoader 要比 ImageRequest 更加高效。
ImageLoader 的用法,主要分为以下四步:
1、创建 RequestQueue 对象2、创建一个 ImageLoader 对象3、获取一个 ImageListener 对象4、调用 ImageLoader 的 get() 方法记载图片
RequestQueue requestQueue = Volley.newRequestQueue(this); ImageLoader imageLoader = new ImageLoader(requestQueue, new ImageLoader.ImageCache() { @Override public Bitmap getBitmap(String url) { return null; } @Override public void putBitmap(String url, Bitmap bitmap) { } }); ImageLoader.ImageListener listener = ImageLoader.getImageListener(mIvShow, R.mipmap.ic_launcher, R.mipmap.ic_launcher_round); imageLoader.get(URL, listener);
可以看到 ImageLoader 的构造函数接收两个参数,第一个参数就是 RequestQueue 对象,第二个参数是 ImageCache,我们这里直接 new 出一个空的 ImageCache 实现就行了。
在 ImageListener 中传入所加载图片的 URL,以及图片占位符和加载失败后显示的图片,最后调用 ImageLoader.get() 方法便能进行图片的加载。
除了以上两种方式之外,Volley 还提供了第三种方式来加载网络图片,NetworkImageView 是一个继承自 ImageView 的自定义 View,在 ImageView 的基础上拓展加载网络图片的功能。NetworkImageView 的用法还是比较简单的。大致可以分为 4 步:
1、创建一个 RequestQueue 对象2、创建一个 ImageLoader 对象3、在代码中获取 NetworkImageView 的实例4、设置要加载的图片地址
如下所示:
RequestQueue requestQueue = Volley.newRequestQueue(this); ImageLoader imageLoader = new ImageLoader(requestQueue, new ImageLoader.ImageCache() { @Override public Bitmap getBitmap(String url) { return null; } @Override public void putBitmap(String url, Bitmap bitmap) { } }); networkImageView.setImageUrl(URL, imageLoader);
在上一节中介绍了 Volley 图片加载的三种方法,从这节开始我们结合源码来分析 Volley 中图片加载的实现,就从 ImageRequest 开始吧。
我们在 Android Volley 源码解析(一),网络请求的执行流程 这篇文章中讲到,网络请求最终会将从服务器返回的结果封装成 NetworkResponse 然后传给 Request 进行处理。而 ImageRequest 的工作,其实就是将 NetworkResponse 解析成包含 Bitmap 的 Response,最后再回调出去。
我们要进行分析的,也就是这个过程。
可以看到 parseNetworkResponse 中只有一个 doParse() 方法
@Override protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) { synchronized (sDecodeLock) { try { return doParse(response); } catch (OutOfMemoryError e) { return Response.error(new ParseError(e)); } } }
就让我们看看 doParse() 里面究竟进行了什么操作
private Response<Bitmap> doParse(NetworkResponse response) { byte[] data = response.data; BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); Bitmap bitmap = null; if (mMaxWidth == 0 && mMaxHeight == 0) { decodeOptions.inPreferredConfig = mDecodeConfig; bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); } else { // ① 获取 Bitmap 原始的宽和高 decodeOptions.inJustDecodeBounds = true; BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); int actualWidth = decodeOptions.outWidth; int actualHeight = decodeOptions.outHeight; // ② 计算我们真正想要的宽和高 int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight, actualWidth, actualHeight, mScaleType); int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth, actualHeight, actualWidth, mScaleType); // ③ 根据我们想要的宽和高得到对应的 Bitmap decodeOptions.inJustDecodeBounds = false; decodeOptions.inSampleSize = findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight); Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); // ④ 如果 Bitmap 不为 bull 而且宽或高大于目标宽高的话,再一次压缩 if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth || tempBitmap.getHeight() > desiredHeight)) { bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desiredHeight, true); tempBitmap.recycle(); } else { bitmap = tempBitmap; } } // ⑤ 将得到的 包含 Bitmap 的 Response 回调出去 if (bitmap == null) { return Response.error(new ParseError(response)); } else { return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response)); } }
代码比较长,我们分为 5 步来看
通过 BitmapFactory 将传入的 NetworkResponse 中的 data 转换成对应的 Bitmap,然后通过设置 BitmapOptions.inJustDecodeBounds = true,得到 Bitmap 的原始宽和高,这里补充一下,当 BitmapOptions.inJustDecodeBounds = true 的时候,BitmapFactory.decode 并不会真的返回一个 bitmap 给你,它仅仅会把一些图片的大小信息(如宽和高)返回给你,而不会占用太多的内存。
应该还记得我们构建 ImageRequest 的时候传入的参数吧,那 6 个参数里面,包含两个分别指定图片最大宽和高的参数,我们将传入的图片最大宽和高以及 Bitmap 真实的宽和高,通过 getResizedDemension() 方法计算出比较合适的图片显示宽高,代码如下:
private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, int actualSecondary, ScaleType scaleType) { if ((maxPrimary == 0) && (maxSecondary == 0)) { return actualPrimary; } if (maxPrimary == 0) { double ratio = (double) maxSecondary / (double) actualSecondary; return (int) (actualPrimary * ratio); } if (maxSecondary == 0) { return maxPrimary; } double ratio = (double) actualSecondary / (double) actualPrimary; int resized = maxPrimary; if (scaleType == ScaleType.CENTER_CROP) { if ((resized * ratio) < maxSecondary) { resized = (int) (maxSecondary / ratio); } return resized; } if ((resized * ratio) > maxSecondary) { resized = (int) (maxSecondary / ratio); } return resized; }
DecodeOptions.inJustDecodeBounds = true 代表将一个真正的 Bitmap 返回给你,DecodeOptions.inSampleSize 代表图片的采样率,是跟图片压缩有关的参数,如果 inSampliSize = 2 则代表将原先图片的宽和高分别减小为原来的 1/2,以此类推。
decodeOptions.inJustDecodeBounds = false; decodeOptions.inSampleSize = findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight); Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
// 计算采样率的方法 static int findBestSampleSize( int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) { double wr = (double) actualWidth / desiredWidth; double hr = (double) actualHeight / desiredHeight; double ratio = Math.min(wr, hr); float n = 1.0f; while ((n * 2) <= ratio) { n *= 2; } return (int) n; }
if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth || tempBitmap.getHeight() > desiredHeight)) { bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desiredHeight, true); tempBitmap.recycle(); } else { bitmap = tempBitmap; }
if (bitmap == null) { return Response.error(new ParseError(response)); } else { return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response)); }
我们在上面说到 ImageLoader 的用法,主要分为四步:
1、创建 RequestQueue 对象2、创建一个 ImageLoader 对象3、获取一个 ImageListener 对象4、调用 ImageLoader 的 get() 方法加载图片
那我们就从它的用法入手,一步一步分析究竟是怎么实现的。
创建 RequestQueue 在之前已经讲过,可以参考这篇文章:Android Volley 源码解析(一),网络请求的执行流程,我们看下 ImageLoader 的构造方法:
public ImageLoader(RequestQueue queue, ImageCache imageCache) { mRequestQueue = queue; mCache = imageCache; }
可以看到构造方法将 RequestQueue 和 ImageCache 赋值给当前实例的成员变量,我们接着看 ImageListener 获取,ImageListener 是通过 ImageLoader.getImageListener() 方法获取的:
public static ImageListener getImageListener(final ImageView view, final int defaultImageResId, final int errorImageResId) { return new ImageListener() { @Override public void onErrorResponse(VolleyError error) { if (errorImageResId != 0) { view.setImageResource(errorImageResId); } } @Override public void onResponse(ImageContainer response, boolean isImmediate) { if (response.getBitmap() != null) { view.setImageBitmap(response.getBitmap()); } else if (defaultImageResId != 0) { view.setImageResource(defaultImageResId); } } }; }
可以看到在这里面主要是将回调出来的 Bitmap 设置给对应的 ImageView,以及做一些图片加载的容错处理。
最后重点来了,ImageLoader 的 get() 方法是 ImageLoader 类最复杂的方法,也是最核心的方法,我们一起来看看吧:
public ImageContainer get(String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight, ScaleType scaleType) { // 如果当前不是在主线程就抛出异常(UI 操作必须在主线程进行) throwIfNotOnMainThread(); final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); // 从缓存中取出对应的 Bitmap,如果 Bitmap 不为 null,直接回调 imageListener 将 Bitmap 设置给 ImageView Bitmap cachedBitmap = mCache.getBitmap(cacheKey); if (cachedBitmap != null) { ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null); imageListener.onResponse(container, true); return container; } ImageContainer imageContainer = new ImageContainer(null, requestUrl, cacheKey, imageListener); imageListener.onResponse(imageContainer, true); // 判断该请求是否是否在缓存队列中 BatchedImageRequest request = mInFlightRequests.get(cacheKey); if (request != null) { request.addContainer(imageContainer); return imageContainer; } // 如果在缓存中并没有找到该请求,便进行一次网络请求, Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey); mRequestQueue.add(newRequest); // 将请求进行缓存 mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer)); return imageContainer; }
首先进行了当前线程的判断,如果不是主线程的话,就直接抛出错误。
private void throwIfNotOnMainThread() { if (Looper.myLooper() != Looper.getMainLooper()) { throw new IllegalStateException("ImageLoader must be invoked from the main thread."); } }
然后从缓存中取出对应的 Bitmap,如果 Bitmap 不为 null,直接回调 ImageListener 将 Bitmap 设置给对应的 ImageView。
Bitmap cachedBitmap = mCache.getBitmap(cacheKey); if (cachedBitmap != null) { ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null); imageListener.onResponse(container, true); return container; }
然后根据 Url 从缓存队列中取出 Request
BatchedImageRequest request = mInFlightRequests.get(cacheKey); if (request != null) { request.addContainer(imageContainer); return imageContainer; }
如果在缓存中并没有找到该请求,便进行一次网络请求
Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey);
可以看到 ImageLoader 调用了 makeImageReqeust() 方法来构建 Request,我们来看看他是怎么实现的:
protected Request<Bitmap> makeImageRequest(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType, final String cacheKey) { return new ImageRequest(requestUrl, new Listener<Bitmap>() { @Override public void onResponse(Bitmap response) { onGetImageSuccess(cacheKey, response); } }, maxWidth, maxHeight, scaleType, Config.RGB_565, new ErrorListener() { @Override public void onErrorResponse(VolleyError error) { onGetImageError(cacheKey, error); } }); }
网络请求成功之后,调用 onGetImageSuccess() 方法,将 Bitmap 进行缓存,以及将缓存队列中 cacheKey 对应的 BatchedImageRequest 移除掉,最后调用 batchResponse() 方法。
protected void onGetImageSuccess(String cacheKey, Bitmap response) { mCache.putBitmap(cacheKey, response); BatchedImageRequest request = mInFlightRequests.remove(cacheKey); if (request != null) { request.mResponseBitmap = response; batchResponse(cacheKey, request); } }
在 batchResponse() 方法中,在主线程里面将 Bitmap 回调给 ImageListner,然后将 Bitmap 设置给 ImageView,这样便完成了图片加载的全部过程。
private void batchResponse(String cacheKey, BatchedImageRequest request) { mBatchedResponses.put(cacheKey, request); if (mRunnable == null) { mRunnable = new Runnable() { @Override public void run() { for (BatchedImageRequest bir : mBatchedResponses.values()) { for (ImageContainer container : 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; } }; mHandler.postDelayed(mRunnable, mBatchResponseDelayMs); } }
NetworkImageView 是一个内部使用 ImageLoader 来进行加载网络图片的自定义 View,我们在上面提到,NetworkImageView 的使用方法主要分为四步:
1、创建一个 RequestQueue 对象2、创建一个 ImageLoader 对象3、在代码中获取 NetworkImageView 的实例4、调用 setImageUrl() 方法来设置要加载的图片地址
其中最后一步是 NetworkImageView 的核心,我们来看看 setImageUrl() 内部是怎么实现的吧:
public void setImageUrl(String url, ImageLoader imageLoader) { mUrl = url; mImageLoader = imageLoader; loadImageIfNecessary(false); }
只有简单的三行代码,想必主要的逻辑就在 loadImageIfNecessary() 这个方法里面,我们点进去看一下:
void loadImageIfNecessary(final boolean isInLayoutPass) { // 如果 URL 为 null,则取消该请求 if (TextUtils.isEmpty(mUrl)) { if (mImageContainer != null) { mImageContainer.cancelRequest(); mImageContainer = null; } setDefaultImageOrNull(); return; } // 如果该 NetworkImageView 之前已经掉用过 setImageUrl(), // 判断当前的 Url 跟之前请求的 URL 是否相同 if (mImageContainer != null && mImageContainer.getRequestUrl() != null) { if (mImageContainer.getRequestUrl().equals(mUrl)) { return; } else { mImageContainer.cancelRequest(); setDefaultImageOrNull(); } } // 通过 ImageLoader 进行图片加载 mImageContainer = 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 (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); }
代码还是相对比较清晰的,先进行一些容错性的处理,然后调用 ImageLoader 来获取对应的 bitmap,最后将其设置给 NetworkImageView.
Volley 源码解析系列,到这里就全部结束了,这是我写过最长的系列文章了,从一开始 Volley 源码的阅读,到之后的代码整理以及现在的文章输出,花了我差不多一个星期的时间,不过对于网络加载和图片加载有了更深的理解。能完整看到这里的都是真爱啊,谢谢大家了。