Android DiskLruCache解析

引言

当我们需要从网络上下载资源(如文本、图片、视频)时,将最近下载过的数据以缓存方式保存起来,以便于下次重新加载时能够节约数据流量,提高加载速度,能够较好地改善用户体验。同时,由于缓存本身容量也有限,需要提供一套管理机制:当超过一定容量限制时,将最近很少使用的缓存数据清理掉,以腾出空间来容纳那些最近经常使用的数据。我们将实现了这套机制的缓存称之为LruCache(也即Least Recently Used Cache)。
Android平台已经给我们提供了两个有用的LruCache工具,按照存储位置的不同来区分,一个是基于内存的LruCache(这个在android.support.v4包里提供),一个是基于磁盘文件系统的DiskLruCache(这个平台上未公开发布,但我们可以从 googlesource 上下载。
内存缓存读写速度快,管理维护简单,但限于移动设备闲置容量小,而且一旦断电重启就必须重建;磁盘缓存读写速度相对较慢,管理维护较复杂,但容量可以很大,同时不用担心断电重启缓存的数据全部消失。平常开发过程中可以基于两个缓存的优缺点差异结合使用,比如让应用先查找内存缓存,若内存缓存未找到,再查找磁盘缓存。本文重点放在解析后者即DiskLruCache上.

用法

先易后简,先介绍下如何使用DiskLruCache:

  • 初始化:

使用DiskLruCahce的第一步是初始化它,由于使用了工厂模式对构造函数进行了私有限制,我们不能直接new一个,而是用它提供的一个open接口:

    public static DiskLruCache open(File directory, 
                             int appVersion,
                             int valueCount,
                             long maxSize)


   /*
 * directory参数表示建立缓存的文件系统路径位置,我们一般放在 /sdcard/Android/data/<application package>/cache 这个路径下面,这个位置在sd卡上,同时一旦应用被卸载,这个路径下的数据也会被自动清理,不会浪费磁盘空间。
 * appVersion参数用于指定软件版本,当版本升级后,可以进行识别区分,清理掉之前版本的缓存数据。
 * valueCount参数定义了每个缓存条目的数据值数目,一般设为1就可以了
 *  maxSize参数定义了缓存的最大字节数,当超过这个字节数时即会清理缓存中不常使用的条目
 *  最后返回值即为一个DiskLruCache对象
   */
  • 写入
    写入缓存条目操作需要借助DiskLruCache的内部类Editor来完成,其调用接口是:
 public Editor edit(String key)
 public final class Editor {
  ...
  public OutputStream newOutputStream(int index) 
  ...
 }
 /*
 这里的key作为区分不同缓存条目的键值,我们一般是把下载文件URL地址通过MD5编码来生成确保其唯一性。为什么不直接用URL地址作为key呢?原因在于URL地址很长,而且其中可能存在一些特殊字符(空字符,回行字符等),无法作为有效key值使用。进行MD5编码后只会存在0-F这些字符,而且一般能保证key值唯一。
 返回的Editor类我们可以利用其newOutputStream接口获取一个输出文件流,并将下载资源以流的形式写入文件系统。这个输出文件流其实是对FileOutputStream做了扩展,增加了一些文件读写的容错机制。而其中的index参数表示缓存条目索引,由于前面我们构建缓存条目时valueCount一般指定为1,所以index一般传入0就可以了。当缓存条目的数据内容完成写入时,我们就调用其commit方法将该缓存条目提交。
 */

 ...
 final DiskLruCache.Editor editor = mDiskLruCache.edit(key);
 if (editor != null) {
    out = editor.newOutputStream(DISK_CACHE_INDEX);
    value.getBitmap().compress(                                             mCacheParams.compressFormat,                                mCacheParams.compressQuality, out);
    editor.commit();
    out.close();
}
  • 读取
    读取缓存条目则使用get方法:
public synchronized Snapshot get(String key)
public final class Snapshot implements Closeable {
...
    InputStream getInputStream(int index)
...
}

/*
返回一个Snapshot后,调用其getInputSream方法即可获取缓存文件输入流
*/

final DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
 if (snapshot != null) {                       
   inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
   if (inputStream != null) {
      FileDescriptor fd = ((FileInputStream)inputStream).getFD();
      bitmap = ImageResizer.decodeSampledBitmapFromDescriptor(
                                    fd, Integer.MAX_VALUE, Integer.MAX_VALUE, this);
    }
}

实现机制

DiskLruCache的机制简单来说就是将每个资源以文件的形式存放一份在磁盘文件系统中供应用服用,并通过维护一个日志文件来记录缓存的读写修改操作。当每次启动运行时会根据日志文件内容在在内存中同步维护一个当前有效的缓存条目链表来管理对应的缓存数据,同时当缓存大小超过上限后,则删除掉很少用的缓存条目链表节点及相关文件以满足不超过上限要求。

具体实现细节先看看open接口的代码:

 public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
            throws IOException {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        if (valueCount <= 0) {
            throw new IllegalArgumentException("valueCount <= 0");
        }

        // prefer to pick up where we left off
        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        if (cache.journalFile.exists()) {
            try {
                cache.readJournal();
                cache.processJournal();
                cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
                        IO_BUFFER_SIZE);
                return cache;
            } catch (IOException journalIsCorrupt) {
//                System.logW("DiskLruCache " + directory + " is corrupt: "
//                        + journalIsCorrupt.getMessage() + ", removing");
                cache.delete();
            }
        }

        // create a new empty cache
        directory.mkdirs();
        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        cache.rebuildJournal();
        return cache;
    }

