继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Android Volley 源码解析(二),探究缓存机制

developerHaoz
关注TA
已关注
手记 12
粉丝 7878
获赞 786

前言

在上一篇文章中,带大家阅读了 Volley 网络请求的执行流程,算是对 Volley 有了一个比较清晰的认识,从这篇文章开始,我们开始针对 Volley 的某个功能进行深入地分析,慢慢将 Volley 的各项功能进行全面把握。

我们先从缓存这一块的内容开始入手,不过今天的缓存分析是是建立在上一篇源码分析的基础上的,还没有看过上一篇文章的朋友,建议先去阅读 Android Volley 源码解析(一),网络请求的执行流程

一、Volley 缓存的总体设计


在开始细节分析之前,我们先来看下 Volley 缓存的设计,了解这个流程有助于我们对于缓存细节的把握。Volley 提供了一个 Cache 作为缓存的接口,封装了缓存的实体 Entry,以及一些常规的增删查操作。

public interface Cache {

    Entry get(String key);

    void put(String key, Entry entry);

    void initialize();

    /**
     * 使缓存中的 Entry 失效
     */
    void invalidate(String key, boolean fullExpire);

    void remove(String key);

    void clear();

    /**
     * 用户缓存的实体
     */
    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 List<Header> allResponseHeaders;

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

        /** 判断 Entry 是否需要刷新. */
        public boolean refreshNeeded() {
            return this.softTtl < System.currentTimeMillis();
        }
    }

}

Entry 里面主要是放网络响应的原始数据 data、跟缓存相关的属性以及对应的响应头,作为缓存的一个实体。Cache 的具体实现类是 DiskBaseCache,它实现了 Cache 接口,并实现了响应的方法,那我们就来看看 DiskBaseCache 的设计吧,我们先看下 DiskBaseCache 中的一个静态内部类 CacheHeader.

    static class CacheHeader {

        long size;

        final String key;

        final String etag;

        final long serverDate;

        final long lastModified;

        final long ttl;

        final long softTtl;

        final List<Header> allResponseHeaders;

        private CacheHeader(String key, String etag, long serverDate, long lastModified, long ttl,
                           long softTtl, List<Header> allResponseHeaders) {
            this.key = key;
            this.etag = ("".equals(etag)) ? null : etag;
            this.serverDate = serverDate;
            this.lastModified = lastModified;
            this.ttl = ttl;
            this.softTtl = softTtl;
            this.allResponseHeaders = allResponseHeaders;
        }

        CacheHeader(String key, Entry entry) {
            this(key, entry.etag, entry.serverDate, entry.lastModified, entry.ttl, entry.softTtl,
                    getAllResponseHeaders(entry));
            size = entry.data.length;
        }
    }

DiskBaseCache 的设计很巧妙,它在内部放入了一个静态内部类 CacheHeader,我们可以发现这个类跟 Cache 的 Entry 非常像,是不是会觉得好像有点多余,Volley 之所以要这样设计,主要是为了缓存的合理性。我们知道每一个应用都是有一定内存限制的,程序占用了过高的内存就容易出现 OOM(Out of Memory),如果每一个请求都原封不动的把所有的信息都缓存到内存中,这样是非常占内存的。

我们可以发现 CacheHeader 和 Entry 最大的区别,其实就是是否有 byte[] data 这个属性,data 代表网络响应的元数据,是返回的内容中最占地方的东西,所以 DiskBaseCache 重新抽象了一个不包含 data 的 CacheHeader,并将其缓存到内存中,而 data 部分便存储在磁盘缓存中,这样就能最大程度的利用有限的内存空间。代码如下:

    BufferedOutputStream fos = new BufferedOutputStream(createOutputStream(file));
    CacheHeader e = new CacheHeader(key, entry);
    boolean success = e.writeHeader(fos);
    // 将 entry.data 写入磁盘中
    fos.write(entry.data);
    fos.close();
    // 将 Cache 缓存到内存中
    putEntry(key, e);

二、DiskBaseCache 的具体实现


看完了 Volley 的缓存设计,我们接着看 DiskBaseCache 的具体实现。

2.1 初始化缓存

  // 内存缓存的目录
  private final File mRootDirectory;

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

  @Override
  public synchronized void initialize() {
      // 如果 mRootDirectroy 不存在,则进行创建
      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;
      }
      // 遍历 mRootDirectory 中的所有文件
      for (File file : files) {
          try {
              long entrySize = file.length();
              CountingInputStream cis = new CountingInputStream(
                      new BufferedInputStream(createInputStream(file)), entrySize);
              // 将对应的文件缓存到内存中
              CacheHeader entry = CacheHeader.readHeader(cis);
              entry.size = entrySize;
              putEntry(entry.key, entry);
          } catch (IOException e) {
              file.delete();
          }
      }
  }

