手撕 Volley (一)


android_volley_tutorial.jpg

前言

从去年开始使用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<String, String> 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<String, String> 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.
     *
     * <ul>
     *     <li>containsKey(cacheKey) indicates that there is a request in flight for the given cache
     *          key.</li>
     *     <li>get(cacheKey) returns waiting requests for the given cache key. The in flight request
     *          is <em>not</em> contained in that list. Is null if no requests are staged.</li>
     * </ul>
     */
    private final Map<String, Queue<Request<?>>> mWaitingRequests =
            new HashMap<String, Queue<Request<?>>>();

    /**
     * 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<Request<?>> mCurrentRequests = new HashSet<Request<?>>();

    /** The cache triage queue. */
    private final PriorityBlockingQueue<Request<?>> mCacheQueue =
        new PriorityBlockingQueue<Request<?>>();

    /** The queue of requests that are actually going out to the network. */
    private final PriorityBlockingQueue<Request<?>> mNetworkQueue =
        new PriorityBlockingQueue<Request<?>>();

    /** 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<RequestFinishedListener> mFinishedListeners =
            new ArrayList<RequestFinishedListener>();

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


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(二)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容