volley源码解析(四)--CacheDispatcher从缓存中获取数据

从上一篇文章我们已经知道,现在要处理的问题就是CacheDispatcher和NetworkDispatcher怎么分别去缓存和网络获取数据的问题,这两个问题我分开来讲。

但是首先说明的是,这两个问题其实是有联系的,当CacheDispatcher获取不到缓存的时候,会将request放入网络请求队列,从而让NetworkDispatcher去处理它;

而当NetworkDispatcher获得数据以后,又会将数据缓存,下次CacheDispatcher就可以从缓存中获得数据了。

这篇文章,就让我们先来了解volley是怎么从缓存中获取数据的。

第一个要说明的,当然是CacheDispatcher类,这个类本质是一个线程,作用就是根据request从缓存中获取数据

我们先来看它的构造方法

     /**
     * Creates a new cache triage dispatcher thread.  You must call {@link #start()}
     * in order to begin processing.
     * 创建一个调度线程
     * @param cacheQueue Queue of incoming requests for triage 
     * @param networkQueue Queue to post requests that require network to 
     * @param cache Cache interface to use for resolution 
     * @param delivery Delivery interface to use for posting responses
     */
    public CacheDispatcher(
            BlockingQueue<Request<?>> cacheQueue, BlockingQueue<Request<?>> networkQueue,
            Cache cache, ResponseDelivery delivery) {
        mCacheQueue = cacheQueue;//缓存请求队列
        mNetworkQueue = networkQueue;//网络请求队列
        mCache = cache;//缓存
        mDelivery = delivery;//响应分发器
    }
从上面的方法看出,CacheDispatcher持有缓存队列cacheQueue,目的当然是为了从队列中获取东西。

而同时持有网络队列networkQueue,目的是为了在缓存请求失败后,将request放入网络队列中。

至于响应分发器delivery是成功请求缓存以后,将响应分发给对应请求的,分发器存在的目的我已经在前面的文章中说过几次了,就是为了灵活性和在主线程更新UI(至于怎么做到,我们以后会讲)

最后是一个缓存类cache,这个cache可以看成是缓存的代表,也就是说它就是缓存,是面向对象思想的体现,至于它是怎么实现的,等下会说明


看完构造方法,我们就直奔对Thread而言,最重要的run()方法

@Override
    public void run() {
        if (DEBUG) VolleyLog.v("start new dispatcher");
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);//设置线程优先级

        // Make a blocking call to initialize the cache.
        mCache.initialize();//初始化缓存对象

        while (true) {
            try {
                // Get a request from the cache triage queue, blocking until
                // at least one is available.
            	// 从缓存队列中取出请求
                final Request<?> request = mCacheQueue.take();
                request.addMarker("cache-queue-take");

                // If the request has been canceled, don't bother dispatching it.
                if (request.isCanceled()) {//是否取消请求
                    request.finish("cache-discard-canceled");
                    continue;
                }

                // Attempt to retrieve this item from cache.
                Cache.Entry entry = mCache.get(request.getCacheKey());//获取缓存
                if (entry == null) {
                    request.addMarker("cache-miss");
                    // Cache miss; send off to the network dispatcher.
                    mNetworkQueue.put(request);//如果没有缓存,放入网络请求队列
                    continue;
                }

                // If it is completely expired, just send it to the network.
                if (entry.isExpired()) {//如果缓存超时
                    request.addMarker("cache-hit-expired");
                    request.setCacheEntry(entry);
                    mNetworkQueue.put(request);
                    continue;
                }

                // We have a cache hit; parse its data for delivery back to the request.
                request.addMarker("cache-hit");
                Response<?> response = request.parseNetworkResponse(//解析响应
                        new NetworkResponse(entry.data, entry.responseHeaders));
                request.addMarker("cache-hit-parsed");

                if (!entry.refreshNeeded()) {//不需要更新缓存
                    // Completely unexpired cache hit. Just deliver the response.
                    mDelivery.postResponse(request, response);
                } else {
                    // Soft-expired cache hit. We can deliver the cached response,
                    // but we need to also send the request to the network for
                    // refreshing.
                    request.addMarker("cache-hit-refresh-needed");
                    request.setCacheEntry(entry);

                    // Mark the response as intermediate.
                    response.intermediate = true;

                    // Post the intermediate response back to the user and have
                    // the delivery then forward the request along to the network.
                    mDelivery.postResponse(request, response, new Runnable() {
                        @Override
                        public void run() {
                            try {
                                mNetworkQueue.put(request);
                            } catch (InterruptedException e) {
                                // Not much we can do about this.
                            }
                        }
                    });
                }

            } catch (InterruptedException e) {
                // We may have been interrupted because it was time to quit.
                if (mQuit) {
                    return;
                }
                continue;
            }
        }
    }
