Volley的cache之硬盘缓存--DiskBasedCache

前言

衡量一个框架的优劣,缓存的处理是很重要的指标。这次我将对Volley的硬盘缓存DiskBasedCache从源码的角度进行解析。
下面先对DiskBasedCache的原理做简要介绍,开个头,然后根据简介做源码分析。
缓存原理

在说缓存原理之前,要说一下缓存的数据怎么来的。
第一步:
当NetWorkDispatcher的run方法开始执行(NetWorkDispatcher是Thread类的子类,后面会写文章介绍),进入循环,从网络请求队列中取出一个请求对象,执行网络请求。
第二步:
将从服务器得到的数据转换为Response对象(此对象代表一个网络响应)。
第三步:
根据 请求对象是否要求缓存(在新建Request的时候设置的值)来决定是否将响应数据写入缓存中。
以上三步得到缓存数据。缓存包括: 网络响应的响应正文和头信息。
得到缓存数据以后就会调用Cache类或者其子类的put方法将缓存信息写入SD卡(这里可以看出,Volley并没有做内存的缓存而是直接写入到磁盘文件)。调用get方法从SD卡取出缓存数据。
知道一点音频知识的都知道,在音频文件中会有该音频的头部信息,用来描述该音频的一些属性。在Volley中,为了描述缓存文件,google攻城狮也将缓存文件的一些重要属性—缓存文件的大小,缓存对应的URL,服务器的响应时间,网络延迟和缓存的新鲜度作为头信息组成CacheHeader对象写入到缓存文件中,并将响应正文写入CacheHeader后面,组成一个缓存文件。
在取缓存的时候,根据内存中的CacheHeader对象(缓存文件属性的封装类)的map集合(此map的key为待请求的URL,Value是CacheHeader对象)判断此请求对象是否有缓存。如果有缓存的话,将缓存读出来,封装成Entry(缓存属性和数据的封装类),从而恢复成response对象。
以上一写一读就完成了缓存的写入和取出操作。
源代码解析

介绍完了以上的原理,接下来就是代码实现了。我以方法作为单位,对DiskBasedCache源码进行解析。
put(string,entry)写缓存

此方法主要用来将缓存写入到SD卡,并在内存中加入该缓存文件的头信息(CacheHeader)、代码如下:

/**
     * 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);
            e.writeHeader(fos);
            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());
        }
    }

方法体的第一行代码的作用是进行 缓存的管理。这一点下面会介绍,先跳过。
接下来,根据请求Url新建缓存文件。然后使用字节流将数据写入该文件中。注意代码将写头部信息和写数据是分开的,这里是因为头部信息是有一定的结构,必须按照头信息的格式写入文件。写完之后,将代表缓存信息的CacheHeader放入内存方便以后对缓存的检索。如果在写文件中遇到异常,删除缓存文件。如果删除不成功,打Log。其实代码很容易理解。其中一些小点,比如 头信息怎么写的?会在接下来说明。
get(string)读缓存

写的逻辑大家都已经清晰了,读逻辑是写逻辑的逆过程。先上代码:

/**
     * 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;
        } finally {
            if (cis != null) {
                try {
                    cis.close();
                } catch (IOException ioe) {
                    return null;
                }
            }
        }
    }

首先根据内存中的头信息,判断是否有该key的缓存。有的话,进入if后面的语句:
首先按照put方法中得到文件的做法,来得到缓存文件。接着用一个字节输入流的包装类CountingInputStream读取缓存文件,此类有一个功能–记住已经读取的字节数,从而方便的读取头信息这类有结构的数据(很赞的想法)。将读取到的头信息和读取到的数据组成entry对象返回。与put过程一样,这里也会涉及到头的处理,包括缓存的校验。接下来会说。
CacheHeader缓存文件头信息类

该类封装了 缓存文件的大小,缓存对应的Url,服务器响应的日期,网络延迟,缓存文件的新鲜度,响应的头信息。
该类包含了三个重要方法:
readHeader(InputStream is)读头信息

代码如下:

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.ttl = readLong(is);
            entry.softTtl = readLong(is);
            entry.responseHeaders = readStringStringMap(is);
            return entry;
        }

在方法体的第三行,验证输入流对应的缓存文件的合法性。如果读出的magic不等于写入的magic,终止读写,如果合法,从缓存文件中读出结构数据,组成对象返回。
writeHeader(OutputStream os)

与上面方法相反,这里将maic和一些属性信息写入到缓存文件。注意写文件也是按照结构来写。

  public boolean writeHeader(OutputStream os) {
            try {
                writeInt(os, CACHE_MAGIC);
                writeString(os, key);
                writeString(os, etag == null ? "" : etag);
                writeLong(os, serverDate);
                writeLong(os, ttl);
                writeLong(os, softTtl);
                writeStringStringMap(responseHeaders, os);
                os.flush();
                return true;
            } catch (IOException e) {
                VolleyLog.d("%s", e.toString());
                return false;
            }
        }

toCacheEntry(byte[] data)方法

此方法用于将头信息和数据信息封装成Entry(Cache的单位数据)返回,代码很简单。

public Entry toCacheEntry(byte[] data) {
            Entry e = new Entry();
            e.data = data;
            e.etag = etag;
            e.serverDate = serverDate;
            e.ttl = ttl;
            e.softTtl = softTtl;
            e.responseHeaders = responseHeaders;
            return e;
        }

以上就是关于CacheHeader的介绍。

了解完以上内容,基本上对DiskBasedCache掌握了60%-70%。还有一些很好的方法,接下来继续解析。
pruneIfNeeded(int neededSpace)压缩缓存

说不上压缩,只是不知道如何翻译 prune.这个方法判断 待写入文件 写入之后是否会超出 缓存的最大值(默认5M)。如果超出,就删除掉之前的缓存数据。代码也好懂,如下:

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

看似很长,实则很优美。主要逻辑在 cacheHeader组成的map的遍历,然后找到头信息所指的缓存文件,删除之。
initialize() 缓存初始化

在缓存处理线程启动的时候,先加载本地已有的缓存文件的头信息到内存方便检索。就是这个作用。代码很清晰:

public synchronized void initialize() {
        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;
        }
        for (File file : files) {
            FileInputStream fis = null;
            try {
                fis = new FileInputStream(file);
                CacheHeader entry = CacheHeader.readHeader(fis);
                entry.size = file.length();
                putEntry(entry.key, entry);
            } catch (IOException e) {
                if (file != null) {
                   file.delete();
                }
            } finally {
                try {
                    if (fis != null) {
                        fis.close();
                    }
                } catch (IOException ignored) { }
            }
        }
    }

总结

到此为止,DiskBasedCache大部分都介绍过了,还有一些方法,如:
readInt 和writeInt
readLong和 writeLong
readString和writeString
readStringStringMap和writeStringStringMap
这些方法主要用于头信息读写有结构的数据,其中夹杂着一些自定义的位运算。

就像方法的代码一样,单个代码很好懂,代码写的很优雅,简洁(写简洁易懂的代码也是一门功课)。既然代码不是难点,那么理清楚方法之间的逻辑关系,就成为要下功夫的地方。读源代码不一定要一遍看懂,首先根据方法名知道方法大概的用途,然后将各个方法的调用关系搞清楚这个类是怎么工作的,最后再一行一行的读代码。这样下来,至少得几遍。还有就是 方法的修饰符 如 public private protected和类的修饰符 static等也成为我们理解的垫脚石。

转载:http://blog.csdn.net/yuan514168845/article/details/49665043

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值