Part1:Volley磁盘缓存

    • CacheDispatcher
    • CacheEntry和DiskBasedCacheCacheHeader
    • DiskBasedCacheCountingInputStream
    • 缓存的核心DiskBasedCache
      • 初始化逻辑initialize函数
      • pruneIfNeeded
      • get和put
      • else
    • 思考
      • LRU算法一定合理吗如何增大缓存的命中率
      • 文件名重复问题
  • 首先研究一下Volley的磁盘缓存原理,它主要包括以下几个类

    • CacheDispatcher缓存的具体执行类,继承Thread
    • DiskBasedCache 缓存核心类,基于Disk的缓存实现类
    • Cache.Entry 真正HTTP请求的缓存实体
    • DiskBasedCache.CacheHeaderCache.Entry一样,就是不存储响应体,只存储了缓存的大小
    • DiskBasedCache.CountingInputStream 添加了记录字节功能的流,继承FilterInputStream

CacheDispatcher

  • 在RequestQueue的run方法中启动了CacheDispatcher的start方法,我们先看一下它的成员变量,在看CacheDispatcher的run方法(着重关注注释)
    /** 可以走Disk缓存的request请求队列. */
    private final BlockingQueue<Request<?>> mCacheQueue;

    /** 需要走网络的request请求队列. */
    private final BlockingQueue<Request<?>> mNetworkQueue;

    /** DiskBasedCache缓存实现类. */
    private final Cache mCache;

    /** 网络请求结果传递类. */
    private final ResponseDelivery mDelivery;

    /** 用来停止线程的标志位. */
    private volatile boolean mQuit = false;
 @Override
    public void run() {
        android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

        // 初始化DiskBasedCache缓存类.
        mCache.initialize();

        while (true) {
            try {
                // 从缓存队列中获取request请求.(缓存队列实现了生产者-消费者队列模型)
                final Request<?> request = mCacheQueue.take();

                // 判断请求是否被取消
                if (request.isCanceled()) {
                    request.finish("cache-discard-canceled");
                    continue;
                }

                // 从缓存系统中获取request请求结果Cache.Entry.
                Cache.Entry entry = mCache.get(request.getCacheKey());
                if (entry == null) {
                    // 如果缓存系统中没有该缓存请求,则将request加入到网络请求队列中.
                    // 由于NetworkQueue跟NetworkDispatcher线程关联,并且也是生产者-消费者队列,
                    // 所以这里添加request请求就相当于将request执行网络请求.
                    mNetworkQueue.put(request);
                    continue;
                }

                // 判断缓存结果是否过期.
                if (entry.isExpired()) {
                    request.setCacheEntry(entry);
                    // 过期的缓存需要重新执行request请求.
                    mNetworkQueue.put(request);
                    continue;
                }

                // We have a cache hit; parse its data for delivery back to the request.
                Response<?> response = request.parseNetworkResponse(new NetworkResponse(entry.data,
                        entry.responseHeaders));

                // 判断Request请求结果是否新鲜?
                if (!entry.refreshNeeded()) {
                    // 请求结果新鲜,则直接将请求结果分发,进行异步回调用户接口.
                    mDelivery.postResponse(request, response);
                } else {
                    // 请求结果不新鲜,但是同样还是将缓存结果返回给用户,并且同时执行网络请求,刷新Request网络结果缓存.
                    request.setCacheEntry(entry);

                    response.intermediate = true;

                    mDelivery.postResponse(request, response, new Runnable() {
                        @Override
                        public void run() {
                            try {
                                mNetworkQueue.put(request);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                if (mQuit) {
                    return;
                }
            }
        }
    }
  • 上面的注释已经说的很清楚了,请对照下面的流程图一起理解

Cache.Entry和DiskBasedCache.CacheHeader

  • 它是HTTP的缓存实体类,看一下它的代码
  class Entry {
        /** HTTP响应体. */
        public byte[] data;

        /** HTTP响应首部中用于缓存新鲜度验证的ETag. */
        public String etag
        /** HTTP响应时间. */
        public long serverDate;

        /** 缓存内容最后一次修改的时间. */
        public long lastModified;

        /** Request的缓存过期时间. */
        public long ttl;

        /** Request的缓存新鲜时间. */
        public long softTtl;

        /** HTTP响应Headers. */
        public Map<String, String> responseHeaders = Collections.emptyMap();

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

        /** 判断缓存是否新鲜,不新鲜的缓存需要发到服务端做新鲜度的检测. */
        public boolean refreshNeeded() {
            return this.softTtl < System.currentTimeMillis();
        }
    }
  • 注意,DiskBasedCache.CacheHeader和它的不同在于它不存储响应体,只存储了缓存的大小

DiskBasedCache.CountingInputStream

  • 它是一个添加了记录读取字节数的辅助类,看它的read函数
        @Override
        public int read() throws IOException {
            int result = super.read();
            if (result != -1) {
                bytesRead ++;
            }
            return result;
        }
  • 在看一下writeInt()函数就一切都明白了,作者封装了一下读取和写入的函数,让它一个字节一个字节的读或者写,为了配合CountingInputStream记录读取的字节
    private static void writeInt(OutputStream os, int n) throws IOException {
        os.write((n) & 0xff);
        os.write((n >> 8) & 0xff);
        os.write((n >> 16) & 0xff);
        os.write((n >> 24) & 0xff);
    }

缓存的核心DiskBasedCache

  • 它是缓存的核心类,基于Disk实现,我们先分析一下他的成员变量
 /** 默认硬盘最大的缓存空间(5M). */
    private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;

    /** 标记缓存起始的MAGIC_NUMBER. */
    private static final int CACHE_MAGIC = 0x20150306;

    /**
     * High water mark percentage for the cache.
     */
    private static final float HYSTERESIS_FACTOR = 0.9f;

    /**
     * Map of the Key, CacheHeaders pairs.
     * accessOrder为true很关键
     */
    private final Map<String, CacheHeader> mEntries =
            new LinkedHashMap<String, CacheHeader>(16, 0.75f, true);

    /** 目前使用的缓存字节数. */
    private long mTotalSize = 0;

    /** 硬盘缓存目录. */
    private final File mRootDirectory;

    /** 硬盘缓存最大容量(默认5M). */
    private final int mMaxCacheSizeInBytes;
  • 请注意这句代码,它的第三个参数很重要,辅助完善LRU算法,请参考
    private final Map<String, CacheHeader> mEntries =
            new LinkedHashMap<String, CacheHeader>(16, 0.75f, true);

初始化逻辑initialize()函数

  • initialize()函数的作用是遍历Disk缓存系统,将缓存文件读出来分为key:url和value:CacheHeader存入到Map中
    @Override
    public void initialize() {
        if (!mRootDirectory.exists() && !mRootDirectory.mkdirs()) {
            return;
        }

        File[] files = mRootDirectory.listFiles();
        if (files == null) {
            return;
        }

        for (File file : files) {
            BufferedInputStream fis = null;
            try {
                fis = new BufferedInputStream(new FileInputStream(file));
                CacheHeader entry = CacheHeader.readHeader(fis);
                entry.size = file.length();
                putEntry(entry.key, entry);
            }catch (IOException e) {
                file.delete();
                e.printStackTrace();
            }finally {
                if (fis != null) {
                    try {
                        fis.close();
                    } catch (IOException ignored) {
                    }
                }
            }
        }
    }
  • 看一下上述函数中的CacheHeader entry = CacheHeader.readHeader(fis);这个函数的作用就是按照写入的顺序把相应的数据读出来,其实就是对象的反序列化
        public static CacheHeader readHeader(InputStream is) throws IOException {
            CacheHeader entry = new CacheHeader();
            // 以CACHE_NUMBER作为读取一个对象的开始
            int magic = readInt(is);
            if (magic != CACHE_MAGIC) {
                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;
  • 再接着看一下putEntry(entry.key, entry);这个函数,将key和value存储到内存中,并更新总字节数(判断缓存是否满)
    private void putEntry(String key, CacheHeader entry) {
        if (!mEntries.containsKey(key)) {
            mTotalSize += entry.size;
        } else {
            CacheHeader oldEntry = mEntries.get(key);
            mTotalSize += (entry.size - oldEntry.size);
        }

        mEntries.put(key, entry);
    }

pruneIfNeeded

  • 这个函数很重要,当缓存满时删除最久未使用的缓存,既是队列前端的缓存,函数很简单,看完就懂了
    /** Disk缓存替换更新机制. */
    private void pruneIfNeeded(int neededSpace) {
        if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
            return;
        }

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

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

get和put

  • 接下来我们看取出缓存和设置缓存的函数,它的作用是从mEntries中获取缓存并构造Entry,为什么要构造?注意之前我们说过CacheHeader没有响应体的内容,所以我们需要构造一个有响应体的类,看以下注释既可明白

    想一想为什么要有一个看无用的DiskBasedCache.CacheHeader类,因为为了避免在内存中存储过多的东西,用的时候在临时构造(从文件中拿响应体的内容)

    public synchronized Entry get(String key) {
        CacheHeader entry = mEntries.get(key);
        if (entry == null) {
            return null;
        }

        File file = getFileForKey(key);
        CountingInputStream cis = null;
        try {
            cis = new CountingInputStream(new BufferedInputStream(new FileInputStream(file)));
            // 读完CacheHeader部分,并通过CountingInputStream的bytesRead成员记录已经读取的字节数.
            CacheHeader.readHeader(cis);
            // 读取缓存文件存储的HTTP响应体内容.
            byte[] data = streamToBytes(cis, (int)(file.length() - cis.bytesRead));
            return entry.toCacheEntry(data);
        } catch (IOException e) {
            remove(key);
            return null;
        } finally {
            if (cis != null) {
                try {
                    cis.close();
                } catch (IOException ignored) {
                }
            }
        }
    }
  • 然后我们看一下put函数,注释我已经写的很详细了
    @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));
            //构造Header
            CacheHeader e = new CacheHeader(key, entry);
            //向文件中写入除了响应体之外的内容
            boolean success = e.writeHeader(fos);
            if (!success) {
                fos.close();
                throw new IOException();
            }
            //把响应体的内容写到文件中
            fos.write(entry.data);
            fos.close();
            //把数据存储到内存中
            putEntry(key, e);
            return;
        } catch (IOException e) {
            e.printStackTrace();
        }
        //如果出现异常,就把文件删除
        file.delete();
    }

else

  • 还有一些辅助函数,代码特别简单,并且我已经做了充分的注释
    /** 清空缓存内容. */
    @Override
    public synchronized void clear() {
        File[] files = mRootDirectory.listFiles();
        if (files != null) {
            for (File file : files) {
                file.delete();
            }
        }
        mEntries.clear();
        mTotalSize = 0;
    }

    /** 标记指定的cache过期. */
    @Override
    public synchronized void invalidate(String key, boolean fullExpire) {
        Entry entry = get(key);
        if (entry != null) {
            entry.softTtl = 0;
            if (fullExpire) {
                entry.ttl = 0;
            }
            put(key, entry);
        }
    }
    /** 获取存储当前key对应value的文件句柄. */
    private File getFileForKey(String key) {
        return new File(mRootDirectory, getFilenameForKey(key));
    }

    /** 根据key的hash值生成对应的存储文件名称. */
    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;
    }
  @Override
    public synchronized void remove(String key) {
        boolean deleted = getFileForKey(key).delete();
        removeEntry(key);
        if (!deleted) {
            Log.e("Volley", "没能删除key=" + key + ", 文件名=" + getFilenameForKey(key) + "缓存.");
        }
    }

    /** 从Map对象中删除key对应的键值对. */
    private void removeEntry(String key) {
        CacheHeader entry = mEntries.get(key);
        if (entry != null) {
            mTotalSize -= entry.size;
            mEntries.remove(key);
        }
    }

思考

LRU算法一定合理吗?如何增大缓存的命中率

  • 因为我们是存储了缓存的过期时间的public long ttl,在删除缓存的时候pruneIfNeeded直接从队列前端删除真的好吗?有没有更好的方法?当然有,鉴于缓存的过期时间,我们可以以这个为基点,遍历一遍,优先删除快过期的缓存,或者我们存储时就按缓存过期时间存储,这样可能会让Volley有更好的表现

文件名重复问题

  • 文件名会重复吗?答案是会的,因为不同的字符串也有可能产生相同的hash值见这边文章,所以Volley采用分割计算两次哈希值的方法减小重复的几率
    /** 根据key的hash值生成对应的存储文件名称. */
    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;
    }
时间: 2024-08-27 04:09:34

Part1:Volley磁盘缓存的相关文章

请注意,Volley已默认使用磁盘缓存

之前学习volley框架,用ImageLoader可以设置内存缓存,用一个LruCache,就可以避免OOM且图片读取速度快,爽极了. 后来想,如果只是内存缓存的话,那退出程序或者内存不够大了,缓存的图片不就被清理掉了,这样每次启动程序就又得去网上下载图片,流量好贵的. 于是找到了磁盘缓存框架DiskLruCache,这是一个挺著名的开源框架,网易云阅读等APP之前都用它来缓存图片,关于这个框架的使用可以看这篇博客. 找到这个框架后我就着手把DiskLruCache和Volley结合起来,用Lr

从源码带看Volley的缓存机制

转载请注明出处:http://blog.csdn.net/asdzheng/article/details/45955653 磁盘缓存DiskBasedCache 如果你还不知道volley有磁盘缓存的话,请看一下我的另一篇博客请注意,Volley已默认使用磁盘缓存 DiskBasedCache内部结构 它由两部分组成,一部分是头部,一部分是内容:先得从它的内部静态类CacheHeader(缓存的头部信息)讲起,先看它的内部结构: responseHeaders; } //可以看到,头部类里包含

LruDiskCache要点--不可不用的磁盘缓存工具类

LruDiskCache是使用Lru算法的磁盘缓存类,它的功能是将LruCache中缓存位置由内存改为磁盘,一般两者结合使用,用于对处理小文件,图片的缓存. 下面记录下阅读过程中几个比较重要的点: Get 获取缓存数据时,LruDiskCache会使用LinkedHashmap的算法,也就是最常使用的放在尾部,最少使用的首先被遍历到. 当你需要获取缓存数据时,首先会得到是一个Snapshot对象(如果数据正常的话:写入成功.在有效内等等),Snapshot其实就是持有缓存文件的输入流,无其它逻辑

XHNetworkCache,一行代码将请求数据写入磁盘缓存

XHNetworkCache 版本记录(持续更新)2016.07.01 Version 1.1(更新) 1.增加手动清除缓存接口 2.增加获取缓存大小接口 2016.06.24 Version 1.0(发布)使用方法:1.写入 [Objective-C] 查看源文件 复制代码 ? 1 2 3 //将数据写入磁盘缓存(参数1:服务器返回的JSON数据, 参数2:数据请求URL) //[按APP版本号缓存,不同版本APP,同一接口缓存数据互不干扰] [XHNetworkCache saveJsonR

详细讲解Android的图片下载框架UniversialImageLoader之磁盘缓存(一)

沉浸在Android的开发世界中有一些年头的猴子们,估计都能够深深的体会到Android中的图片下载.展示.缓存一直是心中抹不去的痛.鄙人亦是如此.Ok,闲话不说,为了督促自己的学习,下面就逐一的挖掘Android中还算是比较牛叉的图片处理框架UniversialImageLoader以飨读者吧! 凡事如果过于草率必将陷入泥塘不能自拔.还是按部就班的一步一步的将这个框架给啃透. 第一个要讲的是磁盘的缓存的接口DiskCache 首先看一下其中的核心的接口的代码: File getDirector

详细讲解Android的图片下载框架UniversialImageLoader之磁盘缓存的扩展(二)

相对于第一篇来讲,这里讲的是磁盘缓存的延续.在这里我们主要是关注四个类,分别是DiskLruCache.LruDiskCache.StrictLineReader以及工具类Util. 接下来逐一的对它们进行剖析.废话不多说. 首先来看一下DiskLruCache. 这个类的主要功能是什么呢?我们先来看一段类的注释: /** * A cache that uses a bounded amount of space on a filesystem. Each cache * entry has a

iOS开发之缓存框架、内存缓存、磁盘缓存、NSCache、TMMemoryCache、PINMemoryCache、YYMemoryCache、TMDiskCache、PINDiskCache

1.在项目中我们难免会用到一些缓存方式来保存服务器传过来的数据,以减少服务器的压力. 缓存的方式分为两种分别为内存缓存和磁盘缓存,内存缓存速度快容量小,磁盘缓存容量大速度慢可持久化.常见的内存缓存有NSCache.TMMemoryCache.PINMemoryCache.YYMemoryCache.常见的磁盘缓存有TMDiskCache.PINDiskCache.YYCache. 1.本文章着重讲下YYCache. 这是为什么呢,因为他比其他的缓存框架更加高效,使用方便. YYCache: 去掉

磁盘缓存

磁盘上必须有缓存,用来接收指令和数据,还被用来进行预读.磁盘缓存时刻处于被打开的状态.在很多资料上提到某些情况可以关闭缓存“禁用”磁盘缓存,这是容易造成误解的说法.缓存在磁盘上就表现为一块电路板的RAM芯片,目前有2MB.8MB.16MB.32MB等容量规格.所谓的禁用即是:WRITE THROUGH模式.即磁盘收到写入指令和数据后,必须先将其写入盘片,然后才向控制器返回成功的信号,这样就相当于禁用了缓存,但是实际上,指令和数据收到到达的一定是缓存. SCSI 指令中有两个参数可以控制对磁盘缓存

【安卓中的缓存策略系列】安卓缓存策略之磁盘缓存DiskLruCache

安卓中的缓存包括两种情况即内存缓存与磁盘缓存,其中内存缓存主要是使用LruCache这个类,其中内存缓存我在[安卓中的缓存策略系列]安卓缓存策略之内存缓存LruCache中已经进行过详细讲解,如看官还没看过此博客,建议看官先去看一下. 我们知道LruCache可以让我们快速的从内存中获取用户最近使用过的Bitmap,但是我们无法保证最近访问过的Bitmap都能够保存在缓存中,像类似GridView等需要大量数据填充的控件很容易就会用完整个内存缓存.另外,我们的应用可能会被类似打电话等行为而暂停导