Volley HTTP 缓存机制

Volley HTTP 缓存规则

在介绍Volley的HTTP缓存机制之前,我们首先来看一下HTTP HEADER中和缓存有关的字段有:

规则 字段 示例值 类型 作用
新鲜度 Expires Sat, 23 Jul 2016 03:34:17 GMT 响应 告诉客户端在过期时间之前可以使用副本
Cache-Control no-cache 响应 告诉客户端忽略资源的缓存副本,强制每次请求都访问服务器
no-store 响应 强制缓存在任何情况下都不要保留任何副本
must-revalidate 响应 表示必须进行新鲜度的再验证之后才能使用
max-age=[秒] 响应 指明缓存副本的有效时长,从请求时间到到期时间的秒数
Last-Modified Mon, 23 Jun 2014 08:43:26 GMT 响应 告诉客户端当前资源的最后修改时间
If-Modified-Since Mon, 23 Jun 2014 08:43:26 GMT 请求 如果浏览器第一次请求时响应的Last-Modified非空,第二次请求同一资源时,会把它作为该项的值发送给服务器
校验值 ETag 53a7e8ae-1f79 响应 告知客户端当前资源在服务器的唯一标识
If-None-Match 53a7e8ae-1f79 请求 如果浏览器第一次请求时响应中ETag非空,第二次请求同一资源时,会把它作为该项的值发给服务器

Volley的缓存机制

Volley的缓存机制在对HTTP RESPONSE的解析中能够明显的看出来:

public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
    long now = System.currentTimeMillis();

    Map<String, String> headers = response.headers;

    long serverDate = 0;
    long lastModified = 0;
    long serverExpires = 0;
    long softExpire = 0;
    long finalExpire = 0;
    long maxAge = 0;
    long staleWhileRevalidate = 0;
    boolean hasCacheControl = false;
    boolean mustRevalidate = false;

    String serverEtag;
    String headerValue;

    headerValue = headers.get("Date");
    if (headerValue != null) {
        serverDate = parseDateAsEpoch(headerValue);
    }

    // 获取响应体的Cache缓存策略.
    headerValue = headers.get("Cache-Control");
    if (headerValue != null) {
        hasCacheControl = true;
        String[] tokens = headerValue.split(",");
        for (String token : tokens) {
            token = token.trim();
            if (token.equals("no-cache") || token.equals("no-store")) {
                // no-cache|no-store代表服务器禁止客户端缓存,每次需要重新发送HTTP请求
                return null;
            } else if (token.startsWith("max-age=")) {
                // 获取缓存的有效时间
                try {
                    maxAge = Long.parseLong(token.substring(8));
                } catch (Exception e) {
                    maxAge = 0;
                }
            } else if (token.startsWith("stale-while-revalidate=")) {
                try {
                    staleWhileRevalidate = Long.parseLong(token.substring(23));
                } catch (Exception e) {
                    staleWhileRevalidate = 0;
                }
            } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
                // 需要进行新鲜度验证
                mustRevalidate = true;
            }
        }
    }

    // 获取服务器资源的过期时间
    headerValue = headers.get("Expires");
    if (headerValue != null) {
        serverExpires = parseDateAsEpoch(headerValue);
    }

    // 获取服务器资源最后一次的修改时间
    headerValue = headers.get("Last-Modified");
    if (headerValue != null) {
        lastModified = parseDateAsEpoch(headerValue);
    }

    // 获取服务器资源标识
    serverEtag = headers.get("ETag");

    // 计算缓存的ttl和softTtl
    if (hasCacheControl) {
        softExpire = now + maxAge * 1000;
        finalExpire = mustRevalidate
                ? softExpire
                : softExpire + staleWhileRevalidate * 1000;
    } else if (serverDate > 0 && serverExpires >= serverDate) {
        // Default semantic for Expire header in HTTP specification is softExpire.
        softExpire = now + (serverExpires - serverDate);
        finalExpire = softExpire;
    }

    Cache.Entry entry = new Cache.Entry();
    entry.data = response.data;
    entry.etag = serverEtag;
    entry.softTtl = softExpire;
    entry.ttl = finalExpire;
    entry.serverDate = serverDate;
    entry.lastModified = lastModified;
    entry.responseHeaders = headers;

    return entry;
}

这个方法其实是实现了Volley的本地缓存的关键代码.


