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>() {
       @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);

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() {
       @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() 方法便能进行图片的加载。

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() {
       @Override
       public Bitmap getBitmap(String url) {
           return null;
       }

       @Override
       public void putBitmap(String url, Bitmap bitmap) {

       }
    });
   networkImageView.setImageUrl(URL, imageLoader);

二、ImageRequest 源码解析


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

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

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

可以看到 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 步来看

① 获取 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() {
            @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<Bitmap>,我们来看看他是怎么实现的:

    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 源码解析


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 源码的阅读,到之后的代码整理以及现在的文章输出,花了我差不多一个星期的时间,不过对于网络加载和图片加载有了更深的理解。能完整看到这里的都是真爱啊,谢谢大家了。


相关文章

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏everhad

View事件分发

NOTE: 笔记,碎片式内容 控件 App界面的开主要就是使用View,或者称为控件。View既绘制内容又响应输入,输入事件主要就是触摸事件。 ViewTree...

28360
来自专栏一个会写诗的程序员的博客

Spring Boot actuator

10330
来自专栏向治洪

Universal Image Loader for Android 使用实例

<span style="white-space:pre">      </span>// 1.获取ImageLoader实例         ImageLo...

244100
来自专栏cloudskyme

gwt之mvc4g

Mvc4g是一个简单的框架来实现的GWT应用程序的MVC模式。 主要思想 其主要思想是,以减轻开发人员的工作,以单独的视图从模型。该框架是一个XML文件,将允...

34260
来自专栏Android知识点总结

O1-开源框架使用之EventBus

说明使用POSTING,发布与订阅在同一个线程,也就是子线程,更新UI会崩 说明使用MAIN,不管发布者在哪,订阅者都在main线程,可更新UI,但不能耗时操...

11120
来自专栏向治洪

android断点下载

断点下载往往用在大文件的下载过程中,如传统的迅雷下载用的就是断点下载技术,说起来原理比较简单:对文件进行分片,并对分片的文件进行标记,然后分片下载,下载完成后对...

293100
来自专栏知识分享

android 之TCP客户端编程

吸取教训!!!本来花了5个小时写完了,没想到,,,因为没点上面的自动保存查看一下,全没了,重新写呗 关于网络通信:每一台电脑都有自己的ip地址,每台电脑上的网络...

38880
来自专栏青蛙要fly的专栏

项目需求讨论-APP中提交信息及编辑信息界面及功能

好久好久没写文章了,这次我们来讨论下一些具有填写很多资料的界面,或者详情编辑界面等如何做起来更方便。 (PS:我写的可能不好,希望大家不好喷,哈哈,可以留言)

13720
来自专栏郭霖

Android ListView异步加载图片乱序问题,原因分析及解决方案

在Android所有系统自带的控件当中,ListView这个控件算是用法比较复杂的了,关键是用法复杂也就算了,它还经常会出现一些稀奇古怪的问题,让人非常头疼。比...

421100
来自专栏潇涧技术专栏

Android Universal Image Loader

最近在阅读Coding的安卓客户端源码,因为该源码的图片加载库使用的是universal-image-loader,我以前也使用过,但是没总结过,所以这次好好研...

8420

扫码关注云+社区

领取腾讯云代金券