Android网络请求库 - Say hello to Volley

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ghost_Programmer/article/details/52333877


书接上篇 《Android网络请求库 - Say hello to OkHttp》,今天接着来简单的看一下常用的网络请求库中的第二种库:Volley。

  • Volley是谷歌2013年在I/O大会期间推出的网络库。开发Volley是因为在Android SDK中缺乏一个用户体验良好的网络加载类。
  • Volley自身的特点在于:适合去进行数据量不大,但通信频繁的网络操作;而对于大数据量的网络操作,例如上传/下载文件,其表现就不怎么给力了。

好的,这里就不多说废话了。下面马上正式走进关于Volley这个库的使用。

导入Volley

有趣的一点是,与其它很多常见的开源库的导入不同,Volley自身是没有提供官方的Maven或者说Jcenter Repository的。
也就是说,如果我们想要使用这个库,通常来说必须依赖于官方提供的源码。所以我们首先需要把它的代码clone到本地:

git clone https://android.googlesource.com/platform/frameworks/volley 

之前可能更多时候是将代码clone后然后打包成jar文件进行导入使用,但随着Volley的发展以及AndroidStudio的普及。我们现在更好的做法是:
将下载好的源码作为一个module导入。在Android Studio中选择File > Import Module,在对应目录下找到下载好的Volley项目,选择好后点击确认。
之后一个名为Volley的文件夹将出现在项目结构中。AS会自动的更新settings.gradle文件以包含Volley module,因此我们只需要在自己的项目添加好依赖compile project(‘:volley’)就搞定了。

但或多或少因为这样那样的原因,例如大天朝的“高墙”或者说作为一个懒人,我们可能不会太喜欢这种方式。没关系,也有另一种更简单的方法:

compile 'com.mcxiaoke.volley:library:1.0.19'

