前言

在上一篇文章中,我们一起深入探究了 Volley 的缓存机制,通过源码分析对缓存的工作原理进行了了解,这篇文章将带大家一起探究「Volley 图片加载的实现」,图片加载跟缓存还是有比较紧密的联系的,建议大家先去看下:Android Volley 源码解析(二),探究缓存机制。

这是 Volley 源码解析系列的最后一篇文章,今天我们通过以基本用法和源码分析相结合的方式来进行,当然本文的源码还是建立在第一篇源码分析的基础上的,还没有看过这篇文章的朋友,建议先去阅读:Android Volley 源码解析(一),网络请求的执行流程。

一、图片加载的基本用法


在进行源码解析之前,我们先来看一下 Volley 中有关图片加载的基本用法。

1.1 ImageRequest 的用法

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>() {@Overridepublic void onResponse(Bitmap response) {imageView.setImageBitmap(response);}}, 0, 0, ImageView.ScaleType.CENTER_CROP, Bitmap.Config.RGB_565, new Response.ErrorListener() {@Overridepublic 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);
复制代码

1.2 ImageLoader 的用法

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() {@Overridepublic Bitmap getBitmap(String url) {return null;}@Overridepublic 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() 方法便能进行图片的加载。

1.3 NetworkImageView

除了以上两种方式之外,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() {@Overridepublic Bitmap getBitmap(String url) {return null;}@Overridepublic void putBitmap(String url, Bitmap bitmap) {}});networkImageView.setImageUrl(URL, imageLoader);
复制代码

二、ImageRequest 源码解析


在上一节中介绍了 Volley 图片加载的三种方法,从这节开始我们结合源码来分析 Volley 中图片加载的实现,就从 ImageRequest 开始吧。

我们在 Android Volley 源码解析(一),网络请求的执行流程 这篇文章中讲到,网络请求最终会将从服务器返回的结果封装成 NetworkResponse 然后传给 Request 进行处理。而 ImageRequest 的工作,其实就是将 NetworkResponse 解析成包含 Bitmap 的 Response,最后再回调出去。

我们要进行分析的,也就是这个过程。

可以看到 parseNetworkResponse 中只有一个 doParse() 方法

    @Overrideprotected 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);// ③ 根据我们想要的宽和高得到对应的 BitmapdecodeOptions.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 步来看

① 获取 Bitmap 原始的宽和高

通过 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;}
复制代码
③ 根据我们想要的宽和高得到对应的 Bitmap

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;}
复制代码
④ 如果 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));}
复制代码

三、ImageLoader 源码解析


