Android网络请求库 - Say hello to Volley

Android 网络请求库Retrofit使用详解 1、首先在Android项目中引入需要的依赖。2、创建一个单例类,返回Retrofit对象。 阅读详情


书接上篇 《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/library和https://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指定的时间,我们就可以复用缓存中的响应,避免新的请求了。

android Http请求库,用于整合所有请求与返回 该http请求库,用于将所有的http请求和返回参数整合到一起,方便以后进行调用,和更改。 立即下载

相关推荐

android Http请求库,整合了所有的请求与返回

该http请求库,用于将所有的http请求和返回参数整合到一起,方便以后进行调用,和更改。 大家可以在我的博客查看详细使用方式 http://blog.youkuaiyun.com/stanny_bing/article/details/52485854

Android网络请求库的使用(okhttp、retrofit、rxjava)

首先新建项目,添加网络权限,这个权限不需要⽤户授权, 默认申请就给, 不添加的话会报错: 然后在gradle中添加一下依赖: dependencies { implementation 'com.squareup.okhttp3:okhttp:4.9.1' implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0

Xzike的博客 3059

Deep Reinforcement Learning - 1. DDPG原理和算法

Deep Reinforcement Learning - 1. DDPG原理和算法 背景描述 DDPG的定义和应用场景 DDPG算法相关基本概念定义 DDPG实现框架和算法 DDPG对于DPG的关键改进 下一篇 以下用RL作为Reinforcement Learning 的简称。背景描述概括来说,RL要解决的问

kenneth_yu的博客 14万+

那些年我用过的Android网络请求库

下面是我使用过的网络框架,每种框架都有自己的优点缺点,要更具自身的应用场景选择。 测试使用一个ip地址查询接口进行测试:http://ip.tianqiapi.com?ip=xxx.xxx.xxx.xxx。 在Android中请求网络时要注意几点问题: 1.权限 :<uses-permission android:name="android.permission.INTERNET"/>...

HouXinLin_优快云的博客 739

《计算机网络》复试问答题总结

DNS 是一个联机分布式数据库系统,负责主机名和 IP 地址之间的转换,需要进行域名查询的机器主动发起域名解析请求,域名服务器则随时准备做出响应。域名服务器的数据库中存放着它所管辖范围的主机名和 IP 地址之间的映射表,域名服务器之间又可以相互联络和协作,以便分布在 Internet 各个域名服务器数据库中的域名都能被有效地搜索,从而实现主机名与 IP 地址的映射。为了提高解析效率,使用了域名缓存技术。在服务器、主机中设置一个专用的内存缓冲区。服务器用来存放近期解析过的域名及其对应的 IP 地址的映射。

weixin_45971154的博客 2027

dbt+Snowflake实战:数据建模、权限隔离与性能优化全指南

dbt(data build tool)作为现代数据栈的核心建模工具,其与Snowflake的深度协同远不止于SQL编译执行——它本质是将软件工程实践(如版本控制、测试驱动、CI/CD)注入数据交付流程。理解dbt的声明式建模原理与Snowflake的虚拟仓库、Time Travel、TRANSIENT表、聚簇键等底层机制如何咬合,是实现可维护、可审计、低成本数据产品交付的关键。本文聚焦dbt在Snowflake环境中的真实落地挑战,涵盖schema与warehouse的物理映射设计、role级硬隔离策略、

weixin_34389926的博客 312

Android网络请求库

1.Retrofit2请求库 添加依赖: compile 'com.squareup.retrofit2:retrofit:2.0.2' compile 'com.squareup.retrofit2:converter-gson:2.0.2' compile 'com.squareup.okhttp3:okhttp:3.5.0' 定义接口,该接口用来定义接受的数据类型与请求的参数: publi...

溯夜,逆流而上 631

反无人机技术实战解析:从探测到拦截的全方位防御策略

本文深入解析了反无人机(C-UAS)技术的全链条防御策略。从雷达、无线电频谱、光电等多维度探测技术,到数据融合与威胁评估,再到无线电干扰、导航诱骗及物理拦截等核心拦截手段,系统阐述了如何构建从探测到拦截的实战化防御体系,为关键设施、大型活动等场景提供定制化空域安全解决方案。

weixin_29230649的博客 401

Shine——更简单的Android网络请求库封装

作者:FreddyChen 写在前面 距离上一篇文章跟我一起开发商业级IM(3)—— 长连接稳定性之连接及重连发布的时间,大概已有一年多,先跟大家说声抱歉。主要是因为工作太忙,业务需求过多,没办法专心写博客。先立个Flag:IM系列文章一定会坚持写完,同时Github项目也会逐步完善,敬请期待。 这次就暂不更新IM系列相关的文章及项目了,先给大家带来一个稍微轻量级同时也比较实用的网络请求封装库:Shine,同时也希望自己借此机会重新拾起写博客和开源项目的激情,废话少说,我们直接开始吧。 Shine是什么.

697

Modbus协议学习第一篇之基础概念

因工作需要而接触Modbus,正在恶补知识,利用博客的方式来加深理解和记忆,同时希望同行大佬能一起交流。

hzf978742221的博客 880

SAP 系统中的数字签名解决方案及应用案例分析

在当前企业信息化及数字化转型的大背景下,数据安全与身份认证显得尤为重要。数字签名技术作为保障数据真实性和完整性的重要手段,在 SAP 系统中得到了广泛应用。本文将详细探讨 SAP 系统中部分应用场景如何利用数字签名技术满足业务需求,涵盖 SAP Single Sign-On、质量管理、过程工业生产计划以及 SAP ArchiveLink 内容服务器等方面,力求通过深入解读,帮助读者更好地理解和应用这一关键技术。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 526

Android 网络请求库Retrofit简单使用

载请标明出处: http://blog.youkuaiyun.com/u011974987/article/details/50895633; 什么是 Retrofit ?Retrofit 是一套 RESTful 架构的 Android(Java) 客户端实现,基于注解,提供 JSON to POJO(Plain Ordinary Java Object ,简单 Java 对象),POJO to JSON,网

stromxu 的专栏 8654

Python 爬虫实战:于好大夫在线抓取医生评价数据,选择优质医疗服务

requests:用于发送 HTTP 请求,获取网页内容。或 lxml:用于解析 HTML 页面,提取目标数据。pandas:用于数据清洗和分析。matplotlib和seaborn:用于数据可视化。time:用于控制爬取速度,避免对服务器造成压力。通过本项目,我们实现了以下目标:使用 Python 爬虫抓取好大夫在线的医生评价数据。对数据进行清洗和分析,找出评价中的关键词和趋势。根据分析结果推荐优质医疗服务。

u014481728的博客 3369

Android 各大网络请求库的比较及实战

自己学习android也有一段时间了,在实际开发中,频繁的接触网络请求,而网络请求的方式很多,最常见的那么几个也就那么几个。本篇文章对常见的网络请求库进行一个总结。HttpUrlConnection最开始学android的时候用的网络请求是HttpUrlConnection,当时很多东西还不知道,但是在android 2.2及以下版本中HttpUrlConnection存在着一些bug

区长的专栏 1万+

[HQChart教程] 去掉“仅学习使用”文字提示的小技巧

最近用 HQChart 做图表时,发现左下角有“仅学习使用”的字样。这种水印虽然是默认的,但如果要展示给客户,可能会显得不够专业。还好,这个问题很好解决,只需要简单修改一下配置。

代码简单说 Vue、JAVA、PHP、Node.js 熟练运用,接口、架构、性能全搞定。 598

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

android-async-http AsyncHttpClient介绍

当我第一眼看到它时就被吸引住了,特别是async关键字,干我们这行的都知道,这是异步执行,也就是说它的网络请求自动在非UI   线程里执行,你不需要任何额外的操作(比如手动new一个Thread之类)。项目的官方网站:   http://loopj.com/android-async-http/,对应的github地址:https://github.com/loopj/an

u010259764的专栏 293

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

Android网络请求库 - Say hello to retrofit

回顾: Android网络请求库 - Say hello to OkHttp Android网络请求库 - Say hello to Volley 之前对Android里常用的网络请求库OkHttp与Volley做了简单的学习归纳与总结,这里看这个系列中的最后一篇,来认识一下Retrofit。 Retrofit可以认为是OkHttp的“升级版”。之所以这么说,是因为其内部默认正

LaterEqualsNever 9683

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

Android开源项目推荐之「网络请求哪家强」

不少人老催这个系列,好吧,今天就更新一篇干货给你们。网络请求这个话题基本是所有 App 开发都会遇到的,这也难怪之前很多人留言让我写写网络请求到底该怎么选择,今天就来说说网络请求到底哪家强! 1. 原则本篇说的网络请求专指 http 请求,在选择一个框架之前,我个人有个习惯,就是我喜欢选择专注的库,其实在软件设计领域有一个原则叫做 「单一职责原则」,跟我所说的「专注」不谋而合,一个库能把一件事

stormzhang的专栏 5339

android理解http请求头,7.1.2 Android Http请求头与响应头的学习.pdf

7.1.2 Android Http请求头与响应头的学习7.1.2 Android Http请求头与响应头的学习本节引⾔:上节中我们对Android涉及的⽹络编程进⾏了了解,也学习了下Http 的基本概念,⽽本节我们 要学习的是Http 的请求 与响应 ,当然,可以把也可以把这节看作⽂档,⽤到的时候来查查 即可 !1.HTTP请求之消息头:这⾥贴下上⼀节给出的图,根据下⾯给出的表,⼤家⾃⼰感受下...

weixin_32803887的博客 449

解决Volley接收数据、中文显示乱码问题。

Volley接收数据后,若收到的数据header内无charset定义,则使用其默认的"ISO-8859-1"格式进行解析,导致乱码出现。 解决方法: 1. 自定义request,重写parseNetworkResponse方法: private class mJsonRequest extends JsonObjectRequest { /**

yangbiyao的专栏 4259

动手写一个Android数据加载时界面状态切换的开源库

文章伊始,让我们先静心回忆三秒:在我们写过的Android应用当中,是不是有很多地方都离不开数据加载的需求呢?如果是,那么我们首先来看下图:好的,从这里开始我们暂时忘记自己是一个安卓开发者,而是以一个不懂技术的APP使用者的身份来继续接下来的交流。如果是作为一个使用者,那么现在我们的内心应该是懵逼的。因为自打我们打开这个应用进入到第一个界面后,就发现没有任何内容。 这个时我们可能会开始推测:什么鬼

LaterEqualsNever 8756

Android网络请求库 - Say hello to OkHttp

引言 网络请求是Android开发中必不可少的一块,通常我们可以用原生的HttpUrlConnection或者Apache封装的HttpClient来完成网络请求的实现。 如今来说,HttpClient在API23问世后也被废弃了。也就是说Google现在更推荐我们使用HttpUrlConnection来实现Http网络请求。 但以Http请求来说,大多数时候都是遵循一定的套

LaterEqualsNever 5332
上一篇: Android网络请求库 - Say hello to OkHttp
下一篇: Android网络请求库 - Say hello to retrofit
LaterEqualsNever
博客等级 码龄12年 297粉丝 61原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值