通过外部传入的 rootDirectory 和 maxCacheSizeInBytes 构造 DiskBaseCache 的实例,mRootDirectory 代表我们内存缓存的目录,maxCacheSizeInBytes 代表磁盘缓存的大小,默认是 5M。如果 mRootDirectory 为 null,则进行创建,然后将 mRootDirectory 中的所有文件进行内存缓存。

2.2 put() 方法的实现

    @Override
    public synchronized void put(String key, Entry entry) {
        pruneIfNeeded(entry.data.length);
        File file = getFileForKey(key);
        try {
            BufferedOutputStream fos = new BufferedOutputStream(createOutputStream(file));
            CacheHeader e = new CacheHeader(key, entry);
            boolean success = e.writeHeader(fos);
            fos.write(entry.data);
            fos.close();
            putEntry(key, e);
            return;
        } catch (IOException e) {
        }
    }

    private void pruneIfNeeded(int neededSpace) {
        // 如果内存还够用,就直接 return.
        if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
            return;
        }

        long before = mTotalSize;
        int prunedFiles = 0;

        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;
            } 
            iterator.remove();
            prunedFiles++;

            // 如果删除文件后,存储空间已经够用了,就停止循环
            if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
                break;
            }
        }
    }

可以看到 Volley 的代码实现是相当完善的,在添加缓存之前,先调用 pruneIfNeed() 方法进行内存空间的判断和处理,如果不进行限制的话,内存占用将无限制的增大,最后到达 SD 卡容量时,会发生无法写入的异常(因为存储空间满了)。

这里有一点要补充一下,Volley 在缓存方面,主要是使用了 LRU(Least Recently Used)算法,LRU 算法是最近最少使用算法,它的核心思想是当缓存满时,优先淘汰那些近期最少使用的缓存对象。主要的算法原理是把最近使用的对象用强引用的方式(即我们平常使用的对象引用方式)存储在 LinkedHashMap 中,当缓存满时,把最近最少使用的对象从内存中移除。有关 LRU 算法,可以看下这篇文章:彻底解析 Android 缓存机制 —— LruCache

在进行内存空间的判断之后,便将 entry.data 保存在磁盘中,将 CacheHeader 缓存在内存中,这样 DiskBaseCache 的 put() 方法就完成了。

2.3 get() 方法的实现

既然是缓存功能,必然有用于进行缓存的 key,我们来看下 Volley 的缓存 key 是怎么生成的。

    private String getFilenameForKey(String key) {
        int firstHalfLength = key.length() / 2;
        String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode());
        localFilename += String.valueOf(key.substring(firstHalfLength).hashCode());
        return localFilename;
    }

Volley 的缓存 key 的生成方法还是很骚的,将网络请求的 Url 分成两半,然后将这两部分的 hashCode 拼接成缓存 key。Volley 之所以要这样做,主要是为了尽量避免 hashCode 重复造成的文件名重复,求两次 hashCode 都与另外一个 Url 相同的概率比只求一次要小很多,不过概率小不代表不存在,但是 Java 在计算 hashCode 的速度是非常快的,这应该是 Volley 在权衡了安全性和效率之后做出的决定,这个思想是很值得我们学习的。

    @Override
    public synchronized Entry get(String key) {
        CacheHeader entry = mEntries.get(key);
        if (entry == null) {
            return null;
        }
        File file = getFileForKey(key);
        try {
            CountingInputStream cis = new CountingInputStream(
                    new BufferedInputStream(createInputStream(file)), file.length());
            try {
                CacheHeader entryOnDisk = CacheHeader.readHeader(cis);
                if (!TextUtils.equals(key, entryOnDisk.key)) {
                    // 一个文件可能映射着两个不同的 key,保存在不同的 Entry 中
                    removeEntry(key);
                    return null;
                }
                byte[] data = streamToBytes(cis, cis.bytesRemaining());
                return entry.toCacheEntry(data);
            } finally {
                cis.close();
            }
        } catch (IOException e) {
            remove(key);
            return null;
        }
    }

