Volley中的缓存策略

Volley实现了对请求响应的结果进行磁盘缓存,这样就不用每次都从服务器上请求数据。Volley的缓存是在DiskBasedCache这个类中实现的,它是在创建请求队列的时候被创建出来的。

private static final String DEFAULT_CACHE_DIR = "volley";
File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);

//DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024 
public DiskBasedCache(File rootDirectory) {
    this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
}

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

Volley的默认缓存路径是在APP的cache目录下的volley目录中File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);DEFAULT_CACHE_DIR = "volley"。缓存的默认大小是5M。

当CacheDispatcher这个线程被启动的时候,就会调用DiskBasedCache.initialize()方法来初始化一遍,从本地缓存文件中来读取缓存数据到内存中。

@Override
public void run() {
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    //Cache线程启动后就开始初始化缓存
    mCache.initialize();
    ...
}

进入initialize()看看是如何初始化缓存的

@Override
public synchronized void initialize() {
    //如果缓存的volley目录不存在旧创建
    if (!mRootDirectory.exists()) {
        if (!mRootDirectory.mkdirs()) {
            VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
        }
        return;
    }
    //扫描volley目录下的缓存文件
    File[] files = mRootDirectory.listFiles();
    if (files == null) {
        return;
    }
    //遍历这些缓存文件
    for (File file : files) {
        try {
            //文件大小
            long entrySize = file.length();
            CountingInputStream cis = new CountingInputStream(
                    new BufferedInputStream(createInputStream(file)), entrySize);
            try {
                //读取缓存文件中的数据
                CacheHeader entry = CacheHeader.readHeader(cis);
                entry.size = entrySize;
                //将数据保存进map集合中
                putEntry(entry.key, entry);
            } finally {
                cis.close();
            }
        } catch (IOException e) {
            file.delete();
        }
    }
}

这里使用了CacheHeader这个类来描述每一个缓存文件的头部信息,在CacheHeader中有如下描述字段:

    //缓存文件大小
    public long size;
    //缓存对应的标识,一般为request请求的url
    public String key;
    //与缓存相关的header标签,一般标识一个服务器资源的hash tag
    public String etag;
    //收到响应的时间
    public long serverDate;
    //资源文件最后修改的时间
    public long lastModified;
    //缓存过期的时间
    public long ttl;
    //整个比较特别,可以理解为缓存可能失效的时间
    public long softTtl;
    //响应中的头部
    public Map<String, String> responseHeaders;

Volley的缓存信息中,ttl和softTtl这连个失效时间容易混淆,ttl还好说,就是失效时间,只要时间已过,就会走网络请求这一条路,不会使用缓存。softTtl表示缓存可能失效了,但仍然继续使用缓存,不过还需要走网络请求这一条路在更新一次。看看代码就清楚了。

//CacheDiapatcher.java
@Override
public void run() {
    ...
    while (true) {
        try {
            ...
            //缓存过期,直接走网络请求,都不同缓存数据
            if (entry.isExpired()) {
                request.addMarker("cache-hit-expired");
                request.setCacheEntry(entry);
                mNetworkQueue.put(request);
                continue;
            }
            request.addMarker("cache-hit");
            Response<?> response = request.parseNetworkResponse(
                    new NetworkResponse(entry.data, entry.responseHeaders));
            request.addMarker("cache-hit-parsed");

            if (!entry.refreshNeeded()) {
                mDelivery.postResponse(request, response);
            } else {
                ...
                //缓存可能过期了,先将就一下用着吧
                mDelivery.postResponse(request, response, new Runnable() {
                    @Override
                    public void run() {
                        try {
                            //这里再走一边网络请求
                            mNetworkQueue.put(request);
                        } catch (InterruptedException e) {
                        }
                    }
                });
            }
            ...
    }
}
//Entry
public boolean isExpired() {
    return this.ttl < System.currentTimeMillis();
}
public boolean refreshNeeded() {
    return this.softTtl < System.currentTimeMillis();
}

在CacheDispatcher中处理缓存请求的时候,通过entry.isExpired()判断缓存数据如果过期了就直接走网络请求,通过entry.refreshNeeded()判断如果缓存可能过期了会先使用缓存数据,再走一遍网络请求。

CacheHeader只保存了缓存数据的关键头信息,每次获取真正的缓存数据都是从文件中读取的。获取缓存数据是在get()里面完成的。

@Override
public synchronized Entry get(String key) {
    //1.获取缓存数据头信息
    CacheHeader entry = mEntries.get(key);
    //2.没有则返回null
    if (entry == null) {
        return null;
    }
    //获取缓存文件,这里的key实际上就是请求的url,这里会通过这个url计算出一个hash值来作为文件名称
    File file = getFileForKey(key);
    CountingInputStream cis = null;
    try {
        //3.CountingInputStream是对FileInputStream的封装,里面记录了读取的数据大小
        cis = new CountingInputStream(new BufferedInputStream(new FileInputStream(file)));
        //4.跳过对缓存数据的头信息
        CacheHeader.readHeader(cis); // eat header
        //5.获取真正的缓存数据
        byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead));
        //6.返回一个Entry对象,缓存数据就放在了这个对象
        return entry.toCacheEntry(data);
    } catch (IOException e) {
        remove(key);
        return null;
    }  catch (NegativeArraySizeException e) {
        remove(key);
        return null;
    } finally {
        if (cis != null) {
            try {
                cis.close();
            } catch (IOException ioe) {
                return null;
            }
        }
    }
}