L2级硬盘缓存的实现和缓存替换机制

之前介绍了用户使用LruCache实现自定义的L1级缓存,而Volley本身利用了FIFO算法实现了L2级硬盘缓存.接下来,就详细介绍一下硬盘缓存的实现和缓存替换机制.

这里我们也是考虑如果自己实现硬盘缓存,需要实现哪几个步骤:

  1. 抽象出存储实体类.
  2. 定义抽象存储接口,包括initialize,get,put,clear等具体缓存系统的操作.
  3. 对象的序列化.

存储实体

存储的实体肯定是响应的结果,响应结果分为响应头和响应体,抽象类代码如下所示:

/** 真正HTTP请求缓存实体类. */
class Entry {
    /** HTTP响应Headers. */
    public Map<String, String> responseHeaders = Collections.emptyMap();

    /** HTTP响应体. */
    public byte[] data;

    /** 服务器资源标识ETag. */
    public String etag;

    /** HTTP响应时间. */
    public long serverDate;

    /** 缓存内容最后一次修改的时间. */
    public long lastModified;

    /** Request的缓存过期时间. */
    public long ttl;

    /** Request的缓存新鲜时间. */
    public long softTtl;

    /** 判断缓存内容是否过期. */
    public boolean isExpired() {
        return this.ttl < System.currentTimeMillis();
    }

    /** 判断缓存是否新鲜,不新鲜的缓存需要发到服务端做新鲜度的检测. */
    public boolean refreshNeeded() {
        return this.softTtl < System.currentTimeMillis();
    }
}

抽象缓存系统类

public interface Cache {
    /** 通过key获取请求的缓存实体. */
    Entry get(String key);

    /** 存入一个请求的缓存实体. */
    void put(String key, Entry entry);

    void initialize();

    void invalidate(String key, boolean fullExpire);

    /** 移除指定的缓存实体. */
    void remove(String key);

    /** 清空缓存. */
    void clear();
}

在Volley中,实现Cache接口的硬盘缓存类是DiskBasedCache.接下来,具体介绍每个方法的具体实现.

构造函数

我们先来看一下DiskBasedCache的构造函数实现:

public DiskBasedCache(File rootDirectory) {
    this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
}

public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
    mRootDirectory = rootDirectory;
    mMaxCacheSizeInBytes = maxCacheSizeInBytes;
}

类似于LruCache,DiskBasedCache的构造函数做了两件事:

  1. 指定硬盘缓存的目录.
  2. 指定硬盘缓存的大小,默认为5M.

initialize函数

在介绍put和get函数之前,先介绍一下硬盘缓存的初始化函数,这个函数主要是用来遍历缓存的文件,从而获取当前缓存大小,和构造

/**
 * 初始化Disk缓存系统.
 * 作用是:遍历Disk缓存系统,将缓存文件中的CacheHeader和key存储到Map对象中.
 */
public void initialize() {
    if (!mRootDirectory.exists() && !mRootDirectory.mkdirs()) {
        // 硬盘缓存目录不存在直接返回即可
        return;
    }

    // 获取硬盘缓存目录所有文件集合.每个HTTP请求结果对应一个文件.
    File[] files = mRootDirectory.listFiles();
    if (files == null) {
        return;
    }

    for (File file : files) {
        BufferedInputStream fis = null;
        try {
            fis = new BufferedInputStream(new FileInputStream(file));
            // 进行对象反序列化
            CacheHeader entry = CacheHeader.readHeader(fis);
            // 将文件的大小赋值给entry.size,单位字节
            entry.size = file.length();
            // 在内存中维护一张硬盘<key,value>映射表
            putEntry(entry.key, entry);
        }catch (IOException e) {
            file.delete();
            e.printStackTrace();
        }finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException ignored) {
                }
            }
        }
    }
}

/** 将key和CacheHeader存入到Map对象中.并更新当前占用的总字节数. */
private void putEntry(String key, CacheHeader entry) {
    if (!mEntries.containsKey(key)) {
        mTotalSize += entry.size;
    } else {
        CacheHeader oldEntry = mEntries.get(key);
        mTotalSize += (entry.size - oldEntry.size);
    }

    mEntries.put(key, entry);
}

put函数

接下来我们讲解put函数,是因为一个缓存系统最为关键的操作就是put,这其中还设计到缓存替换策略的实现.

