android 网络2 Volley(3) 缓存策略

既然上一篇写到这里索性就将Volley的缓存策略也一并分析了,Volley到底是怎么缓存资源的呢?

Volley中负责缓存的基类是Cache,它有三个子类

然后control + G (mac)看看他们在Volley中的引用如下

1)NoCache

2)MockCache

3)DiskBasedCache

从引用我们可以看到,在Volley网络框架中默认使用的是DiskBasedCache作为缓存策略,接下来我们便重点分析这个类,分析一下Volley网络框架的缓存策略。

DiskBasedCache从字面意思理解起来就是做磁盘缓存的,那么这个类到底做了什么工作了?

DiskBasedCache工作就是通过LinkedHashMap以键值对的形式保存资源,在上一篇讲到了,key值就是网络请求的url,值是CacheHeader,DiskBasedCache的一个静态内部类,如下:

CacheHeader的源码:

/**
 * Handles holding onto the cache headers for an entry.
 */
// Visible for testing.
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;

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

    /** 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.lastModified = entry.lastModified;
        this.ttl = entry.ttl;
        this.softTtl = entry.softTtl;
        this.responseHeaders = entry.responseHeaders;
    }

    /**
     * Reads the header off of an InputStream and returns a CacheHeader object.
     * @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.lastModified = readLong(is);
        entry.ttl = readLong(is);
        entry.softTtl = readLong(is);
        entry.responseHeaders = readStringStringMap(is);

        return entry;
    }

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


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

}

源码分析:

先看自变量:

/** The size of the data identified by this CacheHeader. (This is not * serialized to disk. 这个size是CacheHeader标识的数据大小。 (这不是序列化到磁盘*/
public long size;

/** The key that identifies the cache entry. 用来标识查找缓存条目的键*/ 

public String key;

/** ETag for cache coherence. ETag用于缓存一致性*/

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. 记录的TTL 过期时间*/

public long ttl;

/** Soft TTL for this record. 记录的Soft TTL 过期时间*/

public long softTtl;

看到这里不禁有些怪,为什么没有看到我们要缓存的数据,而只是一些指标。

接下来我们看起父类Cache中肯定就存在保存数据的变量,看下面源码:

/**
 * An interface for a cache keyed by a String with a byte array as data.
 */
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();

    /**
     * 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类的源码十分简单,同样有个静态内部类,Entry,里面有果然有byte[] data ,这便是用来存储数据的。

接下来看它被调用的的地方,我先猜测一下,既然这里Entry是Volley中缓存用的数据源,那么从软件设计的角度来看,他虽然存在于Cache中用来做缓存,那么首先应该在内存中存在一份,然后才是持久化到磁盘中,那么如果不做磁盘缓存,它便只能在内存中了,那既然内存中存在了,那么我们是不是不能浪费资源,也就是说没必要再去创建一个类作为保存数据的对象,所以这个Entry会被全局使用,而不会重复造出来一个,所以除了做缓存用,自然也会被直接使用,事实上我们看看其被调用的地方如下图,一目了然,HttpHeaderParser自然是用来做数据解析的,见下面源码,一目了然,这里不多做分析,我们主要看缓存中的使用,分析DiskBasedCache类

public class HttpHeaderParser {

    /**
     * Extracts a {@link Cache.Entry} from a {@link NetworkResponse}.
     *
     * @param response The network response to parse headers from
     * @return a cache entry for the given response, or null if the response is not cacheable.
     */
    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 = null;
        String headerValue;

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

        headerValue = headers.get("Cache-Control");
        if (headerValue != null) {
            hasCacheControl = true;
            String[] tokens = headerValue.split(",");
            for (int i = 0; i < tokens.length; i++) {
                String token = tokens[i].trim();
                if (token.equals("no-cache") || token.equals("no-store")) {
                    return null;
                } else if (token.startsWith("max-age=")) {
                    try {
                        maxAge = Long.parseLong(token.substring(8));
                    } catch (Exception e) {
                    }
                } else if (token.startsWith("stale-while-revalidate=")) {
                    try {
                        staleWhileRevalidate = Long.parseLong(token.substring(23));
                    } catch (Exception e) {
                    }
                } 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");

        // Cache-Control takes precedence over an Expires header, even if both exist and Expires
        // is more restrictive.
        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;
    }

    /**
     * Parse date in RFC1123 format, and return its value as epoch
     */
    public static long parseDateAsEpoch(String dateStr) {
        try {
            // Parse date in RFC1123 format if this header contains one
            return DateUtils.parseDate(dateStr).getTime();
        } catch (DateParseException e) {
            // Date in invalid format, fallback to 0
            return 0;
        }
    }

    /**
     * Retrieve a charset from headers
     *
     * @param headers An {@link java.util.Map} of headers
     * @param defaultCharset Charset to return if none can be found
     * @return Returns the charset specified in the Content-Type of this header,
     * or the defaultCharset if none can be found.
     */
    public static String parseCharset(Map<String, String> headers, String defaultCharset) {
        String contentType = headers.get(HTTP.CONTENT_TYPE);
        if (contentType != null) {
            String[] params = contentType.split(";");
            for (int i = 1; i < params.length; i++) {
                String[] pair = params[i].trim().split("=");
                if (pair.length == 2) {
                    if (pair[0].equals("charset")) {
                        return pair[1];
                    }
                }
            }
        }

        return defaultCharset;
    }

    /**
     * Returns the charset specified in the Content-Type of this header,
     * or the HTTP default (ISO-8859-1) if none can be found.
     */
    public static String parseCharset(Map<String, String> headers) {
        return parseCharset(headers, HTTP.DEFAULT_CONTENT_CHARSET);
    }
}

接下来我们看Entry被用到的地方,回头看上面DiskBasedCache的源码中有个方法toCacheEntry,看看做什么用

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

直接只是接受了一个data参数,然后赋值给了Entry的data变量,但是继续追toCacheEntry被调用的地方在DiskBasedCache的get方法,get方法是返回cache的资源给request的,所以这里是从本地file中读取数据并赋值给了Entry而已。那什么时候缓存的呢?

有个put方法,这个方法的工作内容便是将数据真正存储到本地文件中的,看源码:

/**
 * Puts the entry with the specified key into the cache.
 */
@Override
public synchronized void put(String key, Entry entry) {
    pruneIfNeeded(entry.data.length);
    File file = getFileForKey(key);
    try {
        BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
        CacheHeader e = new CacheHeader(key, entry);
        boolean success = e.writeHeader(fos);
        if (!success) {
            fos.close();
            VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
            throw new IOException();
        }
        fos.write(entry.data);
        fos.close();
        putEntry(key, e);
        return;
    } catch (IOException e) {
    }
    boolean deleted = file.delete();
    if (!deleted) {
        VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
    }
}

上面put方法中蓝色部分很清楚,先把头信息写入到文件中,然后把资源写入到文件中,get刚好是反过来读取文件到内存。put的调用刚好就是在NetworkDispatcher的run方法中。

缓存到此大概分析完毕,流程比较粗糙,源码有很多值得借鉴的地方,可以参照源码仔细研究。

猜你喜欢

转载自blog.csdn.net/weixin_36709064/article/details/81587861