浅入浅出Android Volley

Volley is an HTTP library that makes networking for Android apps easier and, most importantly, faster.

源码地址: https://github.com/google/volley

1. 如何使用

已经有很多文章介绍了Volley,不再重复。下面是一些参考:

2. 设计思路

在volley中有三种类型的线程:

  • 主程/其他线程:主线程即UI线程。主程/其他线程负责将网络请求交给RequestQueue。
  • 网络线程:发起网络调用,得到返回数据。默认是4个线程。
  • 缓存线程:如果请求允许使用缓存,则缓存线程尝试去取缓存数据。

那么网络线程、缓存线程是从什么地方拿到的请求呢?答案是阻塞式队列

涉及到两个队列存储网络请求:

  • 网络线程从 PriorityBlockingQueue 类型的队列中取网络请求,称之为mNetworkQueue
  • 缓存线程也是 PriorityBlockingQueue 类型的队列中取网络请求,称之为mCacheQueue

而RequestQueue 本身不是队列,不过其内部主要逻辑是将请求对象放入mNetworkQueuemCacheQueue

这里有一个问题,两个相同的支持缓存的请求(请求1、请求2)几乎同时到来,那么其实没必要对请求2进行处理,只需要等请求1拿到响应数据后,把数据顺便给请求2即可。Volley中使用 Map<String, Queue<Request<?>>>记录有没有支持缓存的请求正在进行,称之为mWaitingRequests。其中key是请求的key(默认是url),value是请求对象的容器。

2.1 网络请求发起流程

请求发起流程:

  1. 主线程(其他线程也行)生成请求对象,对象内部包含了请求地址、请求方法、处理返回数据的回调对象等。
  2. 主线程将请求对象交给RequestQueue对象。以下是RequestQueue对象内部的处理逻辑:
    1. 判断该请求对象是否允许使用缓存,若不允许,则将请求放入mNetworkQueue流程结束。若允许缓存,进入下一步。
    2. 得到请求对象的key值(默认是网址Url),根据mWaitingRequests判断当前是否有相同key的网络请求正在进行。如果有,则将该请求对象先存到mWaitingRequests中key对应的容器中,等待已有的相同key的网络请求响应回来后,顺便把结果交给该请求。如果没有,则进入下一步:
    3. 将请求对象放入mCacheQueue,并记录当前有key为×××的请求正在进行处理。

2.2 缓存线程

缓存线程是一直运行,内部就是死循环,每一次循环从mCacheQueue拿出一个网络请求进行处理,处理流程如下:

  1. 根据网络请求的key,判断缓存中是否有已有内容,若没有内容、或者内容过期,则将网络请求扔到mNetworkQueue,让网络线程去处理。结束流程。
  2. 若有未过期的缓存,则将缓存内容作为响应内容。在主线程中执行网络请求中回调对象中的函数。

2.3 网络线程

类似的,网络线程是一直运行,内部就是死循环,每一次循环从mNetworkQueue拿出一个网络请求进行处理,处理流程如下:

  1. BasicNetwork对网络请求进行处理,得到响应数据。
  2. 如果该网络请求允许缓存且HTTP响应头也给出了允许缓存相关的信息,则缓存响应结果。
  3. 在主线程中执行该网络请求中回调对象中的函数。

2.4 缓存设计

缓存使用key-value的形式存取,key是从Request类中拿出来的,默认是网址。StringRequestJsonArrayRequest等网络请求类集成自Request。可以根据需要在自定义的Request子类中覆盖该方法。

public abstract class Request<T> implements Comparable<Request<T>> {
    // ...

    public String getCacheKey() {
        return getUrl();
    }

    // ...
}

而value是包含了多个属性,有响应头、响应体、ETag内容、Last-Modified内容、Expires内容等。