从上述代码看,除了构建出了DiskLruCache,同时还打开了一个journal(日志)文件进行扫描处理(如对对日志标记为DIRTY状态(下面会解释DIRTY状态)的文件进行删除),并据此初始化一个LinkedHashMap<String, Entry> lruEntries链表在内存中来管理有效缓存数据 。
上面的Entry内部类,其定义如下:

  private final class Entry {
        private final String key;
        /** Lengths of this entry's files. */
        private final long[] lengths;
        /** True if this entry has ever been published */
        private boolean readable;//当条目状态为CLEAN时此项为true
        /** The ongoing edit or null if this entry is not being edited. */
        private Editor currentEditor;//当条目状态为DIRTY时,此项一般不为null,当为CLEAN时,此项为null
        /** The sequence number of the most recently committed edit to this entry. */
        private long sequenceNumber;
        ...

    }

因此每个Entry对应缓存中的一个缓存条目。记录了相应的key,文件长度及相关Editor。

journal日志文件记录了对缓存的各种读写访问操作,一个典型的日志文件格式如下:

libcore.io.DiskLruCache
1
100
2

CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6

前5行是文件头,分别表示缓存常量,磁盘缓存版本,应用版本(这个由open接口传入),值计数(这个由open接口传入)和空行。接下来的每行分别记录了缓存条目的状态,每行由一个状态和一个key值以及可选特定状态值构成。DIRTY状态表示该条缓存当前正在创建或更新,一般当我们调用前面提到的DiskLruCache.edit接口时除了在lruEntries中加入一个Entry外,还会在日志文件中写一条DIRTY状态行数据。每次成功的DIRTY活动记录在调用Editor.commit接口时会跟随写一条CLEAN或REMOVE状态行数据,如果未跟随这两项则表明是个可能需被删除的临时文件,会在初始化时将缓存文件删除。CLEAN状态行记录了一个缓存条目的成功发布,标志缓存数据可以被正常读取,其后面除了带key值以外还会跟随文件长度。写入REMOVE状态行则通常是调用commit接口出错或Editor.abort接口时触发的,该状态记录了缓存条目的删除操作。READ状态记录了缓存条目的访问操作,也即调用了DiskLruCache.get接口。
而commit接口和abort接口调用,都会触发下面的completeEdit方法,只不过一个success入参为true,另一个的success入参为false.