(更多相关信息可以参考:http://mvnrepository.com/artifact/com.mcxiaoke.volley/libraryhttps://github.com/mcxiaoke/android-volley

很显然通过这种方式引入Volley要方便很多,但缺陷在于:前面也说到了这不是官方提供的repository,也就是说它的未来是不可预知的,这点很致命。
与此同时,另一个缺陷在于:对于开源库来说其实最大的优势就是在于开源,这意味我们虽然是作为使用者,却仍然拥有最大的自由度。我们可以根据自己的需求最大限度的去自定义和扩展库,而使用这种方式则意味着可发挥的空间小了很多。

基本使用

HTTP GET

与之前学习OkHttp的思路保持一致,对于了解Volley的使用,我们同样从发起一个最基本的GET请求开始。

        // 第一步
        RequestQueue queue = Volley.newRequestQueue(this);
        // 第二步
        StringRequest request = new StringRequest(url, new Response.Listener<String>() {
            @Override
            public void onResponse(String response) {
                Log.d(TAG,response);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.d(TAG,error.toString());
            }
        });
        // 第三步
        queue.add(request);

依然是在本地使用tomcat搭建一个简单的servlet服务器,这次get请求如果成功,服务器将返回“get请求成功了,逗逼!”的信息。
好的,那么还有什么好等的呢?现在我们就运行程序来验证一下吧,最后发现得到如下图所示的输出结果:
这里写图片描述
所以我们可以确定我们已经成功的通过Volley发送了一条get请求到服务器了。简单的总结一下:

  • 从目前看来,对于Volley的使用相比于OkHttp更加简单,因为通常我们基本就使用两个东西:RequestQuene和Request。
    而与此同时,就从命名上来说,我们也可以很容易的理解它们各自的作用。显然二者分别代表请求队列以及请求。
    (这里的StringRequest其实就是基类Request的派生,顾名思义也就是针对于那些响应类型为文本的请求做了单独的封装。对应的常见的请求类型有三种:StringRequest、ImageRequest、JsonRequest
  • 接着,我们会注意到队列这个东西,相信Queue这个命名会条件反射一样的让我们与“多个数量”联系起来。
    实际上事实也是如此,RequestQueue会将多个HTTP请求缓存进队列,然后按照一定的算法分别去执行这些请求。
    所以,通常我们很显然没有必要将这个东西在每次需要发起请求时都去new一次,可以将RequestQueue设计为单例来节约资源。
  • 同时我们可以看到在Volley中发起一个请求的操作还是比较简单,只需要如上面示例的代码中的三步操作即可。
    简单的概括来说,就是得到RequestQueue与Request对象,然后将request添加到请求队列中就可以了。

HTTP POST

看完了GET请求的使用,自然就轮到了POST请求了。POST请求在Volley中也很容易实现。以之前的StringRequest来说:
其构造器提供了两种方式,我们之前使用了一种。而另一种多了一个int型的参数,这个参数就是来区分请求方法的。

StringRequest request = new StringRequest(Request.Method.POST,url, new Response.Listener<String>() {

这里写图片描述
我们发现,我靠?这么简单。当然不是,因为实际来说肯定不会出现如此基础的POST请求。例如我们通常起码会在POST请求中上传一些参数值吧。
在Volley中我们该如何去做呢?我们当然会从StringRequest入手,但最后会发现该类暴露出的实例方法基本全是一些get相关的方法,没有我们期望的。
这个时候实际上是挺让人蛋疼的,那既然Volley原本没有提供给我们现成的东西,那我们就只能自己动手了。其实也不复杂,覆写getParams方法吧。

        StringRequest request = new StringRequest(Request.Method.POST, url, new Response.Listener<String>() {
            @Override
            public void onResponse(String response) {
                Log.d(TAG, response);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.d(TAG, error.toString());
            }
        }) {
            @Override
            protected Map<String, String> getParams() throws AuthFailureError {
                Map<String, String> map = new HashMap<>();
                map.put("params1", "value1");
                map.put("params2", "value2");
                return map;
            }
        };

不知道你怎么想,反正个人是觉得Volley通过这样的方式来完成诸如之类的操作,使用起来其实还是有点蛋疼的。
但也无所谓吧,我们把之上的操作当做起步,有了这个基础,我们很容易举一反三。例如我们现在想要添加请求头信息:

            @Override
            public Map<String, String> getHeaders() throws AuthFailureError {
                Map<String, String> map = new HashMap<>();
                map.put(\"header1\", "呵呵");
                map.put("header2", "哈哈");
                return map;
            }

我们这里的重点在于掌握其套路,因为“开源“,类似功能的使用或者拓展在,在掌握套路的基础上结合研究源码通常就不难找到答案了。
所以,关于Volley中GET与POST的基本使用,我们只点到为止,接下来看一些更加有意思一点的东西。

彻底理解Volley中的乱码问题

回忆下,我们在此前的基本使用的介绍中,GET与POST请求的中文响应信息都成功的打印在了日志中。
而我相信我们都可能在自己使用时碰到过服务器返回的中文信息显示乱码的问题。这在之前看郭神写的介绍Volley的系列文章中也看到人提起过:

这里写图片描述
实际上正如这张图里的朋友说的,只要重写parseNetworkResponse就可以了。解决问题的方法不难找到,而我们这里的关注点放在出现问题的原因上。

好的,那么现在我们来看一下为什么在之前的例子中服务器响应中的中文信息能够成功输出呢?首先我们看服务器的实现:

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // 设置编码
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        // 输出响应
        OutputStream out = response.getOutputStream();
        out.write("get请求成功了,逗逼!".getBytes("UTF-8"));
        out.flush();
        out.close();
    }

显然我们做的工作是将服务器的响应信息设定了编码格式,这里是UTF-8,而为什么这样做Volley就不会出现读取乱码了呢?
答案实际上就在之前说到的parseNetworkResponse方法中,让我们打开StringRequest类中这个方法的源码看一下:

    protected Response<String> parseNetworkResponse(NetworkResponse response) {
        String parsed;
        try {
            parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
        } catch (UnsupportedEncodingException e) {
            parsed = new String(response.data);
        }
        return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
    }

我们发现这个方法的逻辑实际上是十分简单的,很快我们的关注焦点就放在了另一个方法HttpHeaderParser.parseCharset上,同样的打开它的源码:

    public static String parseCharset(Map<String, String> headers, String defaultCharset) {
        String contentType = headers.get(HTTP.CONTENT_TYPE);
        if (contentType != null) {
            String[] params = contentType.split(";");
            for (int i = 1; i < params.length; i++) {
                String[] pair = params[i].trim().split("=");
                if (pair.length == 2) {
                    if (pair[0].equals("charset")) {
                        return pair[1];
                    }
                }
            }
        }

        return defaultCharset;
    }

可以看到这个方法的实现其实也很简单,实际就是在解析我们之前在服务器的response中设置的Content-Type,最终得到设置的charset,即编码格式。
当该方法完成解析,得到我们服务器设置在response header中的charset信息之后,通过new String(byte[],”UTF-8”)就让我们成功的避免了乱码。

为了让我们加深对这种错误的印象,我们可以再逆向的思维一下,假设我们在服务器中没有设置编码格式:

        OutputStream out = response.getOutputStream();
        out.write("get请求成功了,逗逼!".getBytes());
        out.flush();
        out.close();

这个时候,我们再次运行程序就会发现得到的响应信息出现了乱码,这个时候我们该如何去解决呢?其实了解了原理就很简单了:

            parsed = new String(response.data, "gb2312");
            //parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));

我们所做的只是简单修改了parseNetworkResponse中的一行代码。那么为什么这里我们修改成“gb2312”就可以了呢?因为response.getOutputStream()的默认编码格式就是gb2312。所以说穿了避免中文乱码的关键很简单,就是和服务器统一编码格式。

接下来,我们再来看另一种情况,我们改用PrintWriter来返回响应信息:

        PrintWriter out = response.getWriter();
        out.write("get请求成功了,逗逼!");
        out.flush();
        out.close();

这个时候情况其实更有意思一点,因为这个时候我们如果不明确的指定编码格式,那么乱码问题是无解的。为什么这么说呢?我们分析一下:
response.getWriter()的默认编码格式是什么呢?是ISO-8859-1。而StringRequest的默认解码格式呢?答案同样是ISO-8859-1。参照以下源码:

    public static String parseCharset(Map<String, String> headers) {
        return parseCharset(headers, HTTP.DEFAULT_CONTENT_CHARSET);
    }
    // ================================================
    public static final String DEFAULT_CONTENT_CHARSET = "ISO-8859-1";

那么,现在的情况乍看一下是:默认情况下服务器和客户端的编码格式实际上是统一的,那为什么还会出现乱码,甚至还说这时的乱码是无解的呢?因为我们说的是中文乱码。ISO-8859-1的编码格式与中文响应,你懂的!

不要说,越深入越带劲了嘿!那我们之前说的POST请求上传参数时,会不会出现乱码呢?当然也是可能出现的。但为什么我们之前没有出现乱码呢?
在源码中找答案,这个答案并不神秘,就在基类Request当中,首先是如下代码:

    /**
     * Returns the content type of the POST or PUT body.
     */
    public String getBodyContentType() {
        return "application/x-www-form-urlencoded; charset=" + getParamsEncoding();
    }

这里首先解释了,为什么我们之前只重写了getParams方法就可以上传参数了,因为Request类里默认为我们指明了Content-Type为表单形式。
与此同时,我们注意到:getParamsEncoding()方法为Content-Type指明了编码格式,打开该方法的源码吧:

    protected String getParamsEncoding() {
        return DEFAULT_PARAMS_ENCODING;
    }

    private static final String DEFAULT_PARAMS_ENCODING = "UTF-8";

话到这里,其实就很清楚了。关于Volley中的乱码相关的东西,就总结到这里吧。

其它两种Request

我们之前知道了在Volley中Request是对应于请求的基类,而常用的Request类型有三种,除了StringRequest,还有JsonRequest与ImageRequest。
其实这种情况实际只是Volley针对于不同的响应数据类型单独做了最合适的封装,这也是为什么之前说Volley相对于OkHttp的优势在于封装度更高。
因为原理是相通的,所以其实对于另外两种的Request的使用方式其实也是类似的,不过我们也可以再看一下。

JsonRequest

JsonRequest顾名思义,就是说服务器响应的数据类型是JSON格式的,而对于其使用,与StringRequest是很相似的:

        JsonObjectRequest request = new JsonObjectRequest(url, new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {

            }
        },null);

可以看到与StringRequest最大的不同就是Listener的泛型变为了JSONObject,进而onResponse返回的也就直接是JSONObject了。
而对于JSON数组来说,使用同样简单,把这里的JsonObjectRequest改为使用JsonArrayRequest就可以了。

ImageRequest

从名字我们可以看到这个类显然就是为了图片专门封装的,相比StringRequest和JsonRequest稍有不同了。(JSON就只是指定了格式的文字而已)
但也是由于Volley本身封装的比较好,所以虽然不再是响应文字而是响应图片,但使用起来也很简单:

        ImageRequest request = new ImageRequest(url, new Response.Listener<Bitmap>() {
            @Override
            public void onResponse(Bitmap response) {
                imageView.setImageBitmap(response);
            }
        },maxWidth,maxHeight,scaleType,decodeConfig,errListener);

我们发现ImageRequest接收的参数比较多,但归根到底这些多出来的参数实际也就是为了设置图片的相关属性而存在的。

  • maxWidth //图片最大宽度
  • maxHeight//图片最大高度
  • scaleType //指定图片缩放类型
  • decodeConfig//指定图片的颜色属性

对于图片的下载显示,Volley还提供了一个相较ImageRequest更加强力一点的东东ImageLoader,关于其使用其实也不复杂,但毕竟Volley并不是一个将精力聚焦在图片加载的库,所以关于ImageLoader如果有兴趣我们可以自己另行研究。

定制自己的Request

前面我们了解了Volley自身提供了三种常用的Request类型,但同时我们也可以根据自定的需要定制自己的Request类型。
在Volley中去自定义自己的Request其实并不复杂,以StringRequest来说,读一下其源码,就会发现逻辑是很简练清晰的。
我们可以以此为参考,根据自己的需要进行修改,很容易就能写出自己需要的Request。而以Http请求来说:我们除了上面三种之外,还有XML格式。
而Volley自身提供的对于JSON格式的数据的响应是基于JSONObject(Array)的,很多时候我们还是喜欢GSON和fastJson。
所以我们就可以去定制基于XML或者GSON的Request类型,而对于这种需求,郭神很早之前的文章《 Android Volley完全解析(三),定制自己的Request》就有了很精彩的讲解,我们借以学习就可以了。

另一些常用的操作

取消请求

取消请求是比较简单,根据不同的需要有以下几种方式:

        //第一种
        request.setTag("tag");
        queue.cancelAll("tag");
        //第二种
        queue.cancelAll(new RequestQueue.RequestFilter() {
            @Override
            public boolean apply(Request<?> request) {
                //通过该返回结果决定该request是否需要终止
                return true;
            }
        });
        //第三种
        request.cancel();

Request优先级

我们知道我们在使用Java的线程的时候,可以通过setPriority来设置线程优先级。而对于Volley中的Request,也可以管理它们的优先级。
不同之处在于,Request类本身没有提供setPriority,而只是提供了getPriority方法而已:

    public Priority getPriority() {
        return Priority.NORMAL;
    }

但没关系,知道了原理我们就可以自己加以修改,从而自己提供一个setPriority的入口,类似下面这样重写Request类:

Priority mPriority;

public void setPriority(Priority priority) {
    mPriority = priority;
}

@Override
public Priority getPriority() {
    // If you didn't use the setPriority method,
    // the priority is automatically set to NORMAL
    return mPriority != null ? mPriority : Priority.NORMAL;
}

Priority是定义在Request类内部的一个枚举类型,它有以下几种取值可能:

    public enum Priority {
        LOW,
        NORMAL,
        HIGH,
        IMMEDIATE
    }

宏观的看Volley的架构

到了现在,我们已经对Volley这个库的使用有了一定程度的认识。现在我们通过Volley的源码从更宏观的角度来看一下其原理,加深我们的理解。

回忆一下,我们在使用Volley发起一个请求时所需的步骤。显然首先我们需要一个RequestQueue对象:

RequestQueue queue = Volley.newRequestQueue(this);

我们就从这行代码作为起点,看一下它的源码实现是如何的,这行代码最后将进入如下调用:

    public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) {
        /*
         * 第一部分
         * 缓存文件相关信息的初始化设置
         */
        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);

        String userAgent = "volley/0";
        try {
            String packageName = context.getPackageName();
            PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
            userAgent = packageName + "/" + info.versionCode;
        } catch (NameNotFoundException e) {
        }

        /*
         * 第二部分
         * 实例化HttpStack对象,这里有一个版本判断。
         * 如果版本高于9,则创建的是HurlStack对象,其内部是通过HttpUrlConnection实现的。
         * 而如果版本低于9,则创建的是HttpClientStack对象,其内部是通过HttpClient实现的。
         */
        if (stack == null) {
            if (Build.VERSION.SDK_INT >= 9) {
                stack = new HurlStack();
            } else {
                // Prior to Gingerbread, HttpUrlConnection was unreliable.
                // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
                stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
            }
        }

        /* 第三部分
         * 创建Network对象,并进行RequestQueue的实例化工作。
         * (BasicNetwork实际就是根据传入的HttpStack在内部处理网络请求的东西)
         * (maxDiskCacheBytes是指定磁盘缓存的最大容量,指定为-1时将使用默认大小'5 * 1024 * 1024')。
         *
         */
        Network network = new BasicNetwork(stack);

        RequestQueue queue;
        if (maxDiskCacheBytes <= -1)
        {
            // No maximum size specified
            queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
        }
        else
        {
            // Disk cache size specified
            queue = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network);
        }

        /*
         * 让RequestQueue开始工作
         */
        queue.start();

        return queue;
    }

