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

前言

在上一篇文章中,带大家阅读了 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 卡容量时,会发生无法写入的异常(因为存储空间满了)。

    扫描二维码关注公众号,回复: 5579293 查看本文章

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

  • 在进行内存空间的判断之后,便将 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,最后将其分发出去。

  • 想学习更多Android知识,或者获取相关资料请加入Android技术开发交流3群:820655513。 有面试资源系统整理分享,Java语言进阶和Kotlin语言与Android相关技术内核,APP开发框架知识, 360°Android App全方位性能优化。Android前沿技术,高级UI、Gradle、RxJava、小程序、Hybrid、 移动架构师专题项目实战环节、React Native、等技术教程!架构师课程、NDK模块开发、 Flutter等全方面的 Android高级实践技术讲解。还有在线答疑

猜你喜欢

转载自blog.csdn.net/qq_43257419/article/details/88410163
今日推荐