我们在上面说道,Volley 将响应的 data 放在磁盘中,将 CacheHeader 缓存在内存中,而 get() 方法其实就是这个过程的逆过程,先通过 key 从 mEntries 从取出 CacheHeader,如果为 null,就直接返回 null,否则通过 key 来获取磁盘中的 data,并通过 entry.toCacheEntry(data) 将 CacheHeader 和 data 拼接成完整的 Entry 然后进行返回。

三、DiskBaseCache 在 Volley 中的使用


看完了 DiskBaseCache 的具体实现,我们最后看下 DiskBaseCache 在 Volley 中是怎么使用的,这样就能把 Volley 的缓存机制全部串联起来了。

3.1 DiskBaseCache 的构建

    private static RequestQueue newRequestQueue(Context context, Network network) {
        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
        RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
        queue.start();
        return queue;
    }

应该还记得 Volley 的基本使用方法吧,当时我们第一步就是使用 Volley.newRequestQueue() 来创建一个 RequestQueue,这也是一切的起点。可以看到我们先通过 context.getCacheDir() 获取缓存路径,然后创建我们缓存所需的目录 cacheDir,这其实就是在 DiskBaseCache 中的 mRootDirectory,然后将其传入 DiskBaseCache 只有一个参数的构造器中,创建了 DiskBaseCache 的实例,默认的内存缓存空间是 5M.

    private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;

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

3.2 initialize() 方法的调用

public class CacheDispatcher extends Thread {

    @Override
    public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        mCache.initialize();

        while (true) {
            try {
                processRequest();
            } catch (InterruptedException e) {
            }
        }
    }
}

initialize() 是在 CacheDispatcher 中的 run 方法进行调用的,CacheDispatcher 是处理缓存队列中请求的线程。实例化 DiskBaseCache 之后,便在 while(true) 这个无线的循环当中,不断地等请求的到来,然后执行请求。

3.3 put() 方法的调用

public class NetworkDispatcher extends Thread {

    private void processRequest() throws InterruptedException {
        Request<?> request = mQueue.take();

        try {
            NetworkResponse networkResponse = mNetwork.performRequest(request);
            Response<?> response = request.parseNetworkResponse(networkResponse);

            if (request.shouldCache() && response.cacheEntry != null) {
                mCache.put(request.getCacheKey(), response.cacheEntry);
                request.addMarker("network-cache-written");
            }
    }
}

可以看到 put() 方法是在 NetworkDispatcher 中进行调用的,NetworkDispatcher 是一个执行网络请求的线程,从请求队列中取出 Request,然后执行请求,如果 Request 是需要被缓存的(默认情况下是必须被缓存的)而且 response 的 cacheEntry 不为 null,就调用 DiskBaseCache 的 put() 方法将 Entry 进行缓存。

3.4 get() 方法的调用

public class CacheDispatcher extends Thread {

    @Override
    public void run() {
        mCache.initialize();
        while (true) {
            try {
                processRequest();
            } catch (InterruptedException e) {
            }
        }
    }

    private void processRequest() throws InterruptedException {
        final Request<?> request = mCacheQueue.take();
        // 调用 get() 方法获取 Entry
        Cache.Entry entry = mCache.get(request.getCacheKey());

        if (entry.isExpired()) {
            request.setCacheEntry(entry);
            mNetworkQueue.put(request);
            return;
        }

        Response<?> response = request.parseNetworkResponse(
                new NetworkResponse(entry.data, entry.responseHeaders));

        if (!entry.refreshNeeded()) {
            mDelivery.postResponse(request, response);
        } 
}

我们在上面说到 DiskBaseCache 的 initialize() 方法是在 CacheDispatcher 中的 run() 方法中调用,其实 get() 方法也是一样的,在 while(true) 里面无限循环,当有请求到来时,便先根据请求的 Url 拿出对应的缓存在内存中的 Entry,然后对 Entry 进行一些判断和处理,最后将其构建成 Response 回调出去。

小结

在调用 Volley.newRequestQueue() 方法获取 RequestQueue 的时候,构建 DiskBaseCache 实例,在 CacheDispatcher 的 run() 方法中调用 DiskBaseCache 的 initialize() 方法初始化 DiskBaseCache,在 NetworkDispatcher 的 run() 方法中,在执行请求的时候,调用 DiskBaseCache 的 put() 方法将其缓存到内存中,然后在 CaheDispatcher 的 run() 方法中执行请求的时候调用 DiskBaseCache 的 get() 方法构建相应的 Response,最后将其分发出去。

打开App,阅读手记
1人推荐
发表评论
随时随地看视频慕课网APP