我们对以上的代码所住的几部分主要工作做了划分,并加以注释进行了说明。可以发现逻辑其实还是很清楚的,现在我们下一步的注意力放在queue.start():

    public void start() {
        stop();  // Make sure any currently running dispatchers are stopped.
        // Create the cache dispatcher and start it.
        mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
        mCacheDispatcher.start();

        // Create network dispatchers (and corresponding threads) up to the pool size.
        for (int i = 0; i < mDispatchers.length; i++) {
            NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
                    mCache, mDelivery);
            mDispatchers[i] = networkDispatcher;
            networkDispatcher.start();
        }
    }

我们发现start方法的源码其实也不复杂,最关键的两个东西分别是:CacheDispatcher和NetworkDispatcher,它们其实都继承自Thread。
而从命名,我们就可以发现它们分别对应着缓存线程与网络线程,而我们发现网络线程包含于一个for循环当中,mDispatchers.length的默认值是4。
这意味着当RequestQueue执行start过后,默认情况下我们的应用中就会有1个缓存线程及4个网络线程待命进入工作,等待着执行我们的请求。

到了这里,我们就得到了RequestQueue对象了。下一部的工作我们也很熟悉,就是将request对象add进行队列。那么啥也别说了,看add方法的源码吧:

 public <T> Request<T> add(Request<T> request) {
        // 标记属于此队列的请求,并将其添加到当前请求的集合中。
        request.setRequestQueue(this);
        synchronized (mCurrentRequests) {
            mCurrentRequests.add(request);
        }

        // 按添加顺序为其设置序列
        request.setSequence(getSequenceNumber());
        request.addMarker("add-to-queue");

        // 如果request不允许缓存,将其加入网络队列
        if (!request.shouldCache()) {
            mNetworkQueue.add(request);
            return request;
        }

        /*
         * 简单的来说就是指:如果已经有拥有相同cacheKey的请求存在于mWaitingRequests当中了,则将本次请求添加进stagedRequests当中
         */
        synchronized (mWaitingRequests) {
            String cacheKey = request.getCacheKey();
            if (mWaitingRequests.containsKey(cacheKey)) {
                // There is already a request in flight. Queue up.
                Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
                if (stagedRequests == null) {
                    stagedRequests = new LinkedList<Request<?>>();
                }
                stagedRequests.add(request);
                mWaitingRequests.put(cacheKey, stagedRequests);
                if (VolleyLog.DEBUG) {
                    VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
                }
            } else {
                // 否则以cacheKey为键,插入null对象作为值,并将本次请求加入缓存队列。
                mWaitingRequests.put(cacheKey, null);
                mCacheQueue.add(request);
            }
            return request;
        }
    }