对缓存数据的一些描述信息,比如失效时间、文件大小等都是直接保存在文件的开头的,所以在获取缓存数据的时候需要跳过这些头。Entry对象除了包含了CacheHeader这些头信息,还包含了实际的缓存数据。

public static class Entry {
    //缓存数据
    public byte[] data;
    public String etag;
    public long serverDate;
    public long lastModified;
    public long ttl;
    public long softTtl;
    public Map<String, String> responseHeaders = Collections.emptyMap();

    public boolean isExpired() {
        return this.ttl < System.currentTimeMillis();
    }
    public boolean refreshNeeded() {
        return this.softTtl < System.currentTimeMillis();
    }
}

既然获取缓存数据是在get()中,那么应该猜到了添加一个缓存数据就是在put()中了。

@Override
public synchronized void put(String key, Entry entry) {
    //1.检查缓存空间是否还能放入数据,不行就删掉一些
    pruneIfNeeded(entry.data.length);
    //2.声明定义一个缓存文件
    File file = getFileForKey(key);
    try {
        BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
        //3.读取缓存entry中的缓存描述头信息
        CacheHeader e = new CacheHeader(key, entry);
        //4.写入头信息
        boolean success = e.writeHeader(fos);
        if (!success) {
            fos.close();
            VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
            throw new IOException();
        }
        //5.写入缓存数据
        fos.write(entry.data);
        fos.close();
        //6.将缓存的头信息保存在map中
        putEntry(key, e);
        return;
    } catch (IOException e) {
    }
    boolean deleted = file.delete();
    if (!deleted) {
        VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
    }
}

在pruneIfNeeded()会检查当前的缓存空间是否还能够放入这些缓存数据,如果不行就删除前面的缓存数据,直到还剩下10%的缓存空间。

private void pruneIfNeeded(int neededSpace) {
    //如果还能放入数据就退出
    if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
        return;
    }

    long before = mTotalSize;
    int prunedFiles = 0;
    long startTime = SystemClock.elapsedRealtime();

    Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
    while (iterator.hasNext()) {
        Map.Entry<String, CacheHeader> entry = iterator.next();
        CacheHeader e = entry.getValue();
        //从第一个缓存文件开始删除
        boolean deleted = getFileForKey(e.key).delete();
        if (deleted) {
            mTotalSize -= e.size;
        } else {
           VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
                   e.key, getFilenameForKey(e.key));
        }
        iterator.remove();
        prunedFiles++;
        //如果删除后缓存空间剩余10%以上就不删除了
        if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
            break;
        }
    }
}

读取缓存和保存缓存已经说了,那么这些缓存数据的描述头信息是在哪里处理的呢。Volley默认提供了好几种Request,在这些Request中都会实现parseNetworkResponse()方法,

protected Response<T> parseNetworkResponse(NetworkResponse response)

该方法是将一个NetworkResponse对象转换为Response对象,在其内部都会调用一个方法HttpHeaderParser.parseCacheHeaders(response),它会从NetworkResponse中读取这些缓存的关键描述头信息,然后返回一个Entry对象,这样就可以调用DiskBasedCache的put()方法来保存缓存数据了。进入HttpHeaderParser.parseCacheHeaders()看看是如何实现的。

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");

    if (hasCacheControl) {
        softExpire = now + maxAge * 1000;
        finalExpire = mustRevalidate
                ? softExpire
                : softExpire + staleWhileRevalidate * 1000;
    } else if (serverDate > 0 && serverExpires >= serverDate) {
        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;
}

在parseCacheHeaders()内部实际就是解析相应中的响应头信息,找到对应的关键信息保存在Entry对象中返回的。

Volley的缓存机制大致就说这么多了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值