性能问题与排球的DiskBasedCache [英] Performance issue with Volley's DiskBasedCache

查看:175
本文介绍了性能问题与排球的DiskBasedCache的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在我的<一个href="https://play.google.com/store/apps/details?id=no.ludde.android.photocollage&referrer=utm_source%3Dphotocollage%26utm_medium%3Dshare">Photo拼贴应用程序为Android,我使用凌空加载图像。 我使用DiskBasedCache(包括截击)与50 MB的存储,以prevent重新下载相同的图像多次。

我最后一次检查的DiskBasedCache包含约1000缓存条目。

在我的应用程序开始排球调用mCache.initialize(),这将花费大约10秒在我的Galaxy S4做到以下几点(!):

  1. 在列表中的缓存文件夹中的所有文件
  2. 打开每个文件并读取标头部分。

我发现读超过1000个文件在启动时不加载缓存索引一个非常有效的方式! : - )

从抽射/工具箱/ DiskBasedCache.java:

  @覆盖
市民同步无效初始化(){
    如果(!mRootDirectory.exists()){
        如果(!mRootDirectory.mkdirs()){
            VolleyLog.e(无法创建缓存目录%S,mRootDirectory.getAbsolutePath());
        }
        返回;
    }

    文件[]文件= mRootDirectory.listFiles();
    如果(文件== NULL){
        返回;
    }
    对于(文件文件:文件){
        的FileInputStream FIS = NULL;
        尝试 {
            FIS =新的FileInputStream(文件);
            CacheHeader条目= CacheHeader.readHeader(FIS);
            entry.size = file.length();
            putEntry(entry.key,入口);
        }赶上(IOException异常E){
            如果(文件!= NULL){
               file.delete();
            }
        } 最后 {
            尝试 {
                如果(FIS!= NULL){
                    fis.close();
                }
            }赶上(IOException异常忽略){}
        }
    }
}
 

我正在寻找一个快速和可扩展的解决方案。也许,关于如何提高凌空库替代DiskBasedCache实施或建议。


更新:(2014年1月6日)

我注意到凌空缓存中使用了大量的小(1字节)IO读取/写入。我克隆DiskBasedCache.java和封装了所有FileInputStreams和FileOutputStreams用的BufferedInputStream和BufferedOutputStreams。我发现,这种优化给了我一个3-10倍加速。

这修改有缺陷相比,编写一个新的磁盘缓存与中央索引文件低风险。


更新:(2014年1月10日)

下面是我使用的是现在新的类BufferedDiskBasedCache.java。

 包no.ludde.android.ds.android.volley;

/ *
 *版权所有(C)2011年Android开源项目
 *
 * Apache许可证下授权,版本2.0(以下简称许可证);
 *您可能不能使用这个文件除了在遵守许可。
 *您可以在获得许可证的副本
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 *除非适用法律要求或书面同意,软件
 *许可证下发布分布在原样的基础上,
 *无担保或任何形式的条件,无论是EX preSS或暗示的保证。
 *请参阅许可的特定语言的管理权限和
 *许可下限制。
 * /

进口android.os.SystemClock;

进口com.android.volley.Cache;
进口com.android.volley.VolleyLog;

进口java.io.BufferedInputStream中;
进口java.io.BufferedOutputStream;
进口java.io.EOFException;
进口的java.io.File;
进口java.io.FileInputStream中;
进口java.io.FileOutputStream中;
进口java.io.FilterInputStream中;
进口java.io.IOException异常;
进口的java.io.InputStream;
进口java.io.OutputStream中;
进口java.util.Collections中;
进口的java.util.HashMap;
进口java.util.Iterator的;
进口java.util.LinkedHashMap中;
进口的java.util.Map;

/ **
 *高速缓存执行直接缓存文件到硬盘指定
 * 目录。默认的磁盘使用大小为5MB,而且是可配置的。
 * /