这个方法里面做了很多事情,我们按顺序看

1,从缓存请求队列中取出request

2,判断这个request已经是否被取消,如果是,调用它的finish()方法,continue

3,否则,利用Cache获得缓存,获得缓存的依据是request.getCacheKey(),也就是request的url

4,如果缓存不存在,将request放入mNetworkQueue,continue

5,否则,检查缓存是否过期,是,同样将request放入mNetworkQueue,continue

6,否则,检查是否希望更新缓存,否,组装成response交给分发器mDelivery

7,否则组装成response交给分发器mDelivery,并且将request再加入mNetworkQueue,去网络请求更新


OK,上面的过程已经说得够清楚了。让人疑惑的很重要一步,就是Cache这个类到底是怎么获取缓存数据的,下面我们就来看看Cache这个类。

这个Cache其实是一个接口(面向抽象编程的思想),而它的具体实现,我们在第一篇文章的Volley类中看到,是DiskBasedCache类。

无论如何,我们先看接口

/**
 * 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.
         * Time To Live 生存时间
         */
        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静态内部类,这个类Entry可以理解成一条缓存记录,也就是说每个Entry就代表一条缓存记录。

这么一说,上面run()方法里面的代码就比较好理解了,我们就知道,为什么Cache获取的缓存,叫做Entry。

然后我们来看DiskBasedCache,从名字上知道,这个类是硬盘缓存的意思

在这里我们注意到,volley其实只提供了硬盘缓存而没有内存缓存的实现,这可以说是它的不足,也可以说它作为一个扩展性很强的框架,是留给使用者自己实现的空间。如果我们需要内存缓存,我们大可自己写一个类继承Cache接口。

在这之前,我们先来看volley是怎么实现硬盘缓存的

首先是构造函数

/**
     * Constructs an instance of the DiskBasedCache at the specified directory.
     * @param rootDirectory The root directory of the cache.
     * @param maxCacheSizeInBytes The maximum size of the cache in bytes.
     */
    public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
        mRootDirectory = rootDirectory;
        mMaxCacheSizeInBytes = maxCacheSizeInBytes;
    }

    /**
     * Constructs an instance of the DiskBasedCache at the specified directory using
     * the default maximum cache size of 5MB.
     * @param rootDirectory The root directory of the cache.
     */
    public DiskBasedCache(File rootDirectory) {
        this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
    }

这个函数传入了两个参数,一个是指缓存根目录,一个是指缓存的最大值

存取缓存,必须有存取方法,我们先从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 {
            FileOutputStream fos = 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());
        }
    }

这个方法一看比较复杂,我先来说明一下主要的存储过程

1,检查要缓存的数据的长度,如果当前已经缓存的数据大小mTotalSize加上要缓存的数据大小,大于缓存最大值mMaxCacheSizeInBytes,则要将旧的缓存文件删除,以腾出空间来存储新的缓存文件