我们在上面说到 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() {@Overridepublic void onErrorResponse(VolleyError error) {if (errorImageResId != 0) {view.setImageResource(errorImageResId);}}@Overridepublic 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 设置给 ImageViewBitmap 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>() {@Overridepublic void onResponse(Bitmap response) {onGetImageSuccess(cacheKey, response);}}, maxWidth, maxHeight, scaleType, Config.RGB_565, new ErrorListener() {@Overridepublic 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() {@Overridepublic 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 源码解析


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() {@Overridepublic void onErrorResponse(VolleyError error) {if (mErrorImageId != 0) {setImageResource(mErrorImageId);}}@Overridepublic void onResponse(final ImageContainer response, boolean isImmediate) {if (isImmediate && isInLayoutPass) {post(new Runnable() {@Overridepublic 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 源码的阅读,到之后的代码整理以及现在的文章输出,花了我差不多一个星期的时间,不过对于网络加载和图片加载有了更深的理解。能完整看到这里的都是真爱啊,谢谢大家了。


相关文章

  • Android Volley 源码解析(一),网络请求的执行流程
  • Android Volley 源码解析(二),探究缓存机制

Android Volley 源码解析(三),图片加载的实现相关推荐

  1. Android xUtils3源码解析之图片模块

    本文已授权微信公众号<非著名程序员>原创首发,转载请务必注明出处. xUtils3源码解析系列 一. Android xUtils3源码解析之网络模块 二. Android xUtils3 ...

  2. android资源加载流程6,FrameWork源码解析(6)-AssetManager加载资源过程

    之前一段时间项目比较忙所以一直没有更新,接下来准备把插件化系列的文章写完,今天我们就先跳过ContentProvider源码解析来讲资源加载相关的知识,资源加载可以说是插件化非常重要的一环,我们很有必 ...

  3. Volley 源码解析之图片请求

    一.前言 上篇文章我们分析了网络请求,这篇文章分析对图片的处理操作,如果没看上一篇,可以先看上一篇文章Volley 源码解析之网络请求.Volley 不仅仅对请求网络数据作了良好的封装,还封装了对图片 ...

  4. 从源码解析-结合Activity加载流程深入理解ActivityThrad的工作逻辑

    ActivityThread源码解析 前言 类简称 类简介 一 二 三 四 五 代理和桩的理解 ActivityThread ActivityThread.main AT.attach AMN.get ...

  5. Spring源码解析-applicationContext.xml加载和bean的注册

    applicationContext文件加载和bean注册流程 ​ Spring对于从事Java开发的boy来说,再熟悉不过了,对于我们这个牛逼的框架的介绍就不在这里复述了,Spring这个大杂烩,怎 ...

  6. 有没有code能改xml内容_Spring源码解析-applicationContext.xml加载和bean的注册

    applicationContext文件加载和bean注册流程 ​ Spring对于从事Java开发的boy来说,再熟悉不过了,对于我们这个牛逼的框架的介绍就不在这里复述了,Spring这个大杂烩,怎 ...

  7. 【框架源码】Spring源码解析之BeanDefinition加载流程解析

    观看本文之前,我们先思考一个问题,Spring是如何描述Bean对象的? Spring是根据BeanDefinition来创建Bean对象,BeanDefinition就是Spring中表示Bean定 ...

  8. Volley 源码解析之网络请求

    Volley源码分析三部曲 Volley 源码解析之网络请求 Volley 源码解析之图片请求 Volley 源码解析之缓存机制 Volley 是 Google 推出的一款网络通信框架,非常适合数据量 ...

  9. Android xUtils3源码解析之注解模块

    本文已授权微信公众号<非著名程序员>原创首发,转载请务必注明出处. xUtils3源码解析系列 一. Android xUtils3源码解析之网络模块 二. Android xUtils3 ...

  10. Android xUtils3源码解析之数据库模块

    本文已授权微信公众号<非著名程序员>原创首发,转载请务必注明出处. xUtils3源码解析系列 一. Android xUtils3源码解析之网络模块 二. Android xUtils3 ...

最新文章

  1. 内含福利|CSDN 携手字节跳动:云原生Meetup北京站报名热烈启动,1月8日见!
  2. 中国民办教育市场需求与运营策略建议报告2022版
  3. 2048——Java控制台版本
  4. 设置某个元素的标签内容、设置元素的样式、层次选择器、总结选择器
  5. 学计算机需要带笔记本电脑,一年级学生必须带电脑上学吗?顾问给出建议,父母需要事先了解...
  6. android-x86 镜像iso下载_Windows 10(1909)最新12月更新版MSDN官方简体中文原版ISO镜像下载+激huo工ju...
  7. Mybatis-Plus 多表联查分页
  8. Linux学习总结(49)——应当竭力避免在系统中运行的 Linux 命令
  9. ElasticSearch入门 第五篇:使用C#查询文档
  10. python服务端处理post请求_使用JSON处理GET和POST请求的简单Python服务器
  11. excel 通用进销存(由excel+VBA+MSSQL制作)
  12. IIR和FIR滤波器设计低通滤波器
  13. sox源码分析:sox_find_effect()
  14. Linux系统运维与架构设计之Linux概述
  15. 软件工程-非功能需求撰写参考案例
  16. php zend optimizer,【原创】ZendOptimizer 的安装
  17. win2003服务器某一个网站被劫持,windows server 2012 iis被劫持的处理过程
  18. 计算机面试(考研复试)问题整理
  19. Selenium大家族介绍(selenium RC,selenium IDE, selenium Grid, selenium Webdriver)
  20. MATLAB 三维坐标绘图

热门文章

  1. webpack 的使用教程
  2. 打印图形(内测第1届第1题)
  3. 【学习笔记】【OC语言】继承
  4. java性能优化文章
  5. Linux进程间通信IPC学习笔记之同步一(线程、互斥锁和条件变量)
  6. 不可或缺的PrepareImageRegions函数
  7. VB.NET的数据库基础编程[zz]
  8. iOS 链接库“libbaidumapapi.a”缺少此目标所需的一个或多个体系结构:arm64、armv7
  9. 2017.3.27-morning
  10. 移动端开发之px,em和rem详解