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


相关文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,423评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,339评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,241评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,503评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,824评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,262评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,615评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,337评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,989评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,300评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,829评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,193评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,753评论 3 230
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,970评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,708评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,295评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,207评论 2 258

推荐阅读更多精彩内容