我们发现,最终通常来说request就被加入到了缓存队列当中,那么就意味着要CacheDispatcher开始执行了,所以我们看看它的run()方法吧:

 @Override
    public void run() {
        if (DEBUG) VolleyLog.v("start new dispatcher");
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

        // 缓存的初始化工作(mCache的实际类型是DiskBasedCache)
        mCache.initialize();

        Request<?> request;
        while (true) {
            // release previous request object to avoid leaking request object when mQueue is drained.
            request = null;
            try {
                // 从缓存队列中获取request对象
                request = mCacheQueue.take();
            } catch (InterruptedException e) {
                // We may have been interrupted because it was time to quit.
                if (mQuit) {
                    return;
                }
                continue;
            }
            try {
                request.addMarker("cache-queue-take");

                // If the request has been canceled, don't bother dispatching it.
                if (request.isCanceled()) {
                    request.finish("cache-discard-canceled");
                    continue;
                }

                // Attempt to retrieve this item from cache.
                Cache.Entry entry = mCache.get(request.getCacheKey());
                if (entry == null) {
                    request.addMarker("cache-miss");
                    // 找不到缓存的话,则将请求加入到网络队列中去执行
                    mNetworkQueue.put(request);
                    continue;
                }

                // 缓存过期了的话,同样加入到网络队列中去执行
                if (entry.isExpired()) {
                    request.addMarker("cache-hit-expired");
                    request.setCacheEntry(entry);
                    mNetworkQueue.put(request);
                    continue;
                }

                // 否则就代表缓存可以使用,直接从缓存读取数据
                request.addMarker("cache-hit");
                Response<?> response = request.parseNetworkResponse(
                        new NetworkResponse(entry.data, entry.responseHeaders));
                request.addMarker("cache-hit-parsed");

                if (!entry.refreshNeeded()) {
                    // 缓存不需要更新,将从缓存中读取的response通过mDelivery发送回去
                    mDelivery.postResponse(request, response);
                } else {
                    // 否则虽然我们仍然可以使用缓存中读取到的响应返回
                    // 但同时还要进行缓存的更新工作。
                    request.addMarker("cache-hit-refresh-needed");
                    request.setCacheEntry(entry);

                    // Mark the response as intermediate.
                    response.intermediate = true;

                    // Post the intermediate response back to the user and have
                    // the delivery then forward the request along to the network.
                    final Request<?> finalRequest = request;
                    mDelivery.postResponse(request, response, new Runnable() {
                        @Override
                        public void run() {
                            try {
                                mNetworkQueue.put(finalRequest);
                            } catch (InterruptedException e) {
                                // Not much we can do about this.
                            }
                        }
                    });
                }
            } catch (Exception e) {
                VolleyLog.e(e, "Unhandled exception %s", e.toString());
            }
        }
    }

