Volley设计与实现

Volley是google提供网络请求库,使用说明看这里。注:由于Android6.0把HttpClient去掉了,可能会编译不过,这时需要替换里面的常量,或者将对应的Module的sdk版本改成小于23(Android Studio)。

适用于数据量小的请求,因为通过http请求返回的内容不管多大都会被放到内存里面,内容过大容易OOM,如果是下载大文件的需求官方推荐使用DownloadManager,或者使用HttpURLConnnection读流写到文件里面。

Volley大致工作流程如下:

Volley架构

通过向RequestQueue添加定制的Request发起请求,RequestQueue内部维护着两个优先级阻塞队列(PriorityBlockingQueue),分别用于触发缓存分发器(CacheDispatcher)与网络分发器处理(NetworkDispatcher)。

当一个请求到来时,如果请求不能被缓存,那么就会直接被添加放到mNetworkQueue里,request默认是可以缓存的,所以会先放到mCacheQueue,请求还没有到来之前CacheDispatcher是处于阻塞的状态,当向mCacheQueue添加Request时CacheDispatcher就会被唤醒并拿到这个Reuest,从缓存里面查询缓存,判断缓存是否存在并且有效,如果有效就会被直接交给ResponseDelivery进行结果响应,失效或者不存在就会把请求放到mNetworkQueue里。

同样,当请求没有来临之前NetworkDispatcher也是处于阻塞的状态,当一个网络请求被放到mNetworkQueue,NetworkDispatcher就会被唤醒并拿到这个Request,交给Network处理请求,得到结果Response,判断是否需要写入缓存,最后交给ResponseDelivery结果响应。

Volley处理请求流程就是一个生产者-消费者模式,我们生产Request请求,Dispatcher进行消费,然后进行结果分发。Volley的每个类职责都比较单一,并且很多都是基于抽象接口来实现,比如Request,缓存,处理请求的Network,具有很强的扩展性。下面分别对每个模块进行分析。

Request

Request封装了一个HTTP请求,包含了请求的类型(GET OR POST OR …),请求的路径(url),HTTP请求的头部,POST请求的body等,我们可以根据HTTP协议结合实际需求定制HTTP请求。Request是一个泛型抽象类,我们需要实现如下两个方法,泛型表示响应结果类型。

public class Request<T> {

    abstract protected Response<T> parseNetworkResponse(NetworkResponse response);

    abstract protected void deliverResponse(T response);
    //...
}

Request这两个抽象方法可以认为是Request的两个职责:
1.parseNetworkResponse需要Request自身从HTTP响应的二进制数据去解析需要的类型,NetworkReponse表示一个HTTP的响应结果里面包含了一下内容:

public final int statusCode;
public final byte[] data;
public final Map<String, String> headers;
public final boolean notModified;
public final long networkTimeMs;

