Android Volley 源码解析(三),图片加载的实现
前言
在上一篇文章中,我们一起深入探究了 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 源码解析(三),图片加载的实现相关推荐
- Android xUtils3源码解析之图片模块
本文已授权微信公众号<非著名程序员>原创首发,转载请务必注明出处. xUtils3源码解析系列 一. Android xUtils3源码解析之网络模块 二. Android xUtils3 ...
- android资源加载流程6,FrameWork源码解析(6)-AssetManager加载资源过程
之前一段时间项目比较忙所以一直没有更新,接下来准备把插件化系列的文章写完,今天我们就先跳过ContentProvider源码解析来讲资源加载相关的知识,资源加载可以说是插件化非常重要的一环,我们很有必 ...
- Volley 源码解析之图片请求
一.前言 上篇文章我们分析了网络请求,这篇文章分析对图片的处理操作,如果没看上一篇,可以先看上一篇文章Volley 源码解析之网络请求.Volley 不仅仅对请求网络数据作了良好的封装,还封装了对图片 ...
- 从源码解析-结合Activity加载流程深入理解ActivityThrad的工作逻辑
ActivityThread源码解析 前言 类简称 类简介 一 二 三 四 五 代理和桩的理解 ActivityThread ActivityThread.main AT.attach AMN.get ...
- Spring源码解析-applicationContext.xml加载和bean的注册
applicationContext文件加载和bean注册流程 Spring对于从事Java开发的boy来说,再熟悉不过了,对于我们这个牛逼的框架的介绍就不在这里复述了,Spring这个大杂烩,怎 ...
- 有没有code能改xml内容_Spring源码解析-applicationContext.xml加载和bean的注册
applicationContext文件加载和bean注册流程 Spring对于从事Java开发的boy来说,再熟悉不过了,对于我们这个牛逼的框架的介绍就不在这里复述了,Spring这个大杂烩,怎 ...
- 【框架源码】Spring源码解析之BeanDefinition加载流程解析
观看本文之前,我们先思考一个问题,Spring是如何描述Bean对象的? Spring是根据BeanDefinition来创建Bean对象,BeanDefinition就是Spring中表示Bean定 ...
- Volley 源码解析之网络请求
Volley源码分析三部曲 Volley 源码解析之网络请求 Volley 源码解析之图片请求 Volley 源码解析之缓存机制 Volley 是 Google 推出的一款网络通信框架,非常适合数据量 ...
- Android xUtils3源码解析之注解模块
本文已授权微信公众号<非著名程序员>原创首发,转载请务必注明出处. xUtils3源码解析系列 一. Android xUtils3源码解析之网络模块 二. Android xUtils3 ...
- Android xUtils3源码解析之数据库模块
本文已授权微信公众号<非著名程序员>原创首发,转载请务必注明出处. xUtils3源码解析系列 一. Android xUtils3源码解析之网络模块 二. Android xUtils3 ...
最新文章
- 内含福利|CSDN 携手字节跳动:云原生Meetup北京站报名热烈启动,1月8日见!
- 中国民办教育市场需求与运营策略建议报告2022版
- 2048——Java控制台版本
- 设置某个元素的标签内容、设置元素的样式、层次选择器、总结选择器
- 学计算机需要带笔记本电脑,一年级学生必须带电脑上学吗?顾问给出建议,父母需要事先了解...
- android-x86 镜像iso下载_Windows 10(1909)最新12月更新版MSDN官方简体中文原版ISO镜像下载+激huo工ju...
- Mybatis-Plus 多表联查分页
- Linux学习总结(49)——应当竭力避免在系统中运行的 Linux 命令
- ElasticSearch入门 第五篇:使用C#查询文档
- python服务端处理post请求_使用JSON处理GET和POST请求的简单Python服务器
- excel 通用进销存(由excel+VBA+MSSQL制作)
- IIR和FIR滤波器设计低通滤波器
- sox源码分析:sox_find_effect()
- Linux系统运维与架构设计之Linux概述
- 软件工程-非功能需求撰写参考案例
- php zend optimizer,【原创】ZendOptimizer 的安装
- win2003服务器某一个网站被劫持,windows server 2012 iis被劫持的处理过程
- 计算机面试(考研复试)问题整理
- Selenium大家族介绍(selenium RC,selenium IDE, selenium Grid, selenium Webdriver)
- MATLAB 三维坐标绘图