通过对以上代码的理解我们可以知道,假设我们第一次发送某个请求,在缓存线程中执行后,当然会因为没有缓存而被加入到网络线程。
所以接下来我们要关注的当然就是NetworkDispatcher的run方法,而通过查看源码我们会发现其代码与CacheDispatcher有些相似的,不同在于:
NetworkDispatcher判断Request可以执行后,就会调用mNetwork.performRequest(request);开始实际执行HTTP请求。
关于performRequest的源码,有兴趣的可以自己查看,其实际实现位于BasicNetwork当中。代码很长,由于篇幅就不贴出了。
而当网络线程与服务器完成通信,就会得到响应(networkResponse);这之后做的工作很简单也很重要,大致分为三步:

  • 通过Response< ? > response = request.parseNetworkResponse(networkResponse);将读取到的网络相应于根据响应类型进行具体转换。
  • 正常情况下,还会通过mCache.put(request.getCacheKey(), response.cacheEntry);将本次响应进行缓存。
  • 最后调用mDelivery.postResponse(request, response);将响应传回给客户端。

相信到了这里,我们就在宏观的角度上对于Volley的整个结构有了一个基本的认识了。

为什么没法使用缓存

本来已经准备结束了,突然想起关于Volley中文本类型(包括JSON等)的数据的缓存其实也值得一说。
我们来回忆一下,Volley的缓存机制是怎么样的。其实分别有几个关键点。我们首先回一下RequestQueue类里add方法的几行代码:

        if (!request.shouldCache()) {
            mNetworkQueue.add(request);
            return request;
        }

