【进阶android】Volley源码分析——Volley的缓存

原创 2015年07月28日 16:50:03

         上一章我们重点分析了Volley框架之中两种线程的处理流程,以及这两种线程是如何与UI线程进行通信的。

       本章我们将分析Volley框架之中的缓存机制。

       任何一个网络请求都会存在一定的阻塞延时(哪怕网速再快),而作为一个网络框架,Volley引入了缓存机制,最大程度了减少了这一缺点对用户体验的影响。

       通过上一篇文章【进阶android】Volley源码分析——Volley的线程对Volley框架中的缓存线程分析我们知道:如果一个Request对象存在相应的本地缓存实体(Cache.Entry),而无需将该对象添加到网络队列之中,让网络线程进行网络请求。以此来减少网络请求的延迟,加快网络响应的速率。而需要进行网络请求时,在成功的从网络获取到响应后,会将此响应转为Cache.Entry对象存入缓存之中。

       在清晰了缓存在Volley框架中的作用(为什么要使用缓存)后,我们就应当考虑什么是缓存?Volley框架中怎样实现缓存机制?很幸运,Volley框架分别用Cache接口和Cache接口默认执行类,回答了这两个问题。

一、Volley框架中的缓存机制

        什么是缓存机制?其实前面几篇文章或多或少都有相关的解释和定义。在Volley框架中,缓存机制是一系列缓存实体的集合以及此集合对应的一系列操作。

       在回答了缓存机制的定义后,我们从其定义当中又可以延伸出一系列问题:缓存机制缓存的是什么?缓存实体是什么?缓存集合有哪些操作?

       而将这些问题放入整个Volley框架、甚至整个Android框架之中,我们有推出以下一些问题:缓存实体与Request对象、Response对象有什么关系?缓存机制和Android提供的一些列缓存算法,例如LruCache又有什么关联?

       下面我们从源码的角度入手,尝试着是否能够分析出上述问题的答案。

        首先,我们先贴出Cache接口的代码:

public interface Cache {
    /**
     * Retrieves 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);

    /**
     * Adds or replaces an entry to the cache.
     * 添加或者替换缓存中的一个实体
     * @param key Cache key
     * @param entry Data to store and metadata for cache coherency, TTL, etc.
     */
    public void put(String key, Entry entry);

    /**
     * Performs any potentially long-running actions needed to initialize the cache;
     * will be called from a worker thread.
     * 在工作线程中执行一系列可能耗时的动作,用以初始化缓存
     */
    public void initialize();

    /**
     * Invalidates an entry in the cache.
     * 使缓存中的某一个实体失效
     * @param key Cache key
     * @param fullExpire True to fully expire the entry, false to soft expire
     */
    public void invalidate(String key, boolean fullExpire);

    /**
     * Removes an entry from the cache.
     * 从缓存中删除一个实体
     * @param key Cache key
     */
    public void remove(String key);

    /**
     * Empties the cache.
     * 清空缓存
     */
    public void clear();<pre name="code" class="java">...
}


        Cache接口定义了六个抽象方法,也就是说定义了六个对缓存的操作;这六种操作分别为增(改)、删、查、清空、初始以及失效某个实体。而这六个操作就回答了上文”缓存机制有哪些操作?“这一问题。

       现在我们但看put和get两个方法,发现put方法的第二个入参及get方法的返回类型皆为Entry类型。显然这个Entry类型就是上文所说的缓存实体。

        因此就可以回答“缓存机制缓存的是什么?”这一问题,即缓存机制缓存的是Cache.Entry这一缓存实体。

        Entry类是Cache接口定义的一个内部类,其代码如下:

public interface Cache {
    ...

    /**
     * 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;

        /** TTL for this record.本记录的生存时间值 */
        public long ttl;

        /** Soft TTL for this record. 本记录的软件生存时间值*/
        public long softTtl;

        /** 从服务端接收的不可改变的响应头,不能为空*/
        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();
        }
    }

}
     Entry类中有两个属性较为重要,一个是responseHeaders,一个是data;我们可以根据HttpHeaderParser类中的静态方法parseCacheHeaders,源码如下:
public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
        ...
        //响应头
        Map<String, String> headers = response.headers;
        ...
        Cache.Entry entry = new Cache.Entry();
        entry.data = response.data;
        ...        
        entry.responseHeaders = headers;

        return entry;
    }
     parseCacheHeaders方法将NetworkResponse对象转换成一个Cache.Entry对象;通过上一篇文章【进阶android】Volley源码分析——Volley的线程可知在BasicNetwork类中的performRequet方法将原始的HttpResponse解析成中间产物NetworkResponse对象,而解析的方式是提取原始响应的响应头参数、响应内容以及响应码,以这三者生成一个新的NetWorkResponse对象。

      也就是说NetworkResponse对象中的data就是原始HTTP响应对象中的响应内容,headers就是原始HTTP响应对象中的响应头参数。

      根据parseCacheHeaders方法,我们可以这样认为:Cache.Entry对象中的data属性就是原始HTTP响应的响应内容的映射,CacheEntry对象中的responseHeaders就是原始HTTP响应头参数的映射,进一步而言,Cache.Entry就是原始HTTP响应的一种映射(响应内容和响应头参数都有关联)。

       从本质上来讲,Volley框架缓存机制缓存的是一个http响应。

       另一方面,Request类和Response类都有对Cache.Entry实体的引用;对于Request类而言,Cache.Entry是其缓存在本地的结果的一种体现;而对于Response类,其对象包含一个已经解析了的响应内容(通过Request子类的parseNetworkResponse方法)以及一个原始HTTP的响应内容,而Cache.Entry这个原始HTTP的响应内容的一种映射。

        至此,可以通过下图,来回答“缓存实体与Request对象、Response对象有什么关系?”这一问题:


       以上大致分析了Volley框架之中的缓存机制是什么;下面继续看看Volley框架是如何实现缓存机制的。

二、缓存机制的实现

       在Volley框架之中,默认通过DiskBasedCache类来实现缓存机制。DiskBasedCache主要是通过Disk,也就是磁盘来实现缓存,而具体的表现形式就是不同的文件。通过文件的形式实现缓存,缓存的生命周期比内存缓存的生命周期更长,即便退出应用,这些缓存也不会随之删除。

       DiskBasedCache通过文件映射Cache.Entry;并且,DisBasedCache也封装了两者之间的转换。

      提及两者之间的转换,就不得不说明DiskBashedCache类中的一个内部类CacheHeader,如果说Cahche.Entry被映射为文件,那么CacheHeader就是这个Cahche.Entry对象的一种摘要

       DiskBasedCache是基于文件来实现缓存机制,故而它是通过IO流的方式来进行存取文件内容的操作;众所周知,IO流是阻塞读写,因此除了必要的操作之外,例如将文件转换成Cache.Entry等;一些用以描述缓存实体的信息则可存入一段内存之中(这些信息例如文件的大小、缓存的生命周期等),这样当需要获取这些信息的时候,就可直接从内存中获取,而不用再通过IO方式从文件中读取了。而这段内存就是CacheHeader对象。

       DiskBasedCache间接的通过的CacheHeader来管理文件(Cache.Entry);DiskBasedCache之中有一个LinkedHashMap对象,该对象的元素就是CacheHeader。DiskBasedCache通过LinkedHashMap来管理CacheHeader,实现Lru(Least Recently Used )算法,从来间接管理文件缓存。

       通过上文对Cache接口的分析,确定了Cache接口之中缓存的是Cache.Entry对象这一结论,而在具体的Cache接口实现类DiskBasedCached之中,这一结论有一定的变异,在DiskBasedCache之中缓存的是CacheHeader,由CacheHeader指向一个Cache.Entry,也就是一个文件,因为文件是Cache.Entry的一个映射。

    如图所示:

    因此,在DiskBasedCache类中,缓存和缓存集合是两个不同的概念;缓存囊括了缓存集合和缓存文件,缓存集合则只是CacheHeader的集合。

       说了这么多,我们看看CacheHeader的源码:

static class CacheHeader {
        /** The size of the data identified by this CacheHeader. (This is not
         * serialized to disk. 原始响应内容的长度*/
        public long size;

        /** The key that identifies the cache entry. */
        public String key;

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

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

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

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

        /** Headers from the response resulting in this cache entry. */
        public Map<String, String> responseHeaders;

        private CacheHeader() { }

        /**
         * Instantiates a new CacheHeader object
         * @param key The key that identifies the cache entry
         * @param entry The cache entry.
         */
        public CacheHeader(String key, Entry entry) {
            this.key = key;
            this.size = entry.data.length;
            this.etag = entry.etag;
            this.serverDate = entry.serverDate;
            this.ttl = entry.ttl;
            this.softTtl = entry.softTtl;
            this.responseHeaders = entry.responseHeaders;
        }