公共类BufferedDiskBasedCache实现缓存{

    / **地图的关键,CacheHeader对* /
    私人最终地图&LT;字符串,CacheHeader&GT; mEntries =
            新的LinkedHashMap&LT;字符串,CacheHeader&GT;(16 .75f,真正的);

    / **当前使用的字节缓存空间总量。 * /
    私人长mTotalSize = 0;

    / **根目录要用于高速缓存。 * /
    私人最终文件mRootDirectory;

    / **以字节高速缓存的最大大小。 * /
    私人最终诠释mMaxCacheSizeInBytes;

    / **默认最大磁盘使用量以字节为单位。 * /
    私有静态最终诠释DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;

    / **高水位百分比缓存* /
    私有静态最终浮动HYSTERESIS_FACTOR = 0.9F;

    / **幻数的缓存文件格式的最新版本。 * /
    私有静态最终诠释CACHE_MAGIC = 0x20120504;

    / **
     *构造在指定目录中的DiskBasedCache的一个实例。
     * @参数rootDirectory缓存的根目录。
     * @参数maxCacheSizeInBytes字节高速缓存的最大大小。
     * /
    公共BufferedDiskBasedCache(文件rootDirectory,INT maxCacheSizeInBytes){
        mRootDirectory = rootDirectory;
        mMaxCacheSizeInBytes = maxCacheSizeInBytes;
    }

    / **
     *使用在指定的目录中构造的DiskBasedCache的一个实例
     * 5MB的默认最大缓存大小。
     * @参数rootDirectory缓存的根目录。
     * /
    公共BufferedDiskBasedCache(文件rootDirectory){
        这个(rootDirectory,DEFAULT_DISK_USAGE_BYTES);
    }

    / **
     *清除缓存。删除所有缓存文件从磁盘。
     * /
    @覆盖
    市民同步无效明确(){
        文件[]文件= mRootDirectory.listFiles();
        如果(文件!= NULL){
            对于(文件文件:文件){
                file.delete();
            }
        }
        mEntries.clear();
        mTotalSize = 0;
        VolleyLog.d(缓存清除。);
    }

    / **
     *如果它存在,否则返回null返回缓存项与指定键。
     * /
    @覆盖
    公共同步输入获取(字符串键){
        CacheHeader进入= mEntries.get(密钥);
        //如果条目不存在,则返回。
        如果(入口== NULL){
            返回null;
        }

        文件fil​​e = getFileForKey(密钥);
        CountingInputStream顺= NULL;
        尝试 {
            顺式=新CountingInputStream(新的BufferedInputStream(新的FileInputStream(文件)));
            CacheHeader.readHeader(CIS) //吃头
            byte []的数据= streamToBytes(顺式,(INT)(file.length() -  cis.bytesRead));
            返回entry.toCacheEntry(数据);
        }赶上(IOException异常E){
            VolleyLog.d(%S:%s时,file.getAbsolutePath(),e.​​toString());
            删除(键);
            返回null;
        } 最后 {
            如果(顺!= NULL){
                尝试 {
                    cis.close();
                }赶上(IOException异常IOE){
                    返回null;
                }
            }
        }
    }

    / **
     *初始化DiskBasedCache通过扫描所有文件目前在
     *指定的根目录。如果需要创建的根目录。
     * /
    @覆盖
    市民同步无效初始化(){
        如果(!mRootDirectory.exists()){
            如果(!mRootDirectory.mkdirs()){
                VolleyLog.e(无法创建缓存目录%S,mRootDirectory.getAbsolutePath());
            }
            返回;
        }

        文件[]文件= mRootDirectory.listFiles();
        如果(文件== NULL){
            返回;
        }
        对于(文件文件:文件){
            的BufferedInputStream FIS = NULL;
            尝试 {
                FIS =新的BufferedInputStream(新的FileInputStream(文件));
                CacheHeader条目= CacheHeader.readHeader(FIS);
                entry.size = file.length();
                putEntry(entry.key,入口);
            }赶上(IOException异常E){
                如果(文件!= NULL){
                   file.delete();
                }
            } 最后 {
                尝试 {
                    如果(FIS!= NULL){
                        fis.close();
                    }
                }赶上(IOException异常忽略){}
            }
        }
    }

    / **
     *失效在高速缓存中的条目。
     *参数键缓存键
     * @参数fullExpire真正的完全失效的进入,假软失效
     * /
    @覆盖
    市民同步无效无效(字符串键,布尔fullExpire){
        中入口=获得(键);
        如果(入门!= NULL){
            entry.softTtl = 0;
            如果(fullExpire){
                entry.ttl = 0;
            }
            把(键,进入);
        }

    }

    / **
     *提出具有指定键到缓存中的条目。
     * /
    @覆盖
    市民同步无效认沽(字符串键,进入输入){
        pruneIfNeeded(entry.data.length);
        文件fil​​e = getFileForKey(密钥);
        尝试 {
            的BufferedOutputStream FOS =新的BufferedOutputStream(新的FileOutputStream(文件));
            CacheHeader E =新CacheHeader(键,进入);
            e.writeHeader(FOS);
            fos.write(entry.data);
            fos.close();
            putEntry(键,E);
            返回;
        }赶上(IOException异常E){
        }
        布尔删除= file.delete();
        如果(!删除){
            VolleyLog.d(无法清理文件%s,file.getAbsolutePath());
        }
    }

    / **
     *移除如果它存在缓存中的指定键。
     * /
    @覆盖
    市民同步无效删除(字符串键){
        布尔删除= getFileForKey(键).delete();
        removeEntry(键);
        如果(!删除){
            VolleyLog.d(无法删除键=%s的,文件名=%s的高速缓存条目,
                    键,getFilenameForKey(键));
        }
    }

    / **
     *创建一个伪唯一的文件名指定缓存键。
     *参数键生成一个文件名的关键。
     返回:伪唯一的文件名。
     * /
    私人字符串getFilenameForKey(字符串键){
        INT firstHalfLength = key.length()/ 2;
        字符串localFilename =将String.valueOf(key.substring(0,firstHalfLength).hash code());
        localFilename + =将String.valueOf(key.substring(firstHalfLength).hash code());
        返回localFilename;
    }

    / **
     *返回给定缓存键的文件对象。
     * /
    公共文件getFileForKey(字符串键){
        返回新的文件(mRootDirectory,getFilenameForKey(键));
    }

    / **
     *梅干,以适应指定的字节的数量的高速缓存。
     * @参数neededSpace我们试图适应高速缓存的字节量。
     * /
    私人无效pruneIfNeeded(INT neededSpace){
        如果((mTotalSize + neededSpace)&LT; mMaxCacheSizeInBytes){
            返回;
        }
        如果(VolleyLog.DEBUG){
            VolleyLog.v(修剪旧的缓存项。);
        }

        没过多久= mTotalSize;
        INT prunedFiles = 0;
        长的startTime = SystemClock.elapsedRealtime();

        迭代器&LT;为Map.Entry&LT;字符串,CacheHeader&GT;&GT; 。迭代器= mEntries.entrySet()迭代器();
        而(iterator.hasNext()){
            Map.Entry的&LT;字符串,CacheHeader&GT;条目= iterator.next();
            CacheHeader E = entry.getValue();
            布尔删除= getFileForKey(e.key).delete();
            如果(删除){
                mTotalSize  -  = e.size;
            } 其他 {
               VolleyLog.d(无法删除键=%s的,文件名=%s的高速缓存条目,
                       e.key,getFilenameForKey(e.key));
            }
            iterator.remove();
            prunedFiles ++;

            如果((mTotalSize + neededSpace)&LT; mMaxCacheSizeInBytes * HYSTERESIS_FACTOR){
                打破;
            }
        }

        如果(VolleyLog.DEBUG){
            VolleyLog.v(修剪%d个文件,%d字节,%D毫秒,
                    prunedFiles,(mTotalSize  - 之前),SystemClock.elapsedRealtime() - 的startTime);
        }
    }

    / **
     *提出具有指定键到缓存中的条目。
     * @参数密钥识别由条目的键。
     *参数条目进入高速缓存。
     * /
    私人无效putEntry(字符串键,CacheHeader进入){
        如果(!mEntries.containsKey(键)){
            mTotalSize + = entry.size;
        } 其他 {
            CacheHeader oldEntry = mEntries.get(密钥);
            mTotalSize + =(entry.size  -  oldEntry.size);
        }
        mEntries.put(键,进入);
    }

    / **
     *删除确定从缓存中关键的条目。
     * /
    私人无效removeEntry(字符串键){
        CacheHeader进入= mEntries.get(密钥);
        如果(入门!= NULL){
            mTotalSize  -  = entry.size;
            mEntries.remove(键);
        }
    }

    / **
     *读取一个InputStream的内容到一个byte []。
     * * /
    私有静态的byte [] streamToBytes(InputStream中的,INT的长度)抛出IOException异常{
        字节[]字节=新字节[长度];
        诠释计数;
        INT POS = 0;
        而(POS&L​​T;长度放大器;&安培;!((计数= in.read(字节,POS,长度 -  POS))= -1)){
            POS + =计数;
        }
        如果(POS!=长度){
            抛出新的IOException异常(预期+长度+字节,读+ POS +字节);
        }
        返回字节;
    }

    / **
     *处理保持到缓存头一个条目。
     * /
    //可见的测试。
    静态类CacheHeader {
        / **确定此CacheHeader的数据的大小。 (这不是
         *序列化到磁盘。 * /
        众长大小;

        / **标识缓存项的键。 * /
        公共字符串键;

        / ** ETag的高速缓存一致性。 * /
        公共字符串ETAG;

        / **该响应所报告的服务器的日期。 * /
        众长serverDate;

        / ** TTL此记录。 * /
        众长TTL;

        / **软TTL此记录。 * /
        众长softTtl;

        / **日起,在这个缓存条目的响应头。 * /
        公共地图&LT;字符串,字符串&GT; responseHeaders响应;

        私人CacheHeader(){}

        / **
         *实例化一个新的CacheHeader对象
         * @参数密钥标识高速缓存条目的关键
         * @参数条目的高速缓存条目。
         * /
        公共CacheHeader(字符串键,进入输入){
            this.key =键;
            this.size = entry.data.length;
            this.etag = entry.etag;
            this.serverDate = entry.serverDate;
            this.ttl = entry.ttl;
            this.softTtl = entry.softTtl;
            this.responseHeaders = entry.responseHeaders;
        }

        / **
         *读取过的InputStream的头,并返回一个CacheHeader对象。
         * @参数是要读取的InputStream。
         * @throws IOException异常
         * /
        公共静态CacheHeader readHeader(InputStream的是)抛出IOException异常{
            CacheHeader进入=新CacheHeader();
            INT魔术=的readInt(是);
            如果(魔法!= CACHE_MAGIC){
                //也懒得删除,它会得到最终修剪
                抛出新IOException异常();
            }
            entry.key = readString(是);
            entry.etag = readString(是);
            如果(entry.etag.equals()){
                entry.etag = NULL;
            }
            entry.serverDate = readLong(是);
            entry.ttl = readLong(是);
            entry.softTtl = readLong(是);
            entry.responseHeaders = readStringStringMap(是);
            返回入境;
        }

        / **
         *为指定的数据的高速缓存条目。
         * /
        公共入口toCacheEntry(byte []的数据){
            进入E =新条目();
            e.data =数据;
            e.etag = ETAG;
            e.serverDate = serverDate;
            e.ttl = TTL;
            e.softTtl = softTtl;
            e.responseHeaders = responseHeaders响应;
            返回e的;
        }


        / **
         *写这个CacheHeader的内容到指定的输出流。
         * /
        公共布尔writeHeader(OutputStream的OS){
            尝试 {
                writeInt(OS,CACHE_MAGIC);
                writeString(OS,键);
                writeString(OS,ETAG == NULL:ETAG?);
                writeLong(OS,serverDate);
                writeLong(OS,TTL);
                writeLong(OS,softTtl);
                writeStringStringMap(responseHeaders响应,OS);
                os.flush();
                返回true;
            }赶上(IOException异常E){
                VolleyLog.d(%S,e.toString());
                返回false;
            }
        }

    }

    私有静态类CountingInputStream扩展FilterInputStream中的{
        私人诠释读取动作= 0;

        私人CountingInputStream(InputStream中的){
            超级(在);
        }

        @覆盖
        公众诠释的read()抛出IOException异常{
            INT结果= super.read();
            如果(结果!=  -  1){
                读取动作++;
            }
            返回结果;
        }

        @覆盖
        公众诠释读(byte []的缓冲区,诠释抵消,诠释计数)抛出IOException异常{
            INT结果= super.read(缓冲区,偏移,计数);
            如果(结果!=  -  1){
                读取动作+ =结果;
            }
            返回结果;
        }
    }

    / *
     *使用家酿读取和写入缓存简单的序列化系统
     *磁盘上的标题。曾几何时,这个使用标准的Java
     *对象{输入,输出}流,但默认的实现在很大程度上依赖
     *上反射(即使是标准型),并生成一吨垃圾。
     * /

    / **
     *简单的包装器{@link的InputStream#阅读()}抛出EOFException类
     *,而不是返回-1。
     * /
    私有静态诠释读取(InputStream的是)抛出IOException异常{
        INT B = is.​​read();
        如果(二== -1){
            抛出新EOFException类();
        }
        返回b;
    }

    静态无效writeInt(OutputStream的操作系统,INT N)抛出IOException异常{
        os.write((正&GT;大于0)及0xff的);
        os.write((正&GT;→8)及0xff的);
        os.write((正&GT;&GT; 16)及0xff的);
        os.write((正&GT;&GT; 24)及0xff的);
    }

    静态INT的readInt(InputStream的是)抛出IOException异常{
        INT N = 0;
        N | =(读(是)&LT;&LT; 0);
        N | =(读(是)&LT;&LT; 8);
        N | =(读(是)&LT;&LT; 16);
        N | =(读(是)&LT;&LT; 24);
        返回N;
    }

    静态无效writeLong(OutputStream的操作系统,N久)抛出IOException异常{
        os.write((字节)(正&GT;&GT;大于0));
        os.write((字节)(正&GT;&GT;→8));
        os.write((字节)(N&GT;&GT;&GT; 16));
        os.write((字节)(N&GT;&GT;&GT; 24));
        os.write((字节)(正&GT;&GT;&GT; 32));
        os.write((字节)(正&GT;&GT;→40));
        os.write((字节)(N&GT;&GT;&GT; 48));
        os.write((字节)(正&GT;&GT;&GT; 56));
    }

    静态长readLong(InputStream的是)抛出IOException异常{
        长N = 0;
        N | =((读(是)及0xFFL)&LT;&LT; 0);
        N | =((读(是)及0xFFL)&LT;&LT; 8);
        N | =((读(是)及0xFFL)&LT;&LT; 16);
        N | =((读(是)及0xFFL)&LT;&LT; 24);
        N | =((读(是)及0xFFL)其中;&所述; 32);
        N | =((读(是)及0xFFL)其中;&所述; 40);
        N | =((读(是)及0xFFL)&LT;&LT; 48);
        N | =((读(是)及0xFFL)&LT;&LT; 56);
        返回N;
    }

    静态无效writeString(OutputStream的操作系统,String s)将抛出IOException异常{
        字节[] B = s.getBytes(UTF-8);
        writeLong(OS,b.length个);
        os.write(B,0,b.length个);
    }

    静态字符串readString(InputStream的是)抛出IOException异常{
        INT N =(INT)readLong(是);
        byte []的B = streamToBytes(是,否);
        返回新的String(二,UTF-8);
    }

    静态无效writeStringStringMap(地图&LT;字符串,字符串&GT;地图,OutputStream的OS)抛出IOException异常{
        如果(图!= NULL){
            writeInt(OS,map.size());
            对于(Map.Entry的&LT;字符串,字符串&GT;输入:map.entrySet()){
                writeString(OS,entry.getKey());
                writeString(OS,entry.getValue());
            }
        } 其他 {
            writeInt(OS,0);
        }
    }

    静态地图&LT;字符串,字符串&GT; readStringStringMap(InputStream的是)抛出IOException异常{
        INT大小=的readInt(是);
        地图&LT;字符串,字符串&GT;结果=(大小== 0)
                ?收藏&LT;字符串,字符串&GT; emptyMap()
                新的HashMap&LT;字符串,字符串&GT;(大小);
        的for(int i = 0; I&LT;大小;我++){
            字符串键= readString(是).intern();
            字符串值= readString(是).intern();
            result.put(键,值);
        }
        返回结果;
    }


}
 

解决方案

是的,DiskBasedCache工作,首先需要打开方式的所有的在初始化文件()。这简直是​​....不是一个好主意:-(

您需要做出不同的实现,它的 doesent 的打开的所有文件在启动时。

取DiskBasedCache和变化初始化()的副本

  @覆盖
  市民同步无效初始化(){
    如果(!mRootDirectory.exists()){
      如果(!mRootDirectory.mkdirs()){
        VolleyLog.e(无法创建缓存目录%S,mRootDirectory.getAbsolutePath());
      }
    }
  }
 

和变化得到(),所以它是一个额外的检查,如果该文件存在于文件系统,如

有关

  @覆盖
  公共同步输入获取(字符串键){
    CacheHeader进入= mEntries.get(密钥);
    文件fil​​e = getFileForKey(密钥);
    如果(入口== NULL和放大器;&安培;!file.exists()){//额外的检查
      //如果条目不存在,则返回。
      VolleyLog.d(DrVolleyDiskBasedCache小姐的+键);
      返回null;
    }
    ...
 

我使用 https://play.google这种方法?.COM /存储/应用程序/详细信息ID = dk.dr.radio 并能正常工作 - 它的耐用性已经过测试,由〜30万的用户: - )

您可以从<下载完整版本的文件href="https://$c$c.google.com/p/dr-radio-android/source/browse/trunk/DRRadiov3/src/dk/dr/radio/net/volley/DrDiskBasedCache.java" rel="nofollow">https://$c$c.google.com/p/dr-radio-android/source/browse/trunk/DRRadiov3/src/dk/dr/radio/net/volley/DrDiskBasedCache.java (你必须删除一些DR电台具体的东西)

In my Photo Collage app for Android I'm using Volley for loading images. I'm using the DiskBasedCache (included with volley) with 50 mb storage to prevent re-downloading the same images multiple times.

Last time I checked the DiskBasedCache contained about 1000 cache entries.

When my app starts Volley calls mCache.initialize() and it will spend about 10 seconds (!) on my Galaxy S4 to do the following:

  1. List all files in cache folder
  2. Open each and every file and read the header section.

I find that reading 1000+ files at startup is not a very efficient way to load the cache index! :-)

From volley/toolbox/DiskBasedCache.java:

@Override
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) { }
        }
    }
}