首先是缓存替换策略.

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 e = entry.getValue();
        // 这里的替换策略不太好,其实可以按照serverDate排序,从而实现FIFO的缓存替换策略.
        boolean deleted = getFileForKey(e.key).delete();
        if (deleted) {
            mTotalSize -= e.size;
        }
        iterator.remove();

        // 当硬盘大小满足可以存放新的HTTP请求结果时,停止删除操作
        if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
            break;
        }
    }
}

接下来,是硬盘缓存的插入操作,准备是对象序列化的一些内容.

/** 将Cache.Entry存入到指定的缓存文件中. 并在Map中记录<key,CacheHeader>. */
@Override
public synchronized void put(String key, Entry entry) {
    pruneIfNeeded(entry.data.length);
    // 根据HTTP的url生成缓存文件(ps:根据hash值生成文件名)
    File file = getFileForKey(key);
    try {
        BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
        // 这里有个bug,插入时的size只计算响应体,没有考虑响应头部缓存字段的大小.
        CacheHeader e = new CacheHeader(key, entry);
        boolean success = e.writeHeader(fos);
        if (!success) {
            fos.close();
            throw new IOException();
        }
        fos.write(entry.data);
        fos.close();
        putEntry(key, e);
        return;
    } catch (IOException e) {
        e.printStackTrace();
    }
    file.delete();
}

get函数

get函数比较简单了,源码如下:

/** 从Disk中根据key获取并构造HTTP响应体Cache.Entry. */
@Override
public synchronized Entry get(String key) {
    CacheHeader entry = mEntries.get(key);
    if (entry == null) {
        return null;
    }

    File file = getFileForKey(key);
    CountingInputStream cis = null;
    try {
        cis = new CountingInputStream(new BufferedInputStream(new FileInputStream(file)));
        // 读完CacheHeader部分,并通过CountingInputStream的bytesRead成员记录已经读取的字节数.
        CacheHeader.readHeader(cis);
        // 读取缓存文件存储的HTTP响应体内容.
        byte[] data = streamToBytes(cis, (int)(file.length() - cis.bytesRead));
        return entry.toCacheEntry(data);
    } catch (IOException e) {
        remove(key);
        return null;
    } finally {
        if (cis != null) {
            try {
                cis.close();
            } catch (IOException ignored) {
            }
        }
    }
}

clear函数

clear顾名思义,就是清空硬盘缓存的操作:

public synchronized void clear() {
    File[] files = mRootDirectory.listFiles();
    if (files != null) {
        for (File file : files) {
            file.delete();
        }
    }
    mEntries.clear();
    mTotalSize = 0;
}

所做的事情也比较简单,包括:

  1. 情况缓存文件.
  2. 将使用size置为0.
  3. 清空内存中维护的硬盘

remove函数

remove函数也就是删除指定key对应的硬盘缓存,代码很简单:

@Override
public synchronized void remove(String key) {
    boolean deleted = getFileForKey(key).delete();
    removeEntry(key);
    if (!deleted) {
        Log.e("Volley", "没能删除key=" + key + ", 文件名=" + getFilenameForKey(key) + "缓存.");
    }
}

/** 从Map对象中删除key对应的键值对. */
private void removeEntry(String key) {
    CacheHeader entry = mEntries.get(key);
    if (entry != null) {
        mTotalSize -= entry.size;
        mEntries.remove(key);
    }
}
时间: 2024-11-09 06:40:50

Volley HTTP 缓存机制的相关文章

从源码带看Volley的缓存机制

转载请注明出处:http://blog.csdn.net/asdzheng/article/details/45955653 磁盘缓存DiskBasedCache 如果你还不知道volley有磁盘缓存的话,请看一下我的另一篇博客请注意,Volley已默认使用磁盘缓存 DiskBasedCache内部结构 它由两部分组成,一部分是头部,一部分是内容:先得从它的内部静态类CacheHeader(缓存的头部信息)讲起,先看它的内部结构: responseHeaders; } //可以看到,头部类里包含

浏览器缓存机制浅析