        /**
         * Reads the header off of an InputStream and returns a CacheHeader object.
         * 文件转换成CacheHeader
         * @param is The InputStream to read from.
         * @throws IOException
         */
        public static CacheHeader readHeader(InputStream is) throws IOException {
            CacheHeader entry = new CacheHeader();
            int magic = readInt(is);
            if (magic != CACHE_MAGIC) {
                // don't bother deleting, it'll get pruned eventually
                throw new IOException();
            }
            entry.key = readString(is);
            entry.etag = readString(is);
            if (entry.etag.equals("")) {
                entry.etag = null;
            }
            entry.serverDate = readLong(is);
            entry.ttl = readLong(is);
            entry.softTtl = readLong(is);
            entry.responseHeaders = readStringStringMap(is);
            return entry;
        }

        /**
         * Creates a cache entry for the specified data.
         * CacheHeader到Cache.Entry的转换
         */
        public Entry toCacheEntry(byte[] data) {
            Entry e = new Entry();
            e.data = data;
            e.etag = etag;
            e.serverDate = serverDate;
            e.ttl = ttl;
            e.softTtl = softTtl;
            e.responseHeaders = responseHeaders;
            return e;
        }


        /**
         * Writes the contents of this CacheHeader to the specified OutputStream.
         * CacheHeader转换成文件
         */
        public boolean writeHeader(OutputStream os) {
            try {
                writeInt(os, CACHE_MAGIC);
                writeString(os, key);
                writeString(os, etag == null ? "" : etag);
                writeLong(os, serverDate);
                writeLong(os, ttl);
                writeLong(os, softTtl);
                writeStringStringMap(responseHeaders, os);
                os.flush();
                return true;
            } catch (IOException e) {
                VolleyLog.d("%s", e.toString());
                return false;
            }
        }

    }
       通过源码,我们可以得出CacheHeader与Cache.Entry的区别:Cache.Entry比CacheHeader多了一个data属性,Cache.Entry中该字段用以存储原始响应内容。

      而CahceHeader的方法之中,除了构造函数,其他的都是一些转换方法:

      文件转换成CacheHeader对象,此方法采用流的方式从文件之中读取内容,赋值给CacheHeader相应的属性;

      CacheHeader转换为Cache.Entry;

      CacheHeader转换为文件;此时转换的文件不包含原始的响应内容。

      分析完DiskBasedCache之中缓存实体的实现,我们接着分析其中缓存的实现。

       缓存,其实就是一些列缓存实体的集合,以及这个集合的一些基本操作。在DiskBasedCache之中,通过一个LinkedHashMap定义了这个集合。原型如下:

private final Map<String, CacheHeader> mEntries = new LinkedHashMap<String, CacheHeader>(16, .75f, true);
    下面进入集合的操作,DiskBasedCache之中绝大部分的方法都是封装了对缓存集合的操作,我们主要分析六个:
  • initialize,初始化缓存;
  • put,存入缓存;
  • get,从缓存之中获取;
  • remove,从缓存之中删除;
  • clear,清空缓存;
  • pruneIfNeeded,裁剪缓存。
1、initialize()
   方法的代码如下:
public synchronized void initialize() {
        if (!mRootDirectory.exists()) {//如果根目录不存在,创建根目录
            if (!mRootDirectory.mkdirs()) {
                VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
            }
            return;
        }

        File[] files = mRootDirectory.listFiles();
        if (files == null) {
            return;
        }
        //遍历所有的文件,将文件转换为CacheHeader对象,并将其添加到缓存集合之中
        for (File file : files) {
            FileInputStream fis = null;
            try {
                fis = new FileInputStream(file);
                CacheHeader entry = CacheHeader.readHeader(fis);
                entry.size = file.length();
                putEntry(entry.key, entry);//添加到缓存集合之中
            } catch (IOException e) {
                if (file != null) {
                   file.delete();//如果存在异常则删除异常文件
                }
            } finally {
                ...
            }
        }
    }
