手撕 Volley

2,311
原文链接: www.jianshu.com

前言

从去年开始使用Volley,到现在一年多了。前几天参加某互联网公司校招被问到Volley相对其他的网络框架有什么优缺点,它分别是如何实现的。当时答得的并不好。所以趁十一假期读一下Volley的源码。

写这篇文章的目的有两个:1. 总结下 Android 网络编程,学习 Volley 设计思想。2. 给正在使用 Volley 但仍然心存疑惑的人一些更深入的解析。

Volley到底是什么

Volley Github主页
Android 网络通信框架Volley简介(Google IO 2013)

Volley简介

volley 是 Goole I/O 2013上发布的网络通信库,使网络通信更快、更简单、更健壮。
关键词:数据不大但通信频繁
Volley名称的由来: a burst or emission of many things or a large amount at once

Volley提供的功能
  • Json,图像等异步下载
  • 网络请求的排序(scheduling)
  • 网络请求的优先级处理
  • 缓存
  • 多级别取消请求
  • 和 Activity 的生命周期联动(Activity 结束时同时取消所有网络请求)

Volley好在哪

HttpClient、HttpURLConnection、OKHttp和Volley优缺点和性能对比

物理质量
  • 使用Volley 需要Volley.jar(120k),加上自己的封装最多140k。
  • 使用OkHttp需要 okio.jar (80k), okhttp.jar(330k)这2个jar包,总大小差不多400k,加上自己的封装,差不多得410k。
    Volley 的优点
  • 非常适合进行数据量不大,但通信频繁的网络操作
  • 可直接在主线程调用服务端并处理返回结果
  • 可以取消请求,容易扩展,面向接口编程
  • 网络请求线程NetworkDispatcher默认开启了4个,可以优化,通过手机CPU数量
  • 通过使用标准的HTTP缓存机制保持磁盘和内存响应的一致
Volley 的缺点
  • 使用的是httpclient、HttpURLConnection
  • 6.0不支持httpclient了,如果想支持得添加org.apache.http.legacy.jar
  • 对大文件下载 Volley的表现非常糟糕
  • 只支持http请求
  • 图片加载性能一般

Volley 的使用场景和使用方式

关于 Volley 怎么用网络上的文章太多了,链接整理如下

Android Volley完全解析(一),初识Volley的基本用法
Android Volley完全解析(二),使用Volley加载网络图片
Android Volley完全解析(三),定制自己的Request
Android Volley 之自定义Request
官方教程(需要翻墙)
An Introduction to Volley

Http 权威指南笔记

不是要读 Volley 的源码嘛,怎么又看起了 HTTP 权威指南,原因很简单,Volley 源码里面有很多处理是与 HTTP 协议息息相关的,只有了解了协议才能更深入的理解Volley。Volley里面涉及协议的地方都会在注释中给出协议文档的链接。

Hypertext Transfer Protocol -- HTTP/1.1
HTTP权威指南读书笔记
HTTP协议详解(真的很经典)
HTTP协议详解

这里简单介绍一几个概念:


HTTP.png

  • HTTP 协议属于应用层协议,他的基础是 TCP (传输层)/ IP (网络层)协议
  • 一个HTTP事务由一条请求命令和一个响应结果组成。这种通信通过名为 HTTP 报文(HTTP message)的格式化数据块进行
  • 从Web客户端发往Web服务器的HTTP报文称为请求报文(request message)。从服务器发往客户端的报文称为响应报文(reponse message),请求报文和响应报文格式类似。 HTTP报文包括以下三部分:
    • start line 起始行
      请求报文 包括 method + path + version 响应报文 包括 version + status line
    • headers 消息报头,包含了很多键值对,两者之间用冒号(:)分隔。首部以一个空行结束,这里是我们开发主要会用到的地方,特别是缓存。建议大家还是阅读一下连接中给出的相关文章。
    • entity / body消息实体,空行之后就是可选的报文主体了,其中包含了所有类型的数据。起始行和首部都是文本形式且都是结构化的,而主体则不同,主体可以包含任意的二进制数据(图片、视频、音频、软件程序)。当然,主体还可以包含文本。

下面给出一组请求和响应的样例。


HTTP_RequestMessageExample.png


HTTP_ResponseMessageExample.png

HttpURLConnection 与 HttpClient

这里了解一下 httpClient 和 HttpURLConnection 的区别和历史,并主要学习一下 HttpURLConnection 的使用,android 社区现在更推荐使用 HttpURLConnection 来进行网络开发。
需要注意的是 android 6.0 SDK,不再提供 org.apache.http 的支持,所以 6.0 以后要想使用 Volley(HttpClient)需要手动配置 gradle 了。

HttpURLConnection(官方文档,需要翻墙)
Interface HttpClient
A Comparison of java.net.URLConnection and HTTPClient
HttpClient和HttpURLConnection的区别

Volley 源码解析

ok 做完了前面的准备工作终于可以开始最激动人心的部分了,首先来一张官方给出的流程图。


volley-request.png