DiskBasedCache是默认的缓存实现,一个基于文件存储的cache,所有文件存在一个目录里。有以下几个有趣的设计:

  • 文件名并不是key。key是存在文件里面的。而文件名是基于key生成的具有(伪)1对1关系的名称。不是md5哈。真的冲突了也没关系,key存在文件里呢。

  • 初始化时,会将本地已有的缓存全部载入内存。但是内存中不会有响应体,如图片、html等。添加缓存时,既存入本地,也加入内存中。删除某条缓存,既删本地,也删内存中对应内容。根据key获取缓存时,先看内存中有没有,没有就直接返回null,有的话再从本地存储中把响应体拿出来。

  • 针对响应体,默认允许本地存储最大为5MB。当添加缓存时,会判断有没有超出限制,若超出,则先删除之前已有的部分缓存,直到满足下面的条件才添加新缓存:

本地缓存占用空间+新缓存要占用的空间 < 最大缓存大小*0.9

2.5 超时和重试

超时时间和重试次数的代码在Request类中已经实现:

public Request<?> setRetryPolicy(RetryPolicy retryPolicy) {
    mRetryPolicy = retryPolicy;
    return this;
}

public final int getTimeoutMs() {
    return mRetryPolicy.getCurrentTimeout(); // 单位是毫秒
}

public RetryPolicy getRetryPolicy() {
    return mRetryPolicy;
}

直接在Request子类中实现getTimeoutMs方法是不可能了,所以设置重试策略即可。

重试策略是一个接口,内容如下:

public interface RetryPolicy {

    int getCurrentTimeout();

    int getCurrentRetryCount();

    void retry(VolleyError error) throws VolleyError;
}

结束重试的方式是在retry方法在合适的时候抛出一个异常即可。

DefaultRetryPolicyRetryPolicy的默认实现,可以指定超时时间和重试次数,以及重试时超时时间如何变化。

2.6 图片自动处理

一张很大的图片直接在view中显示会耗费大量内存,甚至产生OOM(OutOfMemory)问题。一个有效的应对方法是imageview多大,就把图片先处理成相应大小,再在UI上展示。

volley提供了ImageRequest、ImageLoader、NetworkImageView类来处理图片,均使用了IMageRequestdoParse方法处理网络请求返回的图片,具体代码在这里

3. 代码防御

根据Android 网络通信框架Volley简介(Google IO 2013)的建议,在Activity onStop 时,cancelAll 所有请求,这样网络请求如果未请求,则不会发起请求,如果已经请求,返回数据后不会执行网络请求的回调。示例:

@Override pubic void onStop() {
    mRequestQueue.cancelAll(this);
    //...
}

4. 一些问题的解决方案

4.1 POST 不支持相同参数

POST 方法如果要带数据,需要覆盖getParams方法。下面是来自 Android Volley完全解析(一),初识Volley的基本用法的一段代码:

StringRequest stringRequest = new StringRequest(Method.POST, url,  listener, errorListener) {  
    @Override  
    protected Map<String, String> getParams() throws AuthFailureError {  
        Map<String, String> map = new HashMap<String, String>();  
        map.put("params1", "value1");  
        map.put("params2", "value2");  
        return map;  
    }  
};

数据的组装其实是在getBody方法里:

public byte[] getBody() throws AuthFailureError {
    Map<String, String> params = getParams();
    if (params != null && params.size() > 0) {
        return encodeParameters(params, getParamsEncoding());
    }
    return null;
}

然而,这种实现不支持类似a=1&a=1&b=123参数名多次出现的场景。

解决办法是,生成Request对象时覆盖getBody方法。

4.2 设置请求头

生成Request对象时,覆盖getHeaders方法即可。

public Map<String, String> getHeaders() throws AuthFailureError {
    return Collections.emptyMap();
}

4.3 图片上传问题

Volley没有图片上传的实用类/函数,解决办法有两个:

  1. 将图片转换为base64,以字符串的形式将图片POST到服务器端。
  2. 基于HTTP上传图片原理,在getBody里生成数据。可以参考这里:Android volley 解析(三)之文件上传篇