非HTTP协议定义的缓存机制 浏览器缓存机制,其实主要就是HTTP协议定义的缓存机制(如: Expires: Cache-control等).但是也有非HTTP协议定义的缓存机制,如使用HTML Meta 标签,Web开发者可以在HTML页面的<head>节点中加入<meta>标签,代码如下: <META HTTP-EQUIV="Pragma" CONTENT="no-cache"> 上述代码的作用是告诉浏览器当前页面不被缓存,每

Hibernate 缓存机制

一.why(为什么要用Hibernate缓存?) Hibernate是一个持久层框架,经常访问物理数据库. 为了降低应用程序对物理数据源访问的频次,从而提高应用程序的运行性能. 缓存内的数据是对物理数据源中的数据的复制,应用程序在运行时从缓存读写数据,在特定的时刻或事件会同步缓存和物理数据源的数据. 二.what(Hibernate缓存原理是怎样的?)Hibernate缓存包括两大类:Hibernate一级缓存和Hibernate二级缓存. 1.Hibernate一级缓存又称为“Session的

Android开源框架ImageLoader:加载图片的三级缓存机制

前言:可从  https://github.com/nostra13/Android-Universal-Image-Loader 下载三级缓存机制的开源框架.下文简单介绍该框架中主要的常用方法,掌握这些方法,基本就可应对多数图片下载的需求. 注意:以下代码为示意代码片断,仔细读一下应能知道怎么用.蓝色表示为开源框架中的类. 1.初始化ImageLoader类对象: ImageLoader imageLoader = ImageLoader.getInstance(); imageLoader.

Varnish缓存机制详细介绍及简单配置

Varnish是一款高性能的开源HTTP加速器,其主要用来做为反向代理中的缓存服务器使用,但其实Varnish本身也是具有反向代理功能的,但在创建连接和维持连接上,与Nginx相比差距很大,现在有一个很流行的架构就是前端用Nginx作为反向代理,后面加Varnish缓存服务器为Web服务加速 在将Varnish前先谈谈我们的浏览器缓存机制,现在的浏览器基本都具有缓存功能,它能将我们以前访问过的静态内容和可进行缓存的动态内容缓存再本地,而后在下次访问相同资源时,如果可以确认Server端的资源未发

【腾讯Bugly干货分享】彻底弄懂 Http 缓存机制 - 基于缓存策略三要素分解法

本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:https://mp.weixin.qq.com/s/qOMO0LIdA47j3RjhbCWUEQ 作者:李志刚 导语 Http 缓存机制作为 web 性能优化的重要手段,对从事 Web 开发的小伙伴们来说是必须要掌握的知识,但最近我遇到了几个缓存头设置相关的题目,发现有好几道题答错了,有的甚至在知道了正确答案后依然不明白其原因,可谓相当的郁闷呢!!为了确认下是否只是自己理解不深,我特意请教了其他几位小伙

Volley超时重试机制详解

Volley超时重试机制 基础用法 Volley为开发者提供了可配置的超时重试机制,我们在使用时只需要为我们的Request设置自定义的RetryPolicy即可. 参考设置代码如下: int DEFAULT_TIMEOUT_MS = 10000; int DEFAULT_MAX_RETRIES = 3; StringRequest stringRequest = new StringRequest(Request.Method.GET, url, new Response.Listener<S

hibernate缓存机制详细分析 复制代码

您可以通过点击 右下角 的按钮 来对文章内容作出评价, 也可以通过左下方的 关注按钮 来关注我的博客的最新动态. 如果文章内容对您有帮助, 不要忘记点击右下角的 推荐按钮 来支持一下哦 如果您对文章内容有任何疑问, 可以通过评论或发邮件的方式联系我: [email protected] / [email protected] 如果需要转载,请注明出处,谢谢!! 在本篇随笔里将会分析一下hibernate的缓存机制,包括一级缓存(session级别).二级缓存(sessionFactory级别)以

Java缓存学习之三:CDN缓存机制

CDN是什么? 关于CDN是什么,此前网友详细介绍过. CDN是Content Delivery Network的简称,即"内容分发网络"的意思.一般我们所说的CDN加速,一般是指网站加速或者用户下载资源加速. 举个通俗的例子: 谈到CDN的作用,可以用8年买火车票的经历来形象比喻:8年前,还没有火车票代售点一说,12306.cn更是无从说起.那时候火车票还只能在火车站的售票大厅购买,而我所住的小县城并不通火车,火车票都要去市里的火车站购买,而从县城到市里,来回就是4个小时车程,简直就