Google网络请求框架Volley源码浅析(2)

Google网络请求框架Volley源码浅析(二)

经过上一篇的学习,我们了解了HttpStack家族和Network家族,这两个苦命的”无产阶级劳动者”为我们辛辛苦苦的在底层努力的工作,本篇我们就继续向下,把newRequestQueue这个方法里余下的类给分析完.


Cache

Cache是Volley中所有缓存的接口,那么它就具有缓存操作的基本方法,我们直接上代码:

/**
* 该接口用于以键值对的形式缓存byte数组,键是一个String
*/
public interface Cache {
/**
 * 返回一个空的Entry.
 * @param key key
 * @return 一个 {@link Entry} or null
 */
Entry get(String key);

/**
 * 添加或者替换entry到cache.
 * @param key key
 * @param entry 要存储的Entry
 */
void put(String key, Entry entry);

/**
 * 执行任何具有潜在长期运行特性的动作之前,必须进行初始化;
 * 该方法运行在子线程.
 */
void initialize();

/**
 * 刷新Cache中的Entry.
 * @param key key
 * @param fullExpire 该Entry是否完整过期
 */
void invalidate(String key, boolean fullExpire);

/**
 * 从cache中移除一个entry.
 * @param key Cache key
 */
void remove(String key);

/**
 * 清空cache.
 */
void clear();

/**
 * 一个entry的数据和主数据实体bean.
 */
class Entry {
    /** 数据区域. */
    public byte[] data;

    /** cache数据的Etag. */
    public String etag;

    /** 服务器响应的时间. */
    public long serverDate;

    /** 资源最后编辑时间. */
    public long lastModified;

    /** record 的ttl. */
    public long ttl;

    /** record 的软ttl. */
    public long softTtl;

    /** 从服务器接收的固定的响应头; 不能为null. */
    public Map<String, String> responseHeaders = Collections.emptyMap();

    /** 判断该资源是否超时. */
    public boolean isExpired() {
        return this.ttl < System.currentTimeMillis();
    }

    /** 返回True如果需要从原数据上刷新. */
    public boolean refreshNeeded() {
        return this.softTtl < System.currentTimeMillis();
    }
}

get,put,remove,clear这些方法我们都不说了,来说说initialize方法,它的注释是说:执行任何具有潜在长期运行特性的动作之前,必须进行初始化,看起来是说如果预计动作执行的时间比较久,就需要调用该方法进行初始化,时间短就无所谓了,这个我的理解是一种防御性手段,打个比方,我们有些缓存的存和取如果耗时很久,则会造成在ActivityThread产生ANR,那么就得不偿失了,因为使用缓存的目的本来就是加快我们应用的响应速度,这样搞出个ANR,比不使用缓存还要让用户崩溃。所以说如果预计我们缓存的执行时间会比较长,则可以在这个方法中预先进行一些操作,比如预先创建一些变量,预先从数据库把数据读出来,预先把文件中的内容读出来等等,况且这个方法在子线程运行,不会影响UI线程的效率。
invalidate方法比较简单,就是刷新某一个指定的缓存元素,那么我们说,为什么要刷新呢?那是因为我们每一个缓存元素都是有生命期限的,因为我们知道,缓存其实都是一种临时数据,这些数据不能一直都存在,否则会影响我们获取资源的时效性,谁愿意每天打开报纸看得都是十天前的新闻呢?所以说要时不时的刷新这些缓存数据的状态,及时的清理那些时间过期的缓存,fullExpire参数应该时说是否让这个缓存元素马上失效,如果指定为true,则马上失效,以后就不能访问该缓存了。
至于这个Entry类,则是Cache缓存的实体数据,因为我们看得出,Cache被设计成了键值对的存取形式,和我们经常使用的Map很雷同,既然是键值对,那么键值对就必须要有数据类型啊,则键的类型就是String,值的类型为Entry,从put方法中也看得到放进去的值为一个Entry对象,我们看看Entry中的变量和方法:

  • data是存放真正缓存数据的地方,类型是byte数组,也就是说不论是图片,文字还是视频,最后统统都会转换为byte[]存放,设计成byte[]是一种通用的解决方式
  • etag是我们的老朋友,我们已经在上一篇中介绍过了,想不起来的回头去看一下
  • serverDate标记着该资源被服务器发送过来的时间,用于判断资源的ttl时间,后续会用的到
  • lastModified也不用说了,之前讲过了
  • ttl和softTtl用于判断资源的剩余存活时间,我们上边的资源刷新方法,刷新的就是这两个值,一旦这两个值为0,那么就表示这个缓存资源死翘翘了。
  • responseHeaders字段存放服务器返回该资源的对应的响应头,人家说了这个字段不能为null,即便size为0也行,但就是不能不存在。
  • isExpired方法判断资源是不是过期,是拿ttl和当前的时间来比的,小于当前时间就过期,我们上边那个刷新方法如果把某个资源的ttl强制置为0,那它肯定过期啊,还用说?!
  • refreshNeeded和过期判断一样,用于判断这个资源是不是还可以过期重用

可以看得出Volley在Cache方面下了些功夫的,一个缓存搞的这个大张旗鼓的,可能这就是为啥Volley效率高的原因之一吧。

DiskBasedCache

接下来我们回到分析流程上,我们看到newRequestQueue中创建消息队列的时候传递了一个DiskBasedCache对象,也就是说Volley使用的缓存实现就是这个DiskBasedCache了,事不宜迟,我们去一探这个DiskBasedCache是怎么实现Cache的。
构造函数很没意思,直接看成员变量:

/**
 * Map的Key, CacheHeader键值对
 * 初始大小16,加载因子0.75,也就是说超过12个元素就会扩容
 * false 基于插入顺序排列元素  true  基于访问顺序排列元素(LRU算法)
 */
private final Map<String, CacheHeader> mEntries = new LinkedHashMap<String, CacheHeader>(16, .75f, true);

/** 当前被使用的总的空间大小(bytes). */
private long mTotalSize = 0;

/** 用于cache的根目录. */
private final File mRootDirectory;

/** 缓存的最大尺寸(bytes). */
private final int mMaxCacheSizeInBytes;

/** 默认最大的缓存尺寸(5MB). */
private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;

/** 缓存中的警报线 */
private static final float HYSTERESIS_FACTOR = 0.9f;

/** 当前缓存文件格式的魔法数. */
private static final int CACHE_MAGIC = 0x20150306;

一看到使用Map存储我们就放心了,看来键值对形式的果然还是离不开Map,我们看到键是String,值是CacheHeader,这个CacheHeader类型我们一看,就是一个山寨的Entry,简直一模一样,可以把这个CacheHeader看作Entry的子类或者实现类或者就是孪生兄弟,在接口中对Entry的操作最终都会通过这个CacheHeader来操作,这个LinkedHashMap初始大小为16,负载因子0.75,accessOrder为true,意味着使用了LRU算法对元素进行排序,我们还看到缓存根目录和最大尺寸,都是在构造中传过来的,默认的最大缓存为5M,警报线0.9,也就是缓存达到了4.5M就会自动清理。
接下来分析其中的方法,代码不贴,每个方法都贴代码就没有写字的地方了:

  • 先看初始化方法initialize,该方法用来扫描缓存目录,取出所有的缓存对象,如果目录没有创建的话,会创建缓存目录,自然也就不用扫描了,因为根本没有缓存对象,然后用CacheHeader的readHeader方法从文件中读CacheHeader对象,把读到的对象调用putEntry放到我们的Map当中,putEntry是DiskBasedCache的私有方法,用于把CacheHeader放进LinkedHashMap,并更新当前的Cache总大小,因为对文件的读取是一件耗时操作,所以我们在initialize方法里边做了,而且这里是子线程,initialize方法发挥了它应有的作用。
  • 接着是put方法,我们分步骤一点一点看:
    • pruneIfNeeded方法用于在存放缓存对象之前,先检查一下目前的缓存空间是否足够,带的参数是要放进去的缓存对象的大小,我们跟进去看看他的逻辑是怎么写的,首先判断当前缓存空间是否能够容纳下要放进来的缓存,空间足够的话就不用trim了,如果空间足够的话,才考虑进行trim,这个trim的流程是LRU算法实现中的一部分,先用Iterator枚举出Map中的每一个Map.Entry,然后根据该Map.Entry中的CacheHeader找到对应的文件并删除,随后更新当前的缓存总大小,如果缓存总大小可以容纳要放进来的缓存对象,则不再删除,否则就继续删除,直到腾出足够的大小,其实我们可以看出来,由于我们的Map本身就是按照访问顺序排序的,所以每次调用iterator.next()拿出来的CacheHeader都是当前所有元素中访问次数最少的那一个,也就这么简单的方式我们就实现了一个LRU算法,这样的方法很值得我们学习参考。
    • 第二步是用getFileForKey创建产生一个key对应的文件,这里我们跟进去一看便知,文件名的生成和key有很大的关系,利用了一个简单的随机hash算法计算出来的文件名,这样可以保证,每一个key计算出来的文件名都是确定并且唯一的,并且由于Hash有着很强的单向性,单单根据文件名是很难计算出来该文件对应的Key,这样其实是一种保密算法,对于我们的缓存的一些比较敏感的数据,起到了一定的保护作用。
    • 后面的一段就没什么好说的了,一直到最后我们发现了一些猫腻,这个缓存文件被删除掉了,这个地方为什么要删除我也没搞懂,有知道的兄弟麻烦留言告诉一下。
  • remove方法我们就不用再唠叨了,getFileForKey方法我们已经分析过,removeEntry方法是DiskBasedCache的private方法,和putEntry是一样的
  • get方法我们还是要拿出来看看,可见直接在Map中根据key而get出来一个CacheHeader,然后根据key取得缓存文件,从文件中取得数据把这个CacheHeader对象填满,之后封装成Entry返回就行了,我们知道本来CacheHeader中已经存储了对应的缓存数据,凡是这里偏偏要从文件中读取,我觉得这样做是为了保险起见,因为从Map中获取的是存在于内存中的变量,既然是变量就存在的不可预知性,难免不保证不被GC回收掉或者被恶意软件截取,远没有存在于目录中的文件来的实在和有安全感,不过这只是我目前的一点愚见,有大神有更好的理解请指出来大家一起学习。
  • invalidate方法的这个实现正如我们想象的一样,果然是在操作softTtl和ttl
  • 其余的方法都是和本地的输入输出流有关,之所以定义这么多IO流是因为我们定义了一个CacheHeader,因为DaskBasedCache是一个基于本地文件的缓存策略,之所以要定义一个Entry的孪生兄弟CacheHeader,就是为了把Entry这个缓存对象写入到本地的文件当中去,这个CacheHeader就是直接和这些IO方法高度关联的,我们通过注释也可以看出,Volley为了效率,抛弃了Java中原生的ObjectStream那一套,为了把CacheHeader写入到文件中,自定义了一套对象流方法,就是我们见到的read(),write(),readInt(),writeInt(),readLong(),writeLong()等这一系列方法。
    ok,我们已经把Cache相关的类看了个遍,关于Cache还有一个子类,叫做NoCache,听名字就知道是一个空壳,跟进去一看果不其然,没有任何的缓存操作,纯粹是一个空实现的子类,我们就不做过多的分析了。

RequestQueue

回归我们的分析步骤,在Volley中创建了一个DiskBasedCache并传递给了RequestQueue,那就是说在RequestQueue中采用了基于磁盘的缓存策略,还等什么,我们马上解决这个RequestQueue。
RequestQueue是Volley中最重要的几个类之一,我们先把它定位成一个请求容器以及请求调度器,所以最主要的功能就是进行请求的存储和调度,而对于RequestQueue的分析我们也会进行的比较仔细,希望大家做好打持久战的准备,考虑到目前我们还不了解请求的执行流程,我们先按照RequestQueue类自顶向下进行分析,然后再按照RequestQueue的执行流程把每一个模块都串起来,这样就会对RequestQueue有一个比较深刻的认识了,ok,下面就进入正题:
首先是请求完成的回调接口,是一个Listener,看代码:

/** Request完成之后的回调接口. */
public interface RequestFinishedListener<T> {
    /** Request的处理完成的时候调用这个接口. */
    void onRequestFinished(Request<T> request);
}

看来会在RequestQueue中设置请求完成回调,当一个Request执行完毕之后,不论结果是成功还是失败,都会调用RequestFinishedListener把这个Request回调给主线程,让主线程拿到这个Request去分析结果。
接下来是一个单调递增的序号器:

/** 用于生成请求的单调递增序列号. */
    private AtomicInteger mSequenceGenerator = new AtomicInteger();

这是一个自动递增返回整数的类,每调用一次就会产生一个比上一次调用大一的整数返回,用于给每一个Request设置序号,因为它是RequestQueue的成员变量,所以只要是处于RequestQueue这个容器对象内被调度的Request都会有且唯一的单调递增整型的序号,但是要知道不同的RequestQueue对象之间就不能保证唯一了,因为那是不同的AtomicInteger对象,产生的序号就有可能重复。
继续,下面我们看两个比较重要的容器:

    /**
     * 当一个request已经存在一个正在执行的另一个重复request的时候,会被放置到这个中转区域
     * <ul>
     *     <li>containsKey(cacheKey) 方法表示给定的Cache key已经存在一个正在执行的请求.</li>
     *     <li>get(cacheKey) 返回给定的cache key对应的正在等待执行的请求. 正在执行的请求不包含在返回列表中.如果没有请求被中转则返回null.</li>
     * </ul>
     */
    private final Map<String, Queue<Request<?>>> mWaitingRequests = new HashMap<String, Queue<Request<?>>>();

    /**
     * 当前正在被RequestQueue处理的request集合. 任何处于等待队列中的请求或者正在被调度器处理的请求都会被放置于该集合中.
     */
    private final Set<Request<?>> mCurrentRequests = new HashSet<Request<?>>();

这两个容器一个是HashMap双列集合,一个是HashSet单列无序集合,先看这个mWaitingRequests,键是一个String,然后根据注释,就知道了这个Key就是我们在Cache中分析的那个Key,这里的Value是一个Collection类型的子类Queue,看来也是一个容器,只不过没有那么多排序或者Hash的要求,只要求能存数据就行了,这个Queue中存放的是Request,看来这是一个大容器套小容器的形式,大容器是Map键值对,小容器是Queue,Map的键是Queue中Request用于Cache时的键,那就明白了,这家伙应该是用于分类的,所有Cache键相同的Request都会自动放到Queue中,然后以Cache键和Queue的形式放进Map,而且他使用Cache时用的key,莫非和Cache有什么关联?因为它的名字叫做mWaitingRequests,这里和下文就称它为中转区。
第二个容器是HashSet,里边就是存储的Request,看名字是mCurrentRequests,比较容易理解,就是所有的Request都要存放在这个的地方,不论是要直接执行的也好,不执行的也好,走网络的还是取缓存数据的也好,只要进来一个Request,就必须放在这个地方,可以说这个容器是所有Request的集散地,那我们就简单粗暴一点,把这个容器叫做总仓。
下面又是两个比较重要的容器:

/** 缓存分类队列. */
private final PriorityBlockingQueue<Request<?>> mCacheQueue = new PriorityBlockingQueue<Request<?>>();

/** 网络分类队列. */
private final PriorityBlockingQueue<Request<?>> mNetworkQueue = new PriorityBlockingQueue<Request<?>>();

PriorityBlockingQueue类型是Java中用于优先级排序的队列类,想知道详细点的就去百度一把,我们这里就简单理解成一个自然排序的集合,这个集合里边都是Request就行了,至于为啥要分成两个类型的队列我们需要明白,我们知道Volley中设计了很强大的缓存机制,那肯定不能浪费啊,得把这些缓存机制派上用场啊,那就是了,这里就是使用缓存最好的地方了,这里把请求分成两类,一种是必须通过网络访问才能拿到资源的,另外一种是通过访问缓存就能拿到资源的,这样就不需要无进行网络请求了,进而增加了效率。
下面是一个常量:

/** 网络请求分发器开启的数量,最大同时允许4个线程进行网络请求. */
private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;

这个常量定义了使用网络请求进行资源访问的时候最多允许启用的线程数量,也就是说,同一时间最多允许四个线程进行网络访问。
下面是两个成员变量:

/** 用于检索和存储响应的高速缓存接口. */
private final Cache mCache;

/** 用于执行请求的Network接口. */
private final Network mNetwork;

Cache我们已经分析过了,出现在这里也不意外,我们需要对缓存进行操作,从而把我们从网络上获得的资源存储到缓存中,也需要从缓存中查找和获取资源,以提高效率,NetWork也是一样,在mNetworkQueue队列中的所有Request都是需要NetWork去进行网络操作的。
下面的几个成员也比较重要:

 /** 响应传递机制. */
 private final ResponseDelivery mDelivery;

 /** 网络调度器. */
 private NetworkDispatcher[] mDispatchers;

 /** 缓存调度器. */
 private CacheDispatcher mCacheDispatcher;

mDispatchers是网络调度器,我们说最多允许四个线程来进行网络请求,那么就需要有一个调度器来调度,每一次到底由哪四个Request去进行网络访问,要不然这么多Request没人管理,大家一窝蜂挤着上,有木有早上挤公交车的赶脚?mCacheDispatcher也是一个意思,这么多Request等着要缓存,总有一个管理员指引大家排排队啊,登个记啊啥的,不能乱来,我们知道越是乱哄哄效率越低,这和我们现实生活中的情景是一样的。mDelivery是响应的回传,我们说网络请求是发出去了,也由Network去执行去了,但是Network执行完毕之后人家返回了个Response,总得有人把这个Response传递回来到RequestQueue中吧,由谁来完成中间这一段路呢?那就是mDelivery了,没跑了。
最后的一个成员是个数组:

/** 请求完成回调接口的集合 */
private final List<RequestFinishedListener> mFinishedListeners = new ArrayList<RequestFinishedListener>();

是一个RequestFinishedListener数组,这个数组是针对所有的Request的,外部可以传很多个RequestFinishedListener进来,也就是说想要监听请求执行完成的外部对象不止一个,那就都传进来吧,传进来之后我用个数组给你们保存着,每当一个Request执行完毕之后,我都挨个的调用数组里的RequestFinishedListener,告诉你们xxx这个Request执行完了,你们拿到这个Request之后想干嘛干嘛,反正我RequestQueue的工作是完成了。
构造方法我们就无须多讲了,只需要知道Cache接口,Network网络执行接口,NetworkDispatcher网络请求分发器数组以及ResponseDelivery响应传递器是必须要在构造方法中创建的就行了。
接下来我们看看RequestQueue中的方法,首先是start方法:

/**
 * 开启队列调度.
 */
public void start() {
    stop();  // 调用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();
    }
}

这个方法就是RequestQueue重要开关,只要一打开开关,整部机器就开始按照我们设定的规则开始运行了,首先调用了一下stop方法,先停止所有已经运行的任务,这也是一个保险的做法,要不然以前运行的任务和现在进行的额任务交错在一起,岂不乱套了,然后创建缓存调度器mCacheDispatcher并调用start方法开启,这是对的,因为一旦任务开始运行,就肯定要进行调度分发,指定那些获取缓存的Request该如何调度,接着就要给mDispatchers数组中的每一个元素创建对象并且也调用start方法开启,好了,我们的Request都进场了,这些调度器也开始调度了,那我们的整部RequestQueue就运行起来了。
上个方法中我们用到了stop方法,这个方法其实很简单:

/**
 * 关闭缓存和网络调度器.
 */
public void stop() {
    if (mCacheDispatcher != null) {
        mCacheDispatcher.quit();
    }
    for (NetworkDispatcher mDispatcher : mDispatchers) {
        if (mDispatcher != null) {
            mDispatcher.quit();
        }
    }
}

