从源码角度看Volley中图片加载ImageLoader的重复URL过滤功能

本文详细介绍了如何解决Android开发中使用Volley时遇到的图片缓存导致无法更新显示的问题,通过分析源码,提供了具体的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在android开发中,volley算的上是一个比较出名的开源库,首先是因为它是google官方出品的,有亲儿子的意思。其次,它功能强大,它不仅能够高效的完成高并发且数据量小的网络请求,还附带有自己的图片请求库。这对于我们这种还在苦苦自学android开发初学者来说,确实相当方便,只需要这一个库,就能完成许多需求,而不用立刻去学习github上多种库的用法,例如retrofit, fresco,glide等。


如果你还不知道volley是什么,怎么用,可以看郭霖老师的系列文章:Android Volley完全解析(一),初识Volley的基本用法


我们知道,volley的图片加载一共有三种方式,ImageRequest,ImageLoader以及NetworkImageView。其中ImageLoader内部是通过ImageRequest来进行的,NetworkImageView使用的时候又必须传入一个ImageLoader参数。所以,这三者在使用上是一个比一个方便。一般来说,为了方便使用,我们直接用NetworkImageView就好了。

现在我有这样一个需求,在app的主界面完成用户登录并加载用户头像,效果如下图所示:

点击个人信息后,通过选择用户相册中的图片来修改用户头像:

单击保存后,把选中的图片头像发送给服务器,服务器返回修改成功后,我们通过刚才本地剪裁的结果,把图片显示在“个人信息”界面的头像上,如下图所示:

这样看起来修改成功了,但是主界面所在的activity并不知道你做了这一切,所以,当你通过finish()退回到主界面时,主界面的用户头像还是不会有任何变化。于是,我准备重写主界面的onStart()方法,于是变成了这样:


但是试验了一下,并没有什么卵用......于是我通过郭大神介绍volley的文章中得知,ImageLoader会自动过滤掉重复的URL,换句话说,为了减少网络请求次数,当缓存存在时,ImageLoader会自动取消掉与之前请求地址相同的网络请求。这就与我们这个需求相悖了,因为虽然请求地址相同,但是服务器返回的结果已经变化了,我们必须让主界面显示的图片更新才行。

这时候,我们就要通过阅读volley的源码来分析它的重复URL过滤机制。

虽然,郭大神的文章中说,过滤URL是ImageLoader做的,但是我们在使用NetworkImageView时,URL是通过NetworkImageView的setImageUrl(String url, ImageLoader imageloader方法传入的。所以,URL的入口是NetworkImageView,所以我们从NetworkImageView的源码看起。

首先,我们发现了这几行:

private String mUrl;
......
......
......
    public void setImageUrl(String url, ImageLoader imageLoader) {
        mUrl = url;
        mImageLoader = imageLoader;
        // The URL has potentially changed. See if we need to load it.
        loadImageIfNecessary(false);
    }


我们看到了setImageUrl的源码,可以了解到,请求地址传入以后,关于URL的任何操作,都是通过操作全局变量mUrl进行的。因此,我们可以只关注mUrl出现的地方,这将提高我们查阅源码的效率。往下翻阅时,看到一个方法,名叫loadImageIfNecessary,直接翻译成中文就是加载图片是否是必要的,通过名字就可以知道,这个方法的作用就是当需要加载图片时判断这个网络请求是否必要。这样一来,这个方法是线索之一,直接贴源码:

void loadImageIfNecessary(final boolean isInLayoutPass) {
        int width = getWidth();
        int height = getHeight();
        ScaleType scaleType = getScaleType();

        boolean wrapWidth = false, wrapHeight = false;
        if (getLayoutParams() != null) {
            wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT;
            wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT;
        }

        // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
        // view, hold off on loading the image.
        boolean isFullyWrapContent = wrapWidth && wrapHeight;
        if (width == 0 && height == 0 && !isFullyWrapContent) {
            return;
        }

        // if the URL to be loaded in this view is empty, cancel any old requests and clear the
        // currently loaded image.
        if (TextUtils.isEmpty(mUrl)) {
            if (mImageContainer != null) {
                mImageContainer.cancelRequest();
                mImageContainer = null;
            }
            setDefaultImageOrNull();
            return;
        }

        // if there was an old request in this view, check if it needs to be canceled.
        if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
            if (mImageContainer.getRequestUrl().equals(mUrl)) {
                // if the request is from the same URL, return.
                return;
            } else {
                // if there is a pre-existing request, cancel it if it's fetching a different URL.
                mImageContainer.cancelRequest();
                setDefaultImageOrNull();
            }
        }

        // Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens.
        int maxWidth = wrapWidth ? 0 : width;
        int maxHeight = wrapHeight ? 0 : height;

        // The pre-existing content of this view didn't match the current URL. Load the new image
        // from the network.
        ImageContainer newContainer = 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 this was an immediate response that was delivered inside of a layout
                        // pass do not set the image immediately as it will trigger a requestLayout
                        // inside of a layout. Instead, defer setting the image by posting back to
                        // the main thread.
                        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);

        // update the ImageContainer to be the new bitmap container.
        mImageContainer = newContainer;
    }