2,根据缓存记录类Entry,提取Entry除了数据以外的其他信息,例如这个缓存的大小,过期时间,写入日期等,并且将这些信息实例成CacheHeader,。这样做的目的是,方便以后我们查询缓存,获得缓存相应信息时,不需要去读取硬盘,因为CacheHeader是内存中的。

3,写入缓存

根据上面步奏,我们来读pruneIfNeeded()方法,这个方法就是完成了步奏1的工作,主要思路是不断删除文件,直到腾出足够的空间给新的缓存文件

 /**
     * Prunes the cache to fit the amount of bytes specified.
     * 修剪缓存大小,去适应规定的缓存比特数
     * @param neededSpace The amount of bytes we are trying to fit into the cache.
     */
    private void pruneIfNeeded(int neededSpace) {
        if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {//如果没有超过最大缓存大小,返回
            return;
        }
        if (VolleyLog.DEBUG) {
            VolleyLog.v("Pruning old cache entries.");
        }

        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++;

            if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
                break;
            }
        }

        if (VolleyLog.DEBUG) {
            VolleyLog.v("pruned %d files, %d bytes, %d ms",
                    prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime);
        }
    }
在这个方法中,我们注意到有一个mEntries,我们看一下它的声明
/** 
     * Map of the Key, CacheHeader pairs
     * 缓存记录表,用于记录所有的缓存文件信息
     * 使用LRU算法
     */
    private final Map<String, CacheHeader> mEntries =
            new LinkedHashMap<String, CacheHeader>(16, .75f, true);
也就是说它实则保存了所有缓存的头信息CacheHeader,而且在map中,这些信息是按照LRU算法排列的,LRU算法是LinkedHashMap的内置算法。

每次存取缓存,都会修改这个map,也就是说要调用LRU算法进行重新排序,这样造成一定效率的下降,但貌似也没有更好的方法。


然后就是第二步,根据Entry生成CacheHeader,我们来看一下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;
            }
        }

    }
应该说没有什么特别的,其实就是把Entry类里面的,出来data以外的信息提取出来而已。

另外还增加了两个读写方法,readHeader(InputStream is)和writeHeader(OutputStream os)

从这两个方法可以知道,对于一个缓存文件来说,前面是关于这个缓存的一些信息,然后才是真正的缓存数据。


最后一步,写入缓存数据,将CacheHeader添加到map

            fos.write(entry.data);//写入数据
            fos.close();
            putEntry(key, e);
OK,到此为止,写入就完成了。那么读取,就是写入的逆过程而已。

/**
     * Returns the cache entry with the specified key if it exists, null otherwise.
     * 查询缓存
     */
    @Override
    public synchronized Entry get(String key) {
        CacheHeader entry = mEntries.get(key);
        // if the entry does not exist, return.
        if (entry == null) {
            return null;
        }

        File file = getFileForKey(key);//获取缓存文件
        CountingInputStream cis = null;
        try {
            cis = new CountingInputStream(new FileInputStream(file));
            CacheHeader.readHeader(cis); // eat header读取头部
            byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead));//去除头部长度
            return entry.toCacheEntry(data);
        } catch (IOException e) {
            VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
            remove(key);
            return null;
        }  catch (NegativeArraySizeException e) {
            VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
            remove(key);
            return null;
        } finally {
            if (cis != null) {
                try {
                    cis.close();
                } catch (IOException ioe) {
                    return null;
                }
            }
        }
    }
读取过程很简单

1,读取缓存文件头部

2,读取缓存文件数据

3,生成Entry,返回

相信大家都可以看懂,因为真的没有那么复杂,我就不再累述了。


get(),put()方法看过以后,其实DiskBasedCache类还有一些public方法,例如缓存信息map的初始化,例如删除所有缓存文件的方法,这些都比较简单,基本上就是利用get,put方法里面的函数就可以完成,我也不再贴出代码来说明了。


DiskBasedCache给大家讲解完毕,整个从缓存中获取数据的过程,相信也说得很清楚。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。