一般情况下需要从data二进制数据里面解析出服务端返回的二进制数据,根据结合响应头部headers生成HTTP缓存Cache.Entry(已提供工具方法HttpHeaderParser # parseCacheHeaders),解析的过程是在Dispatcher线程里面调用的,也就是Work Thread,解析得到结果之后使用Response工厂方法产生正确的响应或者错误的响应:

public class Response {
    public static <T> Response<T> success(T result, Cache.Entry cacheEntry) {
        return new Response<T>(result, cacheEntry);
    }
    public static <T> Response<T> error(VolleyError error) {
        return new Response<T>(error);
    }
}

2.deliverResponse需要Request将响应结果内容分发,这里可以进一步调用接口分发,还可以直接更新UI,因为这里是通过Handler切换到UI线程调用的。

Request实现了Comparable接口,用于处理请求的优先级,每个请求被加入队列都会被分配一个递增序列mSequence实现FIFO,可以设置LOW,NORMAL,HIGH,IMMEDIATE四种优先级,默认优先级是NORMARL。

@Override
public int compareTo(Request<T> other) {
    Priority left = this.getPriority();
    Priority right = other.getPriority();
    //先比较优先级,后比较加入的顺序
    return left == right ?
            this.mSequence - other.mSequence :
            right.ordinal() - left.ordinal();
}

可以给Request设置一个RetryPolicy,表示Request的重试策略,请求失败之后的重试次数,请求等待的超时时间。

Volley给我们实现了五种Request,其中JsonRequest是一个抽象类,用于请求的body为json格式字符串,实现类是JsonArrayRequest,JsonObjectRequest。

Request默认实现

分别用于不同的响应结果String,JsonObject,JsonArray,Bitmap,有个比较特别,ClearCacheRequest清除缓存的请求,会被立刻执行。
Volley考虑到ImageRequest的特殊性做了特殊处理,例如优先级是最低的,并且为了防止OOM在统一时刻只解析一张图片,设置了重试策略。

RequestQueue

RequestQueue#add,添加请求,内部维护了四个不同形式的集合,用于实现不同功能。

public class RequestQueue{
    private final Map<String, Queue<Request<?>>> mWaitingRequests =
            new HashMap<String, Queue<Request<?>>>();

    private final Set<Request<?>> mCurrentRequests = new HashSet<Request<?>>();

    private final PriorityBlockingQueue<Request<?>> mCacheQueue =
        new PriorityBlockingQueue<Request<?>>();

    private final PriorityBlockingQueue<Request<?>> mNetworkQueue =
        new PriorityBlockingQueue<Request<?>>();
}

mCacheQueue和mNetworkQueue前面已经提到,用于触发NetworkDispatcher和CacheDispatcher处理请求。

mWaitingRequests:Request的key相同会被认为是相同的请求(key默认是Request的url),当有相同的请求到来时,如果Request是可以被缓存的(mShouldCache==true),那么,就会被放到mWaitingRequests中key对应的队列里,当请求被处理完成之后,Request会调用finish,通知将与之关联的RequestQueue将重复的Request全部放到mCacheQueue里处理,让CacheDispatcher从缓存里取出结果分发。如果Request不可缓存的,就会直接交给NetworkDispatcher处理。

mCurrentRequests:当一个请求被添加到RequestQueue相应也会就会被添加到mCurrentRequests,表示正在处理,可能在CacheDispatcher里面正等着被处理,或者在NetworkDispatcher里,等等,这个集合存在的目的就是让我们可以阻止队列中的Request进行下一步处理,因为在每一个阶段处理Request都会判断Request.isCanceled(),总之就是不会响应我们构造Request注册的回调。在Request被添加到RequestQueue之前,可以给Request设置一个tag(Object类型),要执行取消操作的时候调用RequestQueue#cancelAll(Object tag),就可以取消对应的Request,也可以自定义规则RequestFilter取消,使用tag取消也是RequestFilter实现的,规则就是tag的对象相等(对象地址)。

public void cancelAll(final Object tag) {
    if (tag == null) {
        throw new IllegalArgumentException("Cannot cancelAll with a null tag");
    }
    cancelAll(new RequestFilter() {
        @Override
        public boolean apply(Request<?> request) {
            return request.getTag() == tag;
        }
    });
}

构造RequestQueue需要将抽象的接口Cache,Network,ResponseDelivery关联起来,因为内部的CacheDispatcher和NetworkDispatcher需要用到。要想让RequestQueue开始工作,需要把CacheDispatcher和NetworkDispatcher线程开启,当不在需要请求结束时最好停止这些线程。默认处理缓存分发的线程只有一个,处理网络分发的线程有四个。

public void start() {
    stop();
    mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
    mCacheDispatcher.start();
    for (int i = 0; i < mDispatchers.length; i++) {
        NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
                mCache, mDelivery);
        mDispatchers[i] = networkDispatcher;
        networkDispatcher.start();
    }
}

另外如果要想要监听每一个Request的处理结果(可能是成功或者取消),可以向RequestQueue注册RequestFinishedListener。

public static interface RequestFinishedListener<T> {
    /** Called when a request has finished processing. */
    public void onRequestFinished(Request<T> request);
}

这应该是RequestQueue的所有工作了。

CacheDispatcher和NetworkDispatcher

CacheDispatcher和NetworkDispatcher,开始工作后会处于循环,从相应的任务队列取出任务来处理。
CacheDispatcher从mCacheQueue取出Request之后,根据CacheKey从缓存里面取出缓存对象Cache.Entry,如果无效(不存在或者为空)就会放到mNetworkQueue里,交给NetworkDispatcher处理。命中缓存的话,调用前面Request实现的抽象接口parseNetworkResponse,从缓存对象里解析对应的数据对象和HTTP响应头部封装成一个Response,让ResponseDelivery去响应。

这里需要还有关键的一步,判断缓存对象是否需要刷新,从缓存头部会解析出来两个用于控制缓存是否有效的变量ttl和soft ttl。(Time To Live)
ttl小于当前的时间戳表示缓存已经完全失效不能再使用,这时就需要交给NetworkDispater处理,而soft ttl则表示缓存你暂时先用着,但还是要交给NetworkDispatcher处理,这次处理也比较特殊,会带上HTTP缓存信息相关头部,等它处理完这次请求才是真正的结束,并响应第二次更新缓存内容。

NetworkDispatcher的任务就是调用mNetwork执行真正的网络请求,解析之后解析HTTP响应,根据需要是否缓存。最后就是通知ResponseDelivery响应结果。