对于这张图我们只需要知道:

  • Volley 运行的过程中一共有三种线程,包括 UI 线程、Cache 调度线程和 NetWork 调度线程池
  • 请求加入优先级队列,Cache 线程进行筛选,如果命中(hit)分发给 UI 线程
  • 未命中(miss)交给 NetWork 调度线程池处理,取回后更新 Cache 并分发给 UI 线程
  • 每次请求执行过程始于 UI 线程, 终于 UI 线程

再来一张总体设计图:


flow.png

入口

我用的 Sublime Text 3 来阅读 Volley ,可以看到 Volley 中大大小小一共43个类。
我们使用 Volley 的第一步是通过Volley 的 newRequestQueue 方法得到 一个RequestQueue 队列。那么我们就从这个方法开始吧。不管几个参数的 newRequestQueue 方法最终都会调用下面这个三个参数的。


QQ截图20161004202730.png

可以看到:

  1. 在磁盘上创建一块文件
  2. 设置 UserAgent ,不知道什么是UserAgent去看前面的HTTP协议
  3. 根据 SDK 版本的不同初始化 HTTPStack
  4. 用 HTTPStack 初始化 BasicNetwork
  5. 用第一步创建的文件初始化磁盘缓存
  6. 用磁盘缓存和 NetWork 创建我们的 请求队列 RequestQueue
  7. 调用 RequestQueue 的 start 方法

这里面我们有几个疑问

  • HttpStack、HurlStack、HttpClientStack 分别是啥
  • NetWork、BasicNetwork 分别是啥
  • DiskBasedCache 是啥
  • RequestQueue 是啥,他的 start 方法做了什么事情

首先看第一个 HttpStack

/**
 * An HTTP stack abstraction.
 */
public interface HttpStack {
    /**
     * Performs an HTTP request with the given parameters.
      */
    public HttpResponse performRequest(Request request, Map additionalHeaders)
        throws IOException, AuthFailureError;

}
/**
 * An HttpStack that performs request over an {@link HttpClient}.
 */
public class HttpClientStack implements HttpStack {
/**
 * An {@link HttpStack} based on {@link HttpURLConnection}.
 */
public class HurlStack implements HttpStack {

HttpStack 类图


HttpStack.png

很明显 HttpStack 是一个接口并且只有一个 performRequest 方法。
而 HurlStack 和 HttpClientStack 分别是基于 HttpUrlConnection 和 HttpClient 对 HttpStack 的实现,是真正用来访问网络的类。内部具体实现后面再看。
下面再来看NetWork 和 BasicNetWork:

/**
 * An interface for performing requests.
 */
public interface Network {
    /**
     * Performs the specified request.
     * @param request Request to process
     * @return A {@link NetworkResponse} with data and caching metadata; will never be null
     * @throws VolleyError on errors
     */
    public NetworkResponse performRequest(Request request) throws VolleyError;
}


}
/**
 * A network performing Volley requests over an {@link HttpStack}.
 */
public class BasicNetwork implements Network {
    protected static final boolean DEBUG = VolleyLog.DEBUG;

    private static int SLOW_REQUEST_THRESHOLD_MS = 3000;

    private static int DEFAULT_POOL_SIZE = 4096;

    protected final HttpStack mHttpStack;

    protected final ByteArrayPool mPool;

    /**
     * @param httpStack HTTP stack to be used
     */
    public BasicNetwork(HttpStack httpStack) {
        // If a pool isn't passed in, then build a small default pool that will give us a lot of
        // benefit and not use too much memory.
        this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
    }

    /**
     * @param httpStack HTTP stack to be used
     * @param pool a buffer pool that improves GC performance in copy operations
     */
    public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
        mHttpStack = httpStack;
        mPool = pool;
    }



}

Network.png

可以看到同样是接口与实现类的关系,内部封装了一个 HttpStack 用来是想网络请求。
接口的方法是一样的,这又是为什么呢,这里暂且不管,后面看实现再分析。
接下来是 DiskBasedCache:

/**
 * Cache implementation that caches files directly onto the hard disk in the specified
 * directory. The default disk usage size is 5MB, but is configurable.
 */
public class DiskBasedCache implements Cache {

可以看得到 DiskBaseCache 继承自 Cache接口,老规矩我们先看接口不看具体实现,先知道他是干嘛的。

/**
 * An interface for a cache keyed by a String with a byte array as data.
 */
public interface Cache {
    /**
     *  an entry from the cache.
     * @param key Cache key
     * @return An {@link Entry} or null in the event of a cache miss
     */
    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();

    /**
     * Data and metadata for an entry returned by the cache.
     */
    public static class Entry {
        /** The data returned from cache. */
        public byte[] data;

        /** ETag for cache coherency. */
        public String etag;

        /** Date of this response as reported by the server. */
        public long serverDate;

        /** The last modified date for the requested object. */
        public long lastModified;

        /** TTL for this record. */
        public long ttl;

        /** Soft TTL for this record. */
        public long softTtl;

        /** Immutable response headers as received from server; must be non-null. */
        public Map responseHeaders = Collections.emptyMap();

        /** True if the entry is expired. */
        public boolean isExpired() {
            return this.ttl < System.currentTimeMillis();
        }