I'm looking for a fast and scalable solution. Perhaps an alternative DiskBasedCache implementation or suggestions on how to improve the volley library.


Update: (2014-01-06)

Noticing that the Volley cache used a lot of small (1 byte) IO read/writes. I cloned DiskBasedCache.java and encapsulating all FileInputStreams and FileOutputStreams with BufferedInputStream and BufferedOutputStreams. I found that that this optimization gave me a 3-10 times speed up.

This modification has a low risks of bugs compared to writing a new disk cache with a central index file.


Update: (2014-01-10)

Here is new class BufferedDiskBasedCache.java that I'm using now.

package no.ludde.android.ds.android.volley;

/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import android.os.SystemClock;

import com.android.volley.Cache;
import com.android.volley.VolleyLog;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Cache implementation that caches files directly onto the hard disk in the specified
 * directory. The default disk usage size is 5MB, but is configurable.
 */
public class BufferedDiskBasedCache implements Cache {

    /** Map of the Key, CacheHeader pairs */
    private final Map<String, CacheHeader> mEntries =
            new LinkedHashMap<String, CacheHeader>(16, .75f, true);

    /** Total amount of space currently used by the cache in bytes. */
    private long mTotalSize = 0;

    /** The root directory to use for the cache. */
    private final File mRootDirectory;