结束CacheDispatcher和NetworkDispatcher都是通过优先级阻塞队列(PriorityBlockingQueue)的take方法,当调用线程的interrupt方法,就会catch
InterruptedException,就可以比较“优雅”地结束掉线程的工作。另外CacheDispatcher往mNetworkQueue里放置Request的方法put也是阻塞的,但由于mNetworkQueue队列的容量是没有限制的,所以就不会被阻塞。

Network和HttpStack

public interface Network {

    public NetworkResponse performRequest(Request<?> request) throws VolleyError;
}


public interface HttpStack {

    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
        throws IOException, AuthFailureError;
}

为了将耦合度降低,Volley将封装HTTP缓存响应头部,解析头部交给Network处理,响应状态码的处理,连接/等待超时的重试机制交给Network。真正请求的操作交给HttpStack,处理返回通用结果HttpResponse。两者的关系是Has-a(也就是聚合)。

Network只提供了一个实现 BasicNetwork。HttpStack提供了两种实现HurlStack,HttpClientStack,即内部实现分别是HttpUrlConnection和HttpClient。由于2.3的之前的版本HttpUrlConnection是不可靠的,所以默认使用的就是HttpClientStack,HttpUrlConnection的性能据说比HttpClient要好,并且后来Android 6.0也把HttpClient给剔除了,一般情况下还是使用HurlStack。我们也可以实现Network其他形式的请求栈,比如okhttp

public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
    //...
    if (stack == null) {
        if (Build.VERSION.SDK_INT >= 9) {
            stack = new HurlStack();
        } else {
            stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
        }
    }
    Network network = new BasicNetwork(stack);
    return queue;
}

HttpStack的职责就是封装请求参数给对应的实现,比如HttpUrlConnection,然后读取响应结果再封装成NetworkResponse。里面也涉及到了有请求Url的重写, Https的相关处理。

ByteArrayPool和PoolingByteArrayOutputStream

在BasicNetwork中有一点也比较特别,将Http的实体内容(HttpEntity)解析为字节数组,将这些临时用到的字节数组放到一个对象池里面,使用结束之后又将其放回对象池,免去了字节数组对象的频繁初始化,抑制内存抖动和垃圾回收器频繁回收,提高性能,实现类是ByteArrayPool。

先来看ByteArrayPool的使用,就提供了两个接口,接口是同步的,因为分别有四个不同的NetworkDispatcher(线程)调用:

public class ByteArrayPool {
    public synchronized void returnBuf(byte[] buf) {
        //...
    }

    public synchronized byte[] getBuf(int len) {
        //...
    }

}

当我们需要使用byte[]的时候就通过调用getBuf获取,len为返回的byte数组大小,使用完毕之后调用returnBuf放回对象池里面。
下面是ByteArrayPool的结构:

ByteArrayPool和PoolingByteOutputStream

mBuffersBySize保存着池里面由小到大的引用,当我们需要一个对象的时候,就会从里面最小的开始查找,如果没有合适的对象,那么就直接new。

public synchronized byte[] getBuf(int len) {
    for (int i = 0; i < mBuffersBySize.size(); i++) {
        byte[] buf = mBuffersBySize.get(i);
        if (buf.length >= len) {
            mCurrentSize -= buf.length;
            mBuffersBySize.remove(i);
            mBuffersByLastUse.remove(buf);
            return buf;
        }
    }
    return new byte[len];
}

mBuffersByLastUse保存最近使用记录,最后被使用的会被添加到链表的尾部,当池里面的总大小超过mSizeLimit,就会从池子里面把最不常使用的移除,逐个移除,从第一个开始,直到满足小于等于mSizeLimit:

public synchronized void returnBuf(byte[] buf) {
        if (buf == null || buf.length > mSizeLimit) {
            return;
        }
        mBuffersByLastUse.add(buf);
        int pos = Collections.binarySearch(mBuffersBySize, buf, BUF_COMPARATOR);
        if (pos < 0) {
            pos = -pos - 1;
        }
        mBuffersBySize.add(pos, buf);
        mCurrentSize += buf.length;
        trim();
}

private synchronized void trim() {
    while (mCurrentSize > mSizeLimit) {
        byte[] buf = mBuffersByLastUse.remove(0);
        mBuffersBySize.remove(buf);
        mCurrentSize -= buf.length;
    }
}

Volley默认对象池里面的总大小为4K,所以比较适合小数据量的频繁请求,可以根据需求调整。

toolbox里与这个类有关系的就是PoolingByteArrayOutputStream,继承于ByteArrayOutputStream,ByteArrayOutputStream的核心就是内部有一个byte数组,通过往里面写内容实质就是往该数组写,里面的字节数组是通过new方式产生的,ByteArrayOutputStream不再使用就会被GC回收。
而PoolingByteArrayOutputStream内部的byte数组是从对象池里面取出来的,当使用结束之后又会放回到对象池里面。