就这么简单,停止工作嘛,所有的调度器都停止工作,那么整个RequestQueue就停止掉了,因为任务执行的引擎熄火了,那么这台巨大的机器自然而然也就停止掉了,由此可见mCacheDispatcher和mDispatchers在整个RequestQueue中是多么的重要,他们可以认为是RequestQueue中的发动机。
下面的这个getSequenceNumber和getCache就没必要说了,如果不懂AtomicInteger机制的同学去看看Java API就明白了,我们说一下这个东西:

/**
 * 一个简单的过滤接口, 用于 {@link RequestQueue#cancelAll(RequestFilter)}.
 */
public interface RequestFilter {
    boolean apply(Request<?> request);
}

/**
 * 取消此队列中的所有请求,该队列中的所有请求都适用 .
 * @param filter 要使用的过滤方法
 */
public void cancelAll(RequestFilter filter) {
    synchronized (mCurrentRequests) {
        for (Request<?> request : mCurrentRequests) {
            if (filter.apply(request)) {
                request.cancel();
            }
        }
    }
}

/**
 * 取消此队列中带有给定的tag的所有请求, Tag不能为null并且具有平等的标识.
 */
public void cancelAll(final Object tag) {
    if (tag == null) {
        throw new IllegalArgumentException("Cannot cancelAll with a null tag");
    }
    // RequestFilter的作用在这里显示出来了
    cancelAll(new RequestFilter() {
        @Override
        public boolean apply(Request<?> request) {
            return request.getTag() == tag;
        }
    });
}

RequestFilter是一个接口,用于对判断指定的Request是否符合某种过滤条件,对,这个接口的作用就是用来过滤的,过滤就需要比较筛选,比如我们去买黄瓜,那肯定要挑选又粗又长的,而且还要比较新鲜的,好用的(随机举例,不要多想……),对吧?那这就是一个筛选的过程,所以其中的apply方法就是定义的筛选方法,具体的怎么筛选由子类实现,因人而异,有人喜欢粗的,有人喜欢长的,有人喜欢好用的,筛选标准都不一样。ok,我们弄懂了这个接口,那就看下面的cancelAll方法,不是第一个cancelAll,我说的是第二个参数是final Object tag的cancelAll,因为他们执行的顺序就是这样,这个cancelAll指定了一个Tag,接着实现了一个RequestFilter,这个过滤器过滤标准是是要Request的tag等于传递进来的tag就ok,接着调用第一个cancelAll,那么第一个cancelAll接到这个过滤器之后,就根据这个过滤器在我们的总仓中找啊找啊,找到符合条件的Request之后干嘛呢?干掉!cancel掉。这就是RequestQueue中取消某个Request执行任务的方法,是不是很简单粗暴直观易懂?
下面的add方法比较重要:

/**
 * 向调度队列中增加一个Request.
 * @param request 要服务的request
 * @return 被过滤器认为可以接受的request
 */