    /** The maximum size of the cache in bytes. */
    private final int mMaxCacheSizeInBytes;

    /** Default maximum disk usage in bytes. */
    private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;

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

    /** Magic number for current version of cache file format. */
    private static final int CACHE_MAGIC = 0x20120504;

    /**
     * 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 BufferedDiskBasedCache(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 BufferedDiskBasedCache(File rootDirectory) {
        this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
    }

    /**
     * Clears the cache. Deletes all cached files from disk.
     */
    @Override
    public synchronized void clear() {
        File[] files = mRootDirectory.listFiles();
        if (files != null) {
            for (File file : files) {
                file.delete();
            }
        }
        mEntries.clear();
        mTotalSize = 0;
        VolleyLog.d("Cache cleared.");
    }

    /**
     * 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 BufferedInputStream(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;
                }
            }
        }
    }

    /**
     * Initializes the DiskBasedCache by scanning for all files currently in the
     * specified root directory. Creates the root directory if necessary.
     */
    @Override
    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) {
            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) {
                if (file != null) {
                   file.delete();
                }
            } finally {
                try {
                    if (fis != null) {
                        fis.close();
                    }
                } catch (IOException ignored) { }
            }
        }
    }

    /**
     * Invalidates an entry in the cache.
     * @param key Cache key
     * @param fullExpire True to fully expire the entry, false to soft expire
     */
    @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);
        }

    }

    /**
     * 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 {
            BufferedOutputStream fos = new BufferedOutputStream(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());
        }
    }

    /**
     * Removes the specified key from the cache if it exists.
     */
    @Override
    public synchronized void remove(String key) {
        boolean deleted = getFileForKey(key).delete();
        removeEntry(key);
        if (!deleted) {
            VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
                    key, getFilenameForKey(key));
        }
    }

    /**
     * Creates a pseudo-unique filename for the specified cache key.
     * @param key The key to generate a file name for.
     * @return A pseudo-unique filename.
     */
    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;
    }

    /**
     * Returns a file object for the given cache key.
     */
    public File getFileForKey(String key) {
        return new File(mRootDirectory, getFilenameForKey(key));
    }

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

    /**
     * Puts the entry with the specified key into the cache.
     * @param key The key to identify the entry by.
     * @param entry The entry to cache.
     */
    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);
    }

    /**
     * Removes the entry identified by 'key' from the cache.
     */
    private void removeEntry(String key) {
        CacheHeader entry = mEntries.get(key);
        if (entry != null) {
            mTotalSize -= entry.size;
            mEntries.remove(key);
        }
    }

    /**
     * Reads the contents of an InputStream into a byte[].
     * */
    private static byte[] streamToBytes(InputStream in, int length) throws IOException {
        byte[] bytes = new byte[length];
        int count;
        int pos = 0;
        while (pos < length && ((count = in.read(bytes, pos, length - pos)) != -1)) {
            pos += count;
        }
        if (pos != length) {
            throw new IOException("Expected " + length + " bytes, read " + pos + " bytes");
        }
        return bytes;
    }

    /**
     * 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;

        /** 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.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.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.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, ttl);
                writeLong(os, softTtl);
                writeStringStringMap(responseHeaders, os);
                os.flush();
                return true;
            } catch (IOException e) {
                VolleyLog.d("%s", e.toString());
                return false;
            }
        }

    }

    private static class CountingInputStream extends FilterInputStream {
        private int bytesRead = 0;

        private CountingInputStream(InputStream in) {
            super(in);
        }

        @Override
        public int read() throws IOException {
            int result = super.read();
            if (result != -1) {
                bytesRead++;
            }
            return result;
        }

        @Override
        public int read(byte[] buffer, int offset, int count) throws IOException {
            int result = super.read(buffer, offset, count);
            if (result != -1) {
                bytesRead += result;
            }
            return result;
        }
    }

    /*
     * Homebrewed simple serialization system used for reading and writing cache
     * headers on disk. Once upon a time, this used the standard Java
     * Object{Input,Output}Stream, but the default implementation relies heavily
     * on reflection (even for standard types) and generates a ton of garbage.
     */

    /**
     * Simple wrapper around {@link InputStream#read()} that throws EOFException
     * instead of returning -1.
     */
    private static int read(InputStream is) throws IOException {
        int b = is.read();
        if (b == -1) {
            throw new EOFException();
        }
        return b;
    }

    static void writeInt(OutputStream os, int n) throws IOException {
        os.write((n >> 0) & 0xff);
        os.write((n >> 8) & 0xff);
        os.write((n >> 16) & 0xff);
        os.write((n >> 24) & 0xff);
    }

    static int readInt(InputStream is) throws IOException {
        int n = 0;
        n |= (read(is) << 0);
        n |= (read(is) << 8);
        n |= (read(is) << 16);
        n |= (read(is) << 24);
        return n;
    }

    static void writeLong(OutputStream os, long n) throws IOException {
        os.write((byte)(n >>> 0));
        os.write((byte)(n >>> 8));
        os.write((byte)(n >>> 16));
        os.write((byte)(n >>> 24));
        os.write((byte)(n >>> 32));
        os.write((byte)(n >>> 40));
        os.write((byte)(n >>> 48));
        os.write((byte)(n >>> 56));
    }

    static long readLong(InputStream is) throws IOException {
        long n = 0;
        n |= ((read(is) & 0xFFL) << 0);
        n |= ((read(is) & 0xFFL) << 8);
        n |= ((read(is) & 0xFFL) << 16);
        n |= ((read(is) & 0xFFL) << 24);
        n |= ((read(is) & 0xFFL) << 32);
        n |= ((read(is) & 0xFFL) << 40);
        n |= ((read(is) & 0xFFL) << 48);
        n |= ((read(is) & 0xFFL) << 56);
        return n;
    }

    static void writeString(OutputStream os, String s) throws IOException {
        byte[] b = s.getBytes("UTF-8");
        writeLong(os, b.length);
        os.write(b, 0, b.length);
    }

    static String readString(InputStream is) throws IOException {
        int n = (int) readLong(is);
        byte[] b = streamToBytes(is, n);
        return new String(b, "UTF-8");
    }

    static void writeStringStringMap(Map<String, String> map, OutputStream os) throws IOException {
        if (map != null) {
            writeInt(os, map.size());
            for (Map.Entry<String, String> entry : map.entrySet()) {
                writeString(os, entry.getKey());
                writeString(os, entry.getValue());
            }
        } else {
            writeInt(os, 0);
        }
    }

    static Map<String, String> readStringStringMap(InputStream is) throws IOException {
        int size = readInt(is);
        Map<String, String> result = (size == 0)
                ? Collections.<String, String>emptyMap()
                : new HashMap<String, String>(size);
        for (int i = 0; i < size; i++) {
            String key = readString(is).intern();
            String value = readString(is).intern();
            result.put(key, value);
        }
        return result;
    }


}

解决方案

Yes, the way DiskBasedCache works it needs to open all the files in initialize(). Which is simply.... not a good idea :-(

You need to make a different implementation that doesent open all the files at startup.

Take a copy of DiskBasedCache and change initialize() to

  @Override
  public synchronized void initialize() {
    if (!mRootDirectory.exists()) {
      if (!mRootDirectory.mkdirs()) {
        VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
      }
    }
  }

And change get() so it makes an additional check for if the file exists on the file system, like

  @Override
  public synchronized Entry get(String key) {
    CacheHeader entry = mEntries.get(key);
    File file = getFileForKey(key);
    if (entry == null && !file.exists()) { // EXTRA CHECK
      // if the entry does not exist, return.
      VolleyLog.d("DrVolleyDiskBasedCache miss for " + key);
      return null;
    }
    ...

I use this approach in https://play.google.com/store/apps/details?id=dk.dr.radio and it works fine - its robustness have been tested by ~300000 users :-)

You can download a full version of the file from https://code.google.com/p/dr-radio-android/source/browse/trunk/DRRadiov3/src/dk/dr/radio/net/volley/DrDiskBasedCache.java (you'll have to delete some DR Radio specific stuff)

这篇关于性能问题与排球的DiskBasedCache的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