public class PoolingByteArrayOutputStream extends ByteArrayOutputStream {

    public PoolingByteArrayOutputStream(ByteArrayPool pool, int size) {
        mPool = pool;
        buf = mPool.getBuf(Math.max(size, DEFAULT_SIZE));
    }

    //...

    @Override
    public void close() throws IOException {
        mPool.returnBuf(buf);
        buf = null;
        super.close();
    }

}

当PoolingByteArrayOutputStream调用close的时候就是把其放回对象池里面。
最后再来看下总体的流程:

/** Reads the contents of HttpEntity into a byte[]. */
private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError {
    PoolingByteArrayOutputStream bytes =
            new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength());
    byte[] buffer = null;
    try {
        InputStream in = entity.getContent();
        if (in == null) {
            throw new ServerError();
        }
        buffer = mPool.getBuf(1024);
        int count;
        while ((count = in.read(buffer)) != -1) {
            bytes.write(buffer, 0, count);
        }
        return bytes.toByteArray();
    } finally {
        try {
            // Close the InputStream and release the resources by "consuming the content".
            entity.consumeContent();
        } catch (IOException e) {
            // This can happen if there was an exception above that left the entity in
            // an invalid state.
            VolleyLog.v("Error occured when calling consumingContent");
        }
        mPool.returnBuf(buffer);
        bytes.close();
    }
}

临时buffer和PoolingByteArrayOutputStream内部的byte[]都是来自对象池。

Cache

Cache是Volley提供的缓存接口,里面的接口也比较好理解:

public Entry get(String key);
public void put(String key, Entry entry);
public void initialize();
public void invalidate(String key, boolean fullExpire);
public void remove(String key);
public void clear();

缓存key为字符串,缓存项为Cache.Entry,里面包含HTTP响应内容的二进制内容,ETag,有效期等相关内容。Volley默认只提供了两种实现,基于磁盘文件的缓存DiskBasedCache,另外一个则是为了实现没有缓存功能的缓存NoCache,里面都是空实现。我们也可已根据自己的需求是实现缓存的方式,例如数据库缓存。

DiskBasedCache默认缓存大小为5MB,会把每一个缓存项当成一个文件,缓存项的格式如下:

DiskBaseCacheHeader

文件头写入的数据cache magic是用于标识该缓存文件的版本,当存入的和读取的cache magic会认为缓存无效而删除。存入字符串时会把其对应的UTF-8的二进制大小先存入,再存对应的数据。
byte[] data以上的内容为缓存CacheHeader,并且byte[] data没有对应的size,可以使用文件的大小减去CacheHeader的大小得到data的大小。
当CacheDispatcher开始工作时会执行缓存初始化操作,DiskBasedCache会将目录下的所有缓存文件载入,读取缓存文件的头部CacheHeader存入:

private final Map<String, CacheHeader> mEntries =
        new LinkedHashMap<String, CacheHeader>(16, .75f, true);

第三个参数为accessOrder为true,访问内容会改变LinkedHashMap的entry顺序,最常访问的会被放到链表的后端。当放置新的缓存时,缓存总大小大于约定的总大小,那么就会移除某些缓存,就是从mEntries链表的前端开始移除。移除缓存就会把对应的文件删除和移除mEntries里面对应的CacheHeader,

ResponseDelivery

ResponseDelivery用于当CacheDispatcher或NetworkDispatcher执行完某个请求,分发处理结果:

public interface ResponseDelivery {
    public void postResponse(Request<?> request, Response<?> response);
    public void postResponse(Request<?> request, Response<?> response, Runnable runnable);
    public void postError(Request<?> request, VolleyError error);
}

Volley的默认实现是ExecutorDelivery:

public class ExecutorDelivery implements ResponseDelivery {

    private final Executor mResponsePoster;

    public ExecutorDelivery(final Handler handler) {
        mResponsePoster = new Executor() {
            @Override
            public void execute(Runnable command) {
                handler.post(command);
            }
        };
    }
}

在RequestQueue里面,如果没有指定ResponseDelivery,那么初始化的就是参数为主线程Handler的ExecutorDelivery:

public RequestQueue(Cache cache, Network network, int threadPoolSize) {
    this(cache, network, threadPoolSize,
            new ExecutorDelivery(new Handler(Looper.getMainLooper())));
}

所以分发默认是在主线程里面执行的,其最终结果会向Request里分发:

if (mResponse.isSuccess()) {
    mRequest.deliverResponse(mResponse.result);
} else {
    mRequest.deliverError(mResponse.error);
}