public <T> Request<T> add(Request<T> request) {
    // 将请求标记为属于这个队列,并将其添加到当前请求的集合中.
    request.setRequestQueue(this);
    synchronized (mCurrentRequests) {
        mCurrentRequests.add(request);
    }

    // 按照他们被添加的顺序处理这些request.
    request.setSequence(getSequenceNumber());
    request.addMarker("add-to-queue");

    // 如果请求是不可缓存的,跳过缓存队列,直接走网络通信
    if (!request.shouldCache()) {
        mNetworkQueue.add(request);
        return request;
    }

    // Insert request into stage if there's already a request with the same cache key in flight.
    // 如果有一个相同的cache Key的request正在被执行,那么当前这个request就会被安排在中转区域
    synchronized (mWaitingRequests) {
        // 拿出当前request的key
        String cacheKey = request.getCacheKey();
        // 查看中转区域中是否存在当前request的key
        if (mWaitingRequests.containsKey(cacheKey)) {
            // 有一个相同的cache Key的request正在被执行. 把当前的request放进队列中.
            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 {
            // 中转区域中没有该Key对应的请求队列,那么我们这个request就是第一个了。我们直接把key放进中转队列中做键
            // 但是要记得我们这第一个request是要被执行的
            // 为这个cacheKey插入一个null队列到中转区域, 标记有一个正在执行的请求.
            mWaitingRequests.put(cacheKey, null);
            // 放进缓存队列中
            mCacheQueue.add(request);
        }
        return request;
    }
}

add方法是向请求队列中添加Requets的方法,首先是调用Request的setRequestQueue方法给它打上标记,就是说你这个Request既然已经被分配到我这个RequestQueue对象中来执行了,那你就要标明你是属于我这个RequestQueue的,然后调用mCurrentRequests.add(request)把这个Request放到总仓中,接着调用setSequence给你做个序列号标记和打上Mark”add-to-queue”,意思就是:”112号Request,等待被分类!”回忆一下你到澡堂子洗澡,交完钱换了衣服拿了手牌,然后服务员就在那儿喊:”112号包厢,男宾一位!”,就是这么个意思。那么接下来就要分类了,就是看看你这个Request是要走网络还是走缓存,怎么分呢?用shouldCache方法判断,返回false直接就把它添加到mNetworkQueue网络请求队列中,说明这个资源之前没有请求过,需要访问网络去获取,那么自然获取完之后下一次访问就会有缓存了,辛苦我一个,方便后来人嘛!返回true就说明这个Request已经有缓存了,看来每一个Request在创建之后都已经知道自己有没有缓存了,那么既然是有缓存的,就肯定有一个CacheKey了对吧,我们在Cache分析中也看到了所有操作缓存的地方都要用到CacheKey,那么就去中转区去找找,看有没有这个CacheKey,要是没有这个CacheKey的话就创建一个键值对,键就是这个CacheKey,值是null,并且要把这个Reuqest放到mCacheQueue中,我们知道,一旦放到mCacheQueue中,那就由mCacheDispatcher去调度了,它就会去读取缓存数据,接下来就是第二种,当我们发现中转区有这个CacheKey的时候,那就是第二个CacheKey相同的Request了,因为当第一个CacheKey相同的Request出现的时候我们已经在中转区放进了这个CacheKey,第二个Request进来之后发现第一个Request仅仅只创建了Key,Value是空的,那么它就创建一个LinkedList类型的Queue把自己放进去,当第三个Resuest进来的时候,中转区里边也有CacheKey了,Queue也创建好了,直接排到队列中就行了,以此类推,所有CacheKey相同的Request都分别放进了中转区,我们到这里已经搞懂了,在CacheKey相同的所有Request中,除了第一个直接被送到了mCacheQueue中,其他的都放进了中转区。这就是add方法所做的工作。
下一个也是比较重要的方法:

/**
 * 该方法被 {@link Request#finish(String)}调用, 标记指定的request已经被处理完毕.
 * <p>Releases waiting requests for <code>request.getCacheKey()</code> if <code>request.shouldCache()</code>.</p>
 */
<T> void finish(Request<T> request) {
    // 从当前正在处理的请求的集合中删除 .
    synchronized (mCurrentRequests) {
        mCurrentRequests.remove(request);
    }
    synchronized (mFinishedListeners) {
        // 挨个调用每一个FinishListener,告诉他们这个Request已经被执行完毕
      for (RequestFinishedListener<T> listener : mFinishedListeners) {
        listener.onRequestFinished(request);
      }
    }
    // request需要被缓存的时候,它有可能有相同CacheKey的request被驻留在中转区域
    if (request.shouldCache()) {
        synchronized (mWaitingRequests) {
            // 获取CacheKey
            String cacheKey = request.getCacheKey();
            // 从中转区域中拿到Key为CacheKey的等待队列
            Queue<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
            if (waitingRequests != null) {
                if (VolleyLog.DEBUG) {
                    VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.", waitingRequests.size(), cacheKey);
                }
                // 处理等待队列中所有的requests. 这些request不会再被一一的执行, 他们可以使用我们已经执行完毕的request给他们准备好的Cache.
                mCacheQueue.addAll(waitingRequests);
            }
        }
    }
}

finish方法是某一个Request执行完毕之后来调用的,要操作的步骤如下:
* 从总仓中移除这个Request,你都已经执行完毕了,就别占这个这个坑了!
* 挨个调用每一个FinishListener,告诉他们这个Request已经被执行完毕,好让关心这个Request的外部对象该干嘛干嘛去。
* 然后判断这个Request的shouldCache,如果是true的话,根据add方法的分析,这个Request肯定是在mCacheQueue中执行之后返回的,那么它就有义务把取得的资源分享给这些在中转区中等待的兄弟们,具体怎么个资源分享法呢?调用这个mCacheQueue.addAll(waitingRequests),里边的实现在以后分析。
* 有人会问,既然mCacheQueue中的Request不是走的网络而是从Cache中拿数据,那所有shouldCache为true的Request为啥不直接去拿缓存,而单单派第一个Request去拿回来大家共享呢?我们要知道,缓存也是数据,不论存放到哪里,文件还是数据库,读取都是要耗费时间的,尤其是像我们分析过的DiskBasedCache,基于文件的对象序列化很耗费资源和时间,既然这样,干嘛不只读取一次大家直接在内存中交换数据,而非要挨个去读取文件呢?你说哪个效率更好?

下面的addRequestFinishedListener和removeRequestFinishedListener没有分析的必要,直接略过吧!所以呢,我们就把Request中的每个成员变量和方法的作用都分析了一边,就像一部机器,零件都认识和知道作用了,那就看看是怎么运行的吧:

  1. 首先就是我们在Volley中看到的了 new RequestQueue创建这部机器,自然的我们熟悉的几个零件,中转区啊,总仓库啊,调度器啊,等等这些都已经有了
  2. 然后就开动机器啊,调用start方法开启调度器,各种调度器就会去执行各自的Queue中的Request。
  3. 可是刚启动的RequestQueue中的各种Queue都还是空的,这机器老开着不干活儿怎么行?你出油钱出电费啊?那就向里边放Request任务吧,就是我们使用Volley中用到的add方法了,我们创建了各种Request都把它add到了RequestQueue中。
  4. 接着调度器就跑去执行Request去了,缓存调度器根据传进来的缓存实现,去取缓存并返回,所以它需要:mCacheQueue队列,mNetworkQueue队列,mCache缓存实现,mDelivery响应回递。为啥操作缓存需要mNetworkQueue队列?保险啊,如果发生意外,缓存没找到,我还可以赶紧把这个Request转移到mNetworkQueue队列中,让它去访问网络,这个时候还来得及,用不了缓存我最起码还可以从网络获取,不至于空手而归。网络调度器则会使用我们的”小组长”或者是”PM”Network对象去访问网络,由他们去驱动下面的”程序员”来干活,所以它需要:mNetworkQueue队列,mNetwork项目经理,mCache缓存实现,mDelivery响应回递。
  5. 然后呢?如果一个Request由Network执行完毕或者是读取缓存结束,则由mDelivery响应回递把对应的Request和Response给传递给RequestQueue的finish方法,那么这个mDelivery就是负责从子线程向主线程的RequestQueue传递数据,其实我们看一眼mDelivery创建的时候就知道了,喏!:

    new ExecutorDelivery(new Handler(Looper.getMainLooper()))

    你拿着主线程的Looper就肯定的给人家传输据。

  6. finish拿到执行完毕后的Request之后就会挨个调用RequestFinishedListener,告诉它这个Request搞定了,你们快来取啊,这些Listener处理完之后就会通知我们创建各种Request时候传递的Response.Listener或者是Response.ErrorListener。

好了,我们整个Volley的执行流程已经分析完毕了,文字说起来比较难以理解,那就来一张图,有图有真相:
Google网络请求框架Volley源码浅析(2)
图画的很简单,但是也基本上表现了RequestQueue的执行流程。

小结

在这一章节中我们分析了Cache家族和RequestQueue类,Volley中的Cache机制很巧妙也很强大,可供我们学习的地方有很多,RequestQueue是Volley中最重要的存储和执行模块,可以说没有RequestQueue拿么Volley只是一个空壳,通过本章的学习我们领悟了Volley的缓存和执行机制,在下一章我们会进一步分析Volley中的调度和执行细节。