可以看到,源码总是这样,不仅又臭又长,中间时不时还夹杂几句英文注释,让我们这些英文水平不太好的小白看起来很是头疼,编码风格看起来非常高端,但是乍一看又让我们这些“外人”不知其所以然。 但是,为了找到问题,就要一行一行看下去!

可以看到,前面几行一看变量名就知道,是和自定义View有关的一些操作,毕竟NetworkImageView是直接继承自系统的ImageView,所以有这些代码很正常,但是似乎与我们今天的问题无关,所以接着往下看。

再往下看,我们看到一个变量名叫mImageContainer,它在一个if判断中如果不为空的话,就要调用一个方法:cancelRequest(),翻译过来的意思就是取消请求。这时,我们知道mImageContainer和取消请求是有关系的,于是我们把代码翻到最上面全局变量定义那里,查看一下mImageContainer的类型发现是ImageContainer,通过import语句可知它是ImageLoader的一个内部类,由此我们知道了,ImageLoader果然或多或少的参与了请求是否取消的这个过程。不然,ImageContainer不会定义在ImageLoader内。但是,这一小段代码并没有提及mUrl,我们知道,过滤请求一定和mUrl有关,所以我们先记住这个地方,接着往下看。

我们看到了对本篇文章所讲内容至关重要的一句代码:if (mImageContainer.getRequestUrl().equals(mUrl)) { return; }。我们看到,当if中的条件成立时,直接return了,而且这句if是在判断两个String是否是一样的,其中一个String就是mUrl,于是我们知道了,mImageContainer.getRequestUrl()返回的是之前请求的URL。这时我们就应该去ImageLoader中找ImageContainer的getRequestUrl()方法。我们看到如下几句代码:

private final String mRequestUrl;
......
......
......
public ImageContainer(Bitmap bitmap, String requestUrl,
                String cacheKey, ImageListener listener) {
            mBitmap = bitmap;
            mRequestUrl = requestUrl;
            mCacheKey = cacheKey;
            mListener = listener;
        }
......
......
......
public String getRequestUrl() {
            return mRequestUrl;
        }

我们知道了,ImageContainer在初始化的时候mRequestUrl被传入的参数赋值。并在调用getRequestUrl()的时候被返回。我们不妨可以回去查看NetworkImageView中mImageContainer的初始化,虽然它没有直接用构造方法,它是用的ImageLoader的get方法完成的赋值。但是我们可以通过查阅相应源码(这里的源码我就懒得贴了)发现mRequestUrl就是指的是NetworkImageView发起第一次请求的请求地址。如果我们要让这个NetworkImageView再次调用setImageUrl方法时能成功发起请求,我们就要在调用前,把mRequestUrl清空。于是我们把它的final修饰符去掉(去掉后会不会对volley内其他地方造成影响或产生不安全的因素,我还没有研究过)。然后在ImageContainer中添加一个方法:
//2016年9月14日修改
        public void clearURL() {
            mRequestUrl = null;
        }
然后在NetworkImageView中再添加一个方法:

    //2016年9月14日修改
    public void updateImage() {
        if (mImageContainer != null) {
            mImageContainer.clearURL();
        }

加一个不为空判断可以更安全。

现在我们在之前的主界面(看源码看了这么久是不是都要忘了主界面是谁了)的onStart()中加上这样一句,于是变成了这样:

    @Override
    protected void onStart() {
        super.onStart();
        preferences = getSharedPreferences("UserFile", MODE_PRIVATE);
        if (preferences.getInt("uid", 0) != 0) {
            headPortrait.updateImage();
            loadHeadPortrait();
        }
    }
再次运行程序,修改头像,再返回主界面,问题完美解决了!

总结:从我这个需求来说,我和服务器程序员(我同学)解决用户头像这个问题的处理方式也许并不聪明,因为加载一张图片消耗的流量和带宽以及对服务器造成的压力肯定是相对较大的,不知道一般在公司中的项目是否会在每次加载前先做个判定,看本地磁盘缓存的图片和服务器上保存的用户头像图片是否一致,不一致才发起请求。这样不知道能不能变的稍微更高效一点,这也是我的一个疑问。其次就是分享一下看源码的经验,最初看源码的时候确实比较痛苦,因为不知道该从哪里开始,或者怎么看,我的经验是,如果你要弄清一个库到底是怎样运作的,应该看的是它的整体结构和设计思想,从宏观的角度来看,不要纠结于每一行代码是什么意思,但是如果想要了解的是这个库的某一个小块是如何实现的,那就应该通过这个库的使用方法入手,通过暴露给用户的API深入到源码内部,观察它的细节;变量名,方法名以及注释都是库的作者提供给我们的线索,可以让我们快速找到我们要了解的那部分源码细节。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值