这个判断的含义告诉我们说,如果request.shouldCache()返回为false,即代表不需要缓存。则会将此次请求直接加入网络队列执行。
否则的话,一个请求通常最后都会被加入到mCacheQueue即缓存队列中执行,而request.shouldCache()的默认返回值为true,即代表默认支持缓存。

接着,当request加入到缓存队列开始执行后,如果因为此时还没有缓存,就会加入到网络队列中执行,而得到响应后,会通过如下代码保存缓存:

                if (request.shouldCache() && response.cacheEntry != null) {
                    Log.d("TestVolley","put in cache");
                    mCache.put(request.getCacheKey(), response.cacheEntry);
                    request.addMarker("network-cache-written");
                }

这个判断默认情况下也是会通过的,也就是说默认情况下我们的响应都是会进行缓存的。

而在回忆一下,我们同样记得,在CacheDispatcher中,是会通过如下代码去获取缓存的:

 Cache.Entry entry = mCache.get(request.getCacheKey());

我们可以自己去写代码测试一下,就会很容易的发现当一次请求成功后。再次发起相同请求时,entry的获取肯定是不会null的,但问题在于:
此次发起的请求却不是读取缓存,而是再次通过网络线程去发起新的请求。这是为什么呢?其实已经不难猜到答案了。问题多半出在如下判断:

if (entry.isExpired()) {

我们可以看一下该方法的源码是如何实现的:

        public boolean isExpired() {
            return this.ttl < System.currentTimeMillis();
        }

从而我们会发现一个关键的信息叫做ttl,当我们继续深入去查看这个ttl,会发现默认情况下它是为0的。也就是说,默认情况下isExpired的返回结果肯定是为true的。
这也就是为什么我们其实有缓存,却没法使用缓存。那么怎么样才能让这个值不为0呢?很简单,这个值的计算是与HTTP的一个header属性,也就是“Cache-Control”相关的。

现在我们在服务器的加上类似代码:

response.setHeader("Cache-Control", "max-age=3600");

OK,搞定收工。有了这行代码,当我们再次去发起该请求的时候,只要没有超过max-age指定的时间,我们就可以复用缓存中的响应,避免新的请求了。

展开阅读全文

没有更多推荐了,返回首页