private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
        Entry entry = editor.entry;
        if (entry.currentEditor != editor) {
            throw new IllegalStateException();
        }

        // 即使success为成功,但如果文件不可见的话,也会转而触发abort接口
        if (success && !entry.readable) {
            for (int i = 0; i < valueCount; i++) {
                if (!entry.getDirtyFile(i).exists()) {
                    editor.abort();
                    throw new IllegalStateException("edit didn't create file " + i);
                }
            }
        }

        for (int i = 0; i < valueCount; i++) {
            File dirty = entry.getDirtyFile(i);
            if (success) {
                if (dirty.exists()) {
                    File clean = entry.getCleanFile(i);
                    dirty.renameTo(clean);
                    long oldLength = entry.lengths[i];
                    long newLength = clean.length();
                    entry.lengths[i] = newLength;
                    size = size - oldLength + newLength;
                }
            } else {
                deleteIfExists(dirty);
            }
        }

        redundantOpCount++;
        entry.currentEditor = null;
        if (entry.readable | success) {
            entry.readable = true;
            journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
            if (success) {
                entry.sequenceNumber = nextSequenceNumber++;
            }
        } else {
            lruEntries.remove(entry.key);
            journalWriter.write(REMOVE + ' ' + entry.key + '\n');
        }

        if (size > maxSize || journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }
    }

另外缓存使用了一个redundantOpCount变量来记录用户操作的次数,每执行一次commit操作,这个变量值都会加1,当变量值达到2000或者缓存容量超过字节上限的时候就会触发一个后台线程在缓存超限时对缓存进行清理,必要时也会重建日志文件:

  private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
            60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
    private final Callable<Void> cleanupCallable = new Callable<Void>() {
        @Override public Void call() throws Exception {
            synchronized (DiskLruCache.this) {
                if (journalWriter == null) {
                    return null; // closed
                }
                trimToSize();
                if (journalRebuildRequired()) {
                    rebuildJournal();
                    redundantOpCount = 0;
                }
            }
            return null;
        }
    };
private void trimToSize() throws IOException {
        while (size > maxSize) {
//            Map.Entry<String, Entry> toEvict = lruEntries.eldest();
            final Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
            remove(toEvict.getKey());
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
资源包主要包含以下内容: ASP项目源码:每个资源包中都包含完整的ASP项目源码,这些源码采用了经典的ASP技术开发,结构清晰、注释详细,帮助用户轻松理解整个项目的逻辑和实现方式。通过这些源码,用户可以学习到ASP的基本语法、服务器端脚本编写方法、数据库操作、用户权限管理等关键技术。 数据库设计文件:为了方便用户更好地理解系统的后台逻辑,每个项目中都附带了完整的数据库设计文件。这些文件通常包括数据库结构图、数据表设计文档,以及示例数据SQL脚本。用户可以通过这些文件快速搭建项目所需的数据库环境,并了解各个数据表之间的关系和作用。 详细的开发文档:每个资源包都附有详细的开发文档,文档内容包括项目背景介绍、功能模块说明、系统流程图、用户界面设计以及关键代码解析等。这些文档为用户提供了深入的学习材料,使得即便是从零开始的开发者也能逐步掌握项目开发的全过程。 项目演示与使用指南:为帮助用户更好地理解和使用这些ASP项目,每个资源包中都包含项目的演示文件和使用指南。演示文件通常以视频或图文形式展示项目的主要功能和操作流程,使用指南则详细说明了如何配置开发环境、部署项目以及常见问题的解决方法。 毕业设计参考:对于正在准备毕业设计的学生来说,这些资源包是绝佳的参考材料。每个项目不仅功能完善、结构清晰,还符合常见的毕业设计要求和标准。通过这些项目,学生可以学习到如何从零开始构建一个完整的Web系统,并积累丰富的项目经验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值