        /** True if a refresh is needed from the original data source. */
        public boolean refreshNeeded() {
            return this.softTtl < System.currentTimeMillis();
        }
    }

}

Cache.png

又是接口,Volley 核心功能的实现都是基于接口的。
我们来看,接口 Cache 里面封装了一个静态内部类 Entry(登记),这个内部类非常重要,看了 HTTP 协议的同学们会发现,Entry 里面定义的这些成员变量跟 headers(消息报头)里面关于缓存的标签是一样的,这也是前面强调要看协议的原因。其中还维护了一个map 用来保存消息报头中的 key / value,data 来保存 entity 消息实体。除此之外就是一些集合操作了。
我们使用 Volley 的时候创建一个 request 然后把它丢到 RequestQueue 中就可以了。那么来看 RequestQueue 的构造方法,下面是最终会调用的构造器。

public RequestQueue(Cache cache, Network network, int threadPoolSize,
            ResponseDelivery delivery) {
        mCache = cache;
        mNetwork = network;
        mDispatchers = new NetworkDispatcher[threadPoolSize];
        mDelivery = delivery;
    }

设置了几个成员变量,那么 RequestQueue到底有哪些成员变量呢

/** Used for generating monotonically-increasing sequence numbers for requests. */
    private AtomicInteger mSequenceGenerator = new AtomicInteger();

    /**
     * Staging area for requests that already have a duplicate request in flight.
     *
     * 
    *
  • containsKey(cacheKey) indicates that there is a request in flight for the given cache * key.
  • *
  • get(cacheKey) returns waiting requests for the given cache key. The in flight request * is not contained in that list. Is null if no requests are staged.
  • *
*/ private final Map>> mWaitingRequests = new HashMap>>(); /** * The set of all requests currently being processed by this RequestQueue. A Request * will be in this set if it is waiting in any queue or currently being processed by * any dispatcher. */ private final Set> mCurrentRequests = new HashSet>(); /** The cache triage queue. */ private final PriorityBlockingQueue> mCacheQueue = new PriorityBlockingQueue>(); /** The queue of requests that are actually going out to the network. */ private final PriorityBlockingQueue> mNetworkQueue = new PriorityBlockingQueue>(); /** Number of network request dispatcher threads to start. */ private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4; /** Cache interface for retrieving and storing responses. */ private final Cache mCache; /** Network interface for performing requests. */ private final Network mNetwork; /** Response delivery mechanism. */ private final ResponseDelivery mDelivery; /** The network dispatchers. */ private NetworkDispatcher[] mDispatchers; /** The cache dispatcher. */ private CacheDispatcher mCacheDispatcher; private List mFinishedListeners = new ArrayList();

所有的成员变量以及核心方法类图如下,为了直观方法没有加参数:


RequestQueue.png

  • mSequenceGenerator:序列号生成器
  • mWaitingRequests:hashmap 通过 method + url 为key,重复 request 组成的 queue 为value
  • mCurrentRequests:HashSet 存储包括正在执行和等待所有的 request
  • mCacheQueue:PriorityBlockingQueue 缓存队列
  • mNetworkQueue:PriorityBlockingQueue 网络请求队列
  • DEFAULT_NETWORK_THREAD_POOL_SIZE 网络请求线程池大小
  • mCache 接口 具体实现由构造器传入
  • mNetwork 同上
  • mDelivery 结果分发器
  • mDispatchers 网络调度数组
  • mCacheDispatcher 缓存调度
    RequestQueue 中一共有五个主要的方法,分别是 start、add、stop、cancel、finish
    我们先看 刚才遇到的 start 方法中都做了些什么

    /**
       * Starts the dispatchers in this queue.
       */
      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();
          }
      }

    先调用了 stop,然后分别调用了dispatcher 的 start

    /**
       * Stops the cache and network dispatchers.
       */
      public void stop() {
          if (mCacheDispatcher != null) {
              mCacheDispatcher.quit();
          }
          for (int i = 0; i < mDispatchers.length; i++) {
              if (mDispatchers[i] != null) {
                  mDispatchers[i].quit();
              }
          }
      }

    stop 调用了分别调用了 dispatcher 的quit
    那么疑问来了,dispatcher 的 quit 和 start 是干嘛呢

    public class CacheDispatcher extends Thread {
    public class NetworkDispatcher extends Thread {

    CacheDispatcher 和 NetworkDispatcher 都继承自Thread,start 方法自然是开启一个新的线程那quit,一定是关闭线程了,看一下 Volley 是怎么实现的

    public void quit() {
          mQuit = true;
          interrupt();
      }
     @Override
      public void run() {
          while (true) {
                  if (mQuit) {
                      return;
                  }
          }
      }

    我们忽略具体实现可以看到,run 方法里面是一个 while true 的无限循环,然后用以个标记字段,来控制循环退出。
    所以 start 方法做的的事情就很清楚了,先 stop 掉跑着的线程,然后开启一个缓存线程, 一组(默认四个)网络线程,每个里面都有一个while ture 死循环。等待 request add 到 Requestqueue 中,接下来我们就来看五个主要方法中的 add
    手撕 Volley(二)