缓存的初始化主要有两方面;
    因为DiskBasedCache是一个基于文件的缓存,所以其对应一个根目录来存放这些文件,因而初始化后缓存时需要确保这个根目录存在,如果不存在则创建之。
    确定了根目录之后,就遍历根目录之下的文件,为每一个文件生成一个摘要,即CacheHeader对象,再将其存放到缓存集合之中。由于是遍历根目录之中的所有文件,并且采用IO流的方式获取文件内容,故而initialize方法可能会很耗时,故而一般不会再UI线程之中运行,Volley框架之中通过缓存线程来运行。
2、put(String key,Entry entry) 
   方法代码如下:
<span style="font-size:18px;">public synchronized void put(String key, Entry entry) {
        pruneIfNeeded(entry.data.length);
        File file = getFileForKey(key);//根据key找到缓存文件
        try {
            FileOutputStream fos = new FileOutputStream(file);
            CacheHeader e = new CacheHeader(key, entry);//通过Cache.Entry生成一个摘要(CacheHeader)
            e.writeHeader(fos);//将摘要写入缓存文件之中
            fos.write(entry.data);//将</span><span style="font-size:18px; font-family: Consolas; widows: auto;">原始响应内容</span><span style="widows: auto; font-family: Consolas;">写入缓存文件之中</span><span style="font-size:18px;">
            fos.close();
            putEntry(key, e);//将摘要添加到缓存集合之中
            return;
        } catch (IOException e) {
        }
        ......
    }</span>
添加缓存的方法也较为简单,先将需要缓存的内容(响应摘要和原始响应内容)分别写入缓存文件之中,再将摘要(CacheHeader)添加到缓存集合之中。
3、get(String key)
   方法代码如下:
public synchronized Entry get(String key) {
        CacheHeader entry = mEntries.get(key);//获取摘要
        ......
        File file = getFileForKey(key);//获取文件
        CountingInputStream cis = null;
        try {
            cis = new CountingInputStream(new FileInputStream(file));
            //eat header文件的具体内容在摘要之后,所有先读取摘要,将当前读取位置移动到摘要之后
            CacheHeader.readHeader(cis);          
            //当前位置移动到摘要之后,读取原始响应内容            byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead));
            return entry.toCacheEntry(data);//根据读取的原始响应内容,结合缓存摘要,生成Cache.Entry对象
        } catch (IOException e) {
            VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
            remove(key);//发生异常则删除异常缓存
            return null;
        } finally {
            ......
        }
    }

           get方法的逻辑也较为简单;总体而言,是要通过CacheHeader(摘要)和原始响应内容两者一起结合为一个Cache.Entry对象;其中CacheHeader来至于缓存集合之中,
原始响应内容则通过IO方式从缓存文件之中读取。
        需要注意一点,要读取缓存文件之中的原始响应内容,就必须从文件中原始响应内容第一个byte的位置开始读取,因而在读取原始响应内容之前,需要调用CacheHeader.readHeader(cis)方法将文件之中原始响应内容前面的摘要内容全部读取一遍,这样才能保证input流当前读取位置为文件中原始响应内容第一个byte所在的位置。
4、remove(String key)
        该方法的逻辑极为简单,就不罗列其源码了。
        该方法,首先根据key找到对应的缓存文件,将其删除;在删除缓存集合之中的缓存摘要。
5、clear()
       该方法的逻辑极为简单,就不罗列其源码了。
       该方法,首先清空根目录下方所有的文件,再清空缓存集合。
6、pruneIfNeeded(int neededSpace)
       DiskBasedCache使用了LRU的模式来维持缓存(注意是缓存,而非缓存集合)的大小(缓存的大小是指所有缓存文件中原始响应内容的长度之和),确保缓存的大小不会超过设定的最大值;如果超过,则删除最近最少使用的缓存。
       DiskBasedCache类中有一个变量mMaxCacheSizeInBytes用来限制缓存的大小,默认情况下,缓存的大小被限制为不超过5M,这个值开发人员可以自行设置。一旦缓存的大小超出了该值,则会调用pruneIfNeed方法裁剪集合。
       pruneIfNeeded方法代码如下:

private void pruneIfNeeded(int neededSpace) {
        if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
            //未超过缓存最大值限制,直接返回
            return;
        }
        ......
        Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
        //遍历缓存集合
        while (iterator.hasNext()) {
            Map.Entry<String, CacheHeader> entry = iterator.next();
            //删除缓存集合头部CacheHeader对象对应的缓存文件,
            CacheHeader e = entry.getValue();
            boolean deleted = getFileForKey(e.key).delete();
            if (deleted) {
                mTotalSize -= e.size;//删除头部元素后当前缓存的大小
            } else {
               ......
            }
            iterator.remove();//删除缓存集合头部CacheHeader对象
            ......
            if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
                //mTotalSize:当前缓存的大小
                //neededSpace:即将添加到缓存文件之中原始响应内容的大小
                //mMaxCacheSizeInBytes:缓存最大值限制
                //HYSTERESIS_FACTOR:缓存最高水准线因子(水准线与mMaxCacheSizeInBytes的比值,其默认值为0.9)
                break;
            }
        }
        ......
    }
           DiskedBasedCache类中,一般在添加缓存(put方法)时,调用此方法,判断算上即将添加的Cache.Entry后是否超过最大限制,如果超过,则进行缓存集合遍历,删除缓存集合头部CacheHeader对象,及其对应的缓存文件;然后在以删除缓存文件后的缓存大小为标准,算上即将添加的Cache.Entry大小,再次判断是否超过最大限制(的水准线)。注意此时判断的是水准线,而非最大限制。

       至此我们就将DiskBasedCache类中的操作分析完毕。

       结合DiskedBasedCache类中的属性和操作,我们就可以明白Volley框架中默认的缓存机制是如何实现的。最后在做一个总结:

       缓存机制:一系列缓存实体的集合以及此集合对应的一系列操作。

       缓存:一系列缓存实体的集合。

       DiskBasedBache中的缓存:缓存摘要集合(简称缓存集合)+缓存文件集合。

       至此,Volley框架里的缓存,我们就分析结束了。

       当然由于本人自身水平所限,文章肯定有一些不对的地方,希望大家指出!

    愿大家一起进步,谢谢!


版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

从源码带看Volley的缓存机制

Volley已默认使用磁盘缓存DiskBasedCache内部结构它由两部分组成,一部分是头部,一部分是内容;先得从它的内部静态类CacheHeader(缓存的头部信息)讲起,先看它的内部结构......

【进阶android】Volley源码分析——总述

本文将从三个方面来对Volley进行综述:Volley是什么?为什么要分析Volley?怎样分析Volley?    一、volley是什么?      volley,对于Android开发师,尤其是...

【进阶android】Volley源码分析——Volley的流程

本文章开始分析Volley的具体源代码了;首先介绍Volley的总体流程,文章总体分为三个部分:Request类的分析、RequestQueen类的分析以及Volley的总体流程。 一、Request...

【进阶android】Volley源码分析——Volley的线程

在上一篇文章中,我们主要分析了Volley一次网络请求的总体流程,并在此基础上初步分析了Request和RequestQueue两个Volley框架中较为重要的类。        而本片文章,将在上一...

【进阶android】Volley源码分析——Volley的工具【StringRequest】

通过【进阶android】Volley源码分析——Volley的流程、【进阶android】Volley源码分析——Volley的线程以及【进阶android】Volley源码分析——Volley的缓...

Android进阶——Volley+Https给你的安卓应用加上SSL证书

背景       作为开发人员,我们需要对网络访问的安全性加以保证,这样才能在基本上保证我们的数据不受到修改和攻击。笔者的项目之前用的是Volley框架访问的网络,基于http协议。现在我们需要使用更...

看Volley源码,对HTTP缓存机制分析

Volley是android官方实现的HTTP请求库,实现feizh
  • dxyoo7
  • dxyoo7
  • 2014年05月27日 16:06
  • 2962

4、Volley解析(二),源码的深入分析一,缓存线程和网络请求线程

前言首先来看一下谷歌的官方流程图
  • firea6
  • firea6
  • 2017年04月28日 15:21
  • 138

Android 谷歌 开源 通信框架 VOLLEY(五)——源码架构设计

我们已经知道了volley的种种功能,但是大家肯定不满足。volley是开源的,这就造福了亿万的程序员。 下面我们打开volley最后一层,深入架构设计。 当客户端在请求网络数据的时候,是需要消耗...

Volley源码分析(四)——ImageLoader

Volley框架中有一个ImageLoader类,用于加载图片,其使用方法如下: RequestQueue requestQueue = Volley.newRequestQueue(this);...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:【进阶android】Volley源码分析——Volley的缓存
举报原因:
原因补充:

(最多只允许输入30个字)