DiskLurCache 源码总结

DiskLurCache

使用教程

源码解析

使用

打开缓存

  1. 打开缓存函数

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

    ​ open()方法接收四个参数,第一个参数指定的是数据的缓存地址,第二个参数指定当前应用程序的版本号,第三个参数指定同一个key可以对应多少个缓存文件,基本都是传1,第四个参数指定最多可以缓存多少字节的数据。

  2. 实际调用

    public File getDiskCacheDir(Context context, String uniqueName) {  
        String cachePath;  
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())  
            || !Environment.isExternalStorageRemovable()) {  
            cachePath = context.getExternalCacheDir().getPath();  
        } else {  
            cachePath = context.getCacheDir().getPath();  
        }  
        return new File(cachePath + File.separator + uniqueName);  
    }  
    
    public int getAppVersion(Context context) {  
        try {  
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);  
            return info.versionCode;  
        } catch (NameNotFoundException e) {  
            e.printStackTrace();  
        }  
        return 1;  
    }  
    
    //调用open 函数, 这里的 open 函数, 当版本号改变后, 
    //appVersionString, valueCountString 改变的时候, 会直接 报 IO异常
    DiskLruCache mDiskLruCache = null;  
    try {  
        File cacheDir = getDiskCacheDir(context, "bitmap");  
        if (!cacheDir.exists()) {  
            cacheDir.mkdirs();  
        }  
        mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);  
    } catch (IOException e) {  
        e.printStackTrace();  
    }  
    
//private void readJournal() throws IOException  函数.

String magic = reader.readLine();

String version = reader.readLine();

String appVersionString = reader.readLine();

String valueCountString = reader.readLine();

String blank = reader.readLine();

if (!MAGIC.equals(magic)

    || !VERSION_1.equals(version)
    || !Integer.toString(appVersion).equals(appVersionString)
    || !Integer.toString(valueCount).equals(valueCountString)
    || !"".equals(blank)) {
    throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
                          + valueCountString + ", " + blank + "]");
}

写入缓存

  1. 下载一张图片

    //调用 URL 系在一张图片, 并写入 outputStream 中.
    private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {  
        HttpURLConnection urlConnection = null;  
        BufferedOutputStream out = null;  
        BufferedInputStream in = null;  
        try {  
            final URL url = new URL(urlString);  
            urlConnection = (HttpURLConnection) url.openConnection();  
            in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);  
            out = new BufferedOutputStream(outputStream, 8 * 1024);  
            int b;  
            while ((b = in.read()) != -1) {  
                out.write(b);  
            }  
            return true;  
        } catch (final IOException e) {  
            e.printStackTrace();  
        } finally {  
            if (urlConnection != null) {  
                urlConnection.disconnect();  
            }  
            try {  
                if (out != null) {  
                    out.close();  
                }  
                if (in != null) {  
                    in.close();  
                }  
            } catch (final IOException e) {  
                e.printStackTrace();  
            }  
        }  
        return false;  
    }  
    

  2. 根据 URL 生成 MD5值, 唯一标识, 当作内部的 LURCache List的键

    public String hashKeyForDisk(String key) {  
        String cacheKey;  
        try {  
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");  
            mDigest.update(key.getBytes());  
            cacheKey = bytesToHexString(mDigest.digest());  
        } catch (NoSuchAlgorithmException e) {  
            cacheKey = String.valueOf(key.hashCode());  
        }  
        return cacheKey;  
    }  
    
    private String bytesToHexString(byte[] bytes) {  
        StringBuilder sb = new StringBuilder();  
        for (int i = 0; i < bytes.length; i++) {  
            String hex = Integer.toHexString(0xFF & bytes[i]);  
            if (hex.length() == 1) {  
                sb.append('0');  
            }  
            sb.append(hex);  
        }  
        return sb.toString();  
    }
    
  3. 调用 edit(key) 获取 Editor 对象

    String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
    String key = hashKeyForDisk(imageUrl);
    DiskLruCache.Editor editor = mDiskLruCache.edit(key); 
    
    //这里的 editor 里面可以生成一个 关于 dirty 文件的 output 对象, 并重写了
    //输出流的 write()函数, 一旦写操作报 IO 异常, 会将 Entry 中的 hasError 置为 true,
    //在 commit 的时候会调用 commitEdit(false), 将 dirty 删除掉, 
    //理想情况下是成功的, 那么会将文件保存为 clean 文件, 并记录一行  DIRTY操作,
    
    //下载图片使用的 outputStream 是editor 的, 也就是 指向 dirty 的 outputStream. 
    //在调用 commit/abort 函数时,会将dirty文件转换为clean文件,或者删除掉.
    new Thread(new Runnable() {  
        @Override  
        public void run() {  
            try {  
                String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
                String key = hashKeyForDisk(imageUrl);  
                DiskLruCache.Editor editor = mDiskLruCache.edit(key);  
                if (editor != null) {  
                    OutputStream outputStream = editor.newOutputStream(0);  
                    if (downloadUrlToStream(imageUrl, outputStream)) {  
                        editor.commit();  
                    } else {  
                        editor.abort();  
                    }  
                }
                //检查当前 存储的size 是否大于设置的maxSize, 
                //大于, 将删除 LUR 中原本不常用的 文件, 直到 size < maxSize.
                mDiskLruCache.flush();  
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
        }  
    }).start();  
    

读取缓存

  1. 调用函数

    public synchronized Snapshot get(String key) throws IOException  
    
  2. 根据 URL 生成的KEY 去获取对应的文件, 获取 SnapShot 对象.

    String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
    String key = hashKeyForDisk(imageUrl);  
    DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);  
    
  3. 调用 SnapShot 对象里面输入流

    输入流 指向的是 KEY 对应的 CLEAN 文件 的 InputStrean, 并且在 SnapSnot 中保存的是一个 inputStream 数组, 数组的长度是 valueCount(一个KEY 对应几个文件) 的大小,

    每个 inputStream[] 保存的是对应的下标 文件, 具体的 获取文件的函数在 Entry 中, 调用 getCleanFile(index) 函数获取.

    ​ 这里调用getInputStream(0), 将 inputStream 转换为 Bitmap 并显示出来.

    try {  
        String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
        String key = hashKeyForDisk(imageUrl);  
        DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);  
        if (snapShot != null) {  
            InputStream is = snapShot.getInputStream(0);  
            Bitmap bitmap = BitmapFactory.decodeStream(is);  
            mImage.setImageBitmap(bitmap);  
        }  
    } catch (IOException e) {  
        e.printStackTrace();  
    }  
    

移除缓存

mDiskLruCache.remove(key); 调用 remove 函数, 关键判断为 key 值来删除

try {  
    String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";    
    String key = hashKeyForDisk(imageUrl);    
    mDiskLruCache.remove(key);  
} catch (IOException e) {  
    e.printStackTrace();  
}  

将会在 journal 文件中写入一行 REMOVE 操作, 并将 redundantOpCount++

源码解析

概述

  • DiskLurCache 涉及到一个 journal 的文件, 这个文件保存 CLEAN, DIRTY, REMOVE, READ 操作

  • 初始化一个 DiskLurCache 对象, 需要调用 open 函数

    DiskLruCache.open(directory, appVersion, 
                      valueCount, maxSize) ;
    
  • 关于写操作

    String key = generateKey(url);  
    DiskLruCache.Editor editor = mDiskLruCache.edit(key); 
    OuputStream os = editor.newOutputStream(0);
    
    os.write(...)
    os.falsh();
    
    //提交写操作, 将之前写的 temp 文件保存为 clean 文件, 
    //当之前写操作出现错误的时候, 会将文件删除, 并将 KEY 从 lur 中删除掉.
    editor.commit();
    
  • 关于读操作

    DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);  
    if (snapShot != null) {  
        InputStream is = snapShot.getInputStream(0);  
    }
    
    Bitmap bitmap = BitmapFactory.decodeStream(is);
    imageView.setBitmap(bitmap);
    
    //关闭所有的 inputStream.
    snapShot.close();
    

journal 文件

journal文件你打开以后呢,是这个格式;

libcore.io.DiskLruCache
1
1
1

DIRTY c3bac86f2e7a291a1a200b853835b664
CLEAN c3bac86f2e7a291a1a200b853835b664 4698
READ c3bac86f2e7a291a1a200b853835b664
DIRTY c59f9eec4b616dc6682c7fa8bd1e061f
CLEAN c59f9eec4b616dc6682c7fa8bd1e061f 4698
READ c59f9eec4b616dc6682c7fa8bd1e061f
DIRTY be8bdac81c12a08e15988555d85dfd2b
CLEAN be8bdac81c12a08e15988555d85dfd2b 99
READ be8bdac81c12a08e15988555d85dfd2b
DIRTY 536788f4dbdffeecfbb8f350a941eea3
REMOVE 536788f4dbdffeecfbb8f350a941eea3 

首先看前五行:ok,以上5行可以称为该文件的文件头,DiskLruCache初始化的时候,如果该文件存在需要校验该文件头。

DiskLruCache初始化的时候,如果该文件存在需要校验该文件头。

  • 第一行固定字符串libcore.io.DiskLruCache
  • 第二行DiskLruCache的版本号,源码中为常量1
  • 第三行为你的app的版本号,当然这个是你自己传入指定的
  • 第四行指每个key对应几个文件,一般为1
  • 第五行,空行

操作记录:

  • DIRTY 表示一个entry正在被写入(其实就是把文件的OutputStream交给你了)。那么写入分两种情况,如果成功会紧接着写入一行CLEAN的记录;如果失败,会增加一行REMOVE记录。
  • REMOVE除了上述的情况呢,当你自己手动调用remove(key)方法的时候也会写入一条REMOVE记录。
  • READ就是说明有一次读取的记录。
  • 每个CLEAN的后面还记录了文件的长度,注意可能会一个key对应多个文件,那么就会有多个数字(参照文件头第四行)。

DiskLruCache#open

  1. open 函数

      /**
       * 打开缓存在文件夹中, 如果不存在就创建.
       * Opens the cache in {@code directory}, creating a cache if none exists
       * there.
       *
       * @param directory a writable directory    缓存目录
       * @param valueCount the number of values per cache entry. Must be positive. 每个缓存条目的值数量. 每个 KEY 对应的文件
       * @param maxSize the maximum number of bytes this cache should use to store  用于存储的最大字节数.
       * @throws IOException if reading or writing the cache directory fails   当写文件和度文件失败, 会抛出异常.
       */
      //创建 DiskLruCache 对象, 并初始化文件存放的地址.
      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");
        }
    
        //查找 bkp 文件是否存在, 不存在
        // If a bkp file exists, use it instead.
        File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
        if (backupFile.exists()) {
          File journalFile = new File(directory, JOURNAL_FILE);
          // If journal file also exists just delete backup file.
          //即存在 bkp 文件又存在 journal 文件,  删除backup 文件,
          //存在 bkp 文件,但是不存在 journal文件, 将文件重命名.
          if (journalFile.exists()) {
            backupFile.delete();
          } else {
            renameTo(backupFile, journalFile, false);
          }
        }
    
        // Prefer to pick up where we left off.
        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        //根据文件夹信息, 创建对应的源信息, 和backup , 和临时操作的文件.
        if (cache.journalFile.exists()) {
          try {
            cache.readJournal();
            cache.processJournal();
            return cache;
          } catch (IOException journalIsCorrupt) {
            System.out
                .println("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;
      }
    
    
  2. 重新创建 journal 文件 (1. 文件不存在, 2. 文件存在, 但是多余的操作超过 2000, 为了保证 journal 文件的大小, 会重新生成文件.)

     /**
       * Creates a new journal that omits redundant information. This replaces the
       * current journal if it exists.
       */
      private synchronized void rebuildJournal() throws IOException {
        if (journalWriter != null) {
          journalWriter.close();
        }
    
        //新将数据写到  tmp 文件中.
        Writer writer = new BufferedWriter(
            new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
        try {
          writer.write(MAGIC);
          writer.write("\n");
          writer.write(VERSION_1);
          writer.write("\n");
          writer.write(Integer.toString(appVersion));
          writer.write("\n");
          writer.write(Integer.toString(valueCount));
          writer.write("\n");
          writer.write("\n");
    
          //重新写文件, REMOVE, READ 操作会被干光, 重新写文件.
          for (Entry entry : lruEntries.values()) {
            //判断entry 是否是脏数据的存在.
            if (entry.currentEditor != null) {
              writer.write(DIRTY + ' ' + entry.key + '\n');
            } else {
              writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
            }
          }
        } finally {
          writer.close();
        }
    
        if (journalFile.exists()) {
          //在清除 REMOVE, READ 操作的时候, 如果 之前存在  journalFile, 那么将文件保存为 Backup 文件, 并将源文件删除.
          renameTo(journalFile, journalFileBackup, true);
        }
        //将 tmp 文件重新保存为 journalFile 文件, 但是不删除 tmp 文件.
        renameTo(journalFileTmp, journalFile, false);
        //在转换成功后, 将 备份文件也删除掉, 在 DiskLurCache 的每关于文件的操作都会将 IO 异常抛出去,
        //这里就是当 renameTo() 这个函数被抛出了 IO 异常的时候备份文件不会被删除掉.
        journalFileBackup.delete();
    
        journalWriter = new BufferedWriter(
            new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
      }
    
    
  3. 初始化的时候, 文件存在, 读取文件行, 并保证 lur 中保存的记录是只有 CLEAN, 而没有 REMOVE/ DIRTY 操作的 KEY - ENTRY

    
      //只会在初始化的时候被调用 open() 函数的时候才会被调用.
    
        /**
         * 1. 通过文件头来检测是否是 journal 文件, 如果不是, 直接报IO 异常,
         * 2. 对文件进行while 循环, 一直跑到捕获文件尾异常,
         *       2.1. while 循环中会将 标签为 CLEAN 标志的标签的KEY 添加到 LUR 数组中去,
         *            但是当在轮询中碰到 Remove 的操作标签, 会将 对应的 KEY 从原本的 LUR 数组中移除,
         *
         *       2.3. 判断3个状态, REMOVE, CLEAN, DIRTY,
         *             REMOVE: 会删除在 LUR 中的 KEY 值.
         *             CLEAN:  生成一个 Entry(不管是不是空的),
         *                          设置 currentEditor = null,
         *                          设置 readable = true (可读)
         *                          设置 lengths, 也是通过空格来区分的.
         *
         *             DIRTY: 脏数据, 如果文件是脏数据(正在操作)时. 分配一个新的  Editor(关于 entry的 Editor(文件流))
         *         REMOVE, CLEAR, DIRTY, READ, 和KEY 之间都有空格, 他们之间的判断第一个是名称, 第二个是空格的数量.
         *
         *       2.4. 统计当前文件中多余的操作次数:
         *            文本的行数 - LUR.size() = 多余操作次数.
         *
         *       2.5. 判断文件的读写是否是异常停止, 文件未读到末尾, 则调用 reBuildJournal() 函数. 重写生成 journal 文件.
         *
         *       2.6. journalWriter 初始化 journalWriter, 写字段到 journal文件中的 输出流.
         *
         * @throws IOException
         */
    private void readJournal() throws IOException {
        StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
        try {
            String magic = reader.readLine();
            String version = reader.readLine();
            String appVersionString = reader.readLine();
            String valueCountString = reader.readLine();
            String blank = reader.readLine();
            if (!MAGIC.equals(magic)
                || !VERSION_1.equals(version)
                || !Integer.toString(appVersion).equals(appVersionString)
                || !Integer.toString(valueCount).equals(valueCountString)
                || !"".equals(blank)) {
                throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
                                      + valueCountString + ", " + blank + "]");
            }
    
            int lineCount = 0;
            while (true) {
                try {
                    readJournalLine(reader.readLine());
                    lineCount++;
                } catch (EOFException endOfJournal) {
                    //捕获 crash 来跳出循环.
                    break;
                }
            }
    
            //多余的操作次数.
            redundantOpCount = lineCount - lruEntries.size();
    
            // If we ended on a truncated line, rebuild the journal before appending to it.
            //函数执行错误, 未结束, 但是报了 EOFException 错误.
            if (reader.hasUnterminatedLine()) {
                rebuildJournal();
            } else {
                //初始化 write.
                journalWriter = new BufferedWriter(new OutputStreamWriter(
                    new FileOutputStream(journalFile, true), Util.US_ASCII));
            }
        } finally {
            //关闭 reader 流.
            Util.closeQuietly(reader);
        }
    }
    
    private void readJournalLine(String line) throws IOException {
        //切割字符串, 准备判断关于 KEY 值的状态, 是否需要被删除掉.
        int firstSpace = line.indexOf(' ');
        if (firstSpace == -1) {
            throw new IOException("unexpected journal line: " + line);
        }
    
        int keyBegin = firstSpace + 1;
        int secondSpace = line.indexOf(' ', keyBegin); // 查找第二个空格.
        final String key;
        if (secondSpace == -1) {
            key = line.substring(keyBegin);
            //判断状态是否被标志位 REMOVE. 是的话, 将会被移除.
            if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
                //lruEntries 是没有数据的. 删除对应的标识, 也就是 一个 key(一个文件) 的可能被多次操作.
                lruEntries.remove(key);
                return;
            }
        } else {
            key = line.substring(keyBegin, secondSpace);
        }
    
        //将key 值和 Entry() 保存在 lruEntries 中.
        Entry entry = lruEntries.get(key);
        if (entry == null) {
            entry = new Entry(key);
            lruEntries.put(key, entry);
        }
    
        if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
            //当前的状态为 Clean , 即将数据保存起来了, 或者洗衣个将会删除数据.
            //获取key 之后的string, 使用空格分割, 分割出来的 lengths 即时对应的 entry lengths 的值.
            String[] parts = line.substring(secondSpace + 1).split(" ");
            entry.readable = true;
            entry.currentEditor = null;
            entry.setLengths(parts);  //设置文件的长度.
        } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
            entry.currentEditor = new Editor(entry); //如果文件是脏数据(正在操作)时. 分配一个新的  Editor(关于 entry的 Editer(文件流))
        } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
            // This work was already done by calling lruEntries.get().
        } else {
            throw new IOException("unexpected journal line: " + line);
        }
    }
    
    /**
       * Computes the initial size and collects garbage as a part of opening the
       * cache. Dirty entries are assumed to be inconsistent and will be deleted.
       *
       * 1. 计算已经保存文件的长度,(CLEAN 标记的)
       * 2. 删除 DIRTY 标记的条目对应的 文本文件和 设置 entry 为null.
       *    并将自己从原本的LUR数列中删除掉, 擦除记录.
       */
    private void processJournal() throws IOException {
        deleteIfExists(journalFileTmp);//删除 临时文件.
        for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
            Entry entry = i.next();
            //文件的操作点是  CLEAR. 也就是干净的,
            if (entry.currentEditor == null) {
                //valueCount 是针对一个 key 能存多上个 value., 数据存在 Entry 里面.
                for (int t = 0; t < valueCount; t++) {
                    //增加文件的长度, size.
                    size += entry.lengths[t];
                }
            } else {
                entry.currentEditor = null;
                //删除对应的 cleanFIle 和脏数据, 只要key 值被标记了 REMOVE / 脏数据操作的标记, 那么之前就会有 CLEAN 操作
                // 这个地方会将原本的操作也删除掉.
                for (int t = 0; t < valueCount; t++) {
                    deleteIfExists(entry.getCleanFile(t));
                    deleteIfExists(entry.getDirtyFile(t));
                }
                //从数组中删除自己.
                i.remove();
            }
        }
    }
    
  4. open 总结

    经过open以后,journal文件肯定存在了;lruEntries里面肯定有值了;size存储了当前所有的实体占据的容量;。

存入缓存

  1. 示例

    String key = generateKey(url);  
    DiskLruCache.Editor editor = mDiskLruCache.edit(key); 
    OuputStream os = editor.newOutputStream(0); 
    //...after op
    editor.commit();
    
  2. 调用对外部提供的 edit 函数, 获取 Editor 对象

    /**
       * Returns an editor for the entry named {@code key}, or null if another
       * edit is in progress.
       *
       * 对外开发 获取 Editor 对象的函数, 根据 KEY获取 Editor 对象
       */
    public Editor edit(String key) throws IOException {
        return edit(key, ANY_SEQUENCE_NUMBER);
    }
    
    /**
         * 创建一个新的 Editor 对象, 将从 LUR 里面获取的 Entry / 重新创建的 Entry 对象赋值到 Editor 上面去.
         * 并给 Entry 赋值  entry.currentEditor = editor.
         *
         *  在日志文件中写入 DIRTY 操作日志.
         *
         *  注: 在初始化 Editor 之前,会先判断  entry.currentEditor != null ,
         *      如果
         *
         * 1. 检查当前的 write 是否被close, 检查key值的正常
         * 2. 创建一个新的 KEY / LUR 中获取, 并将 editor.currentEntry 指向当前的 entry.
         * 3. 记录一行脏数据操作, 并返回 Editor对象,
         */
    private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
        checkNotClosed();
        validateKey(key);
        Entry entry = lruEntries.get(key);
        if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
                                                              || entry.sequenceNumber != expectedSequenceNumber)) {
            return null; // Snapshot is stale.
        }
        //当 entry 为 null 时, 创建一个  entry 对象并保存到  LUR 里面去.
        if (entry == null) {
            entry = new Entry(key);
            lruEntries.put(key, entry);
            //当前的 editor 正在被操作, 直接返回 null.
        } else if (entry.currentEditor != null) {
            return null; // Another edit is in progress.
        }
    
        //创建一个新的 Editor对象, 并设置 currentEditor 对象为之前的 Entry, 或者 lruEntries.get(key) 获取的editor
        Editor editor = new Editor(entry);
        entry.currentEditor = editor;
    
        //设置脏当前的KEY 为脏数据标记.
        // Flush the journal before creating files to prevent file leaks.
        journalWriter.write(DIRTY + ' ' + key + '\n');
        journalWriter.flush();
        return editor;
    }
    
  3. Editor/ Entry 对象

     //空的 output , 对write 做的操作都不做任何事情.
      private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
        @Override
        public void write(int b) throws IOException {
          // Eat all writes silently. Nom nom.
        }
      };
    
      /** Edits the values for an entry. */
      public final class Editor {
        private final Entry entry;
        //对应的 写入者.
        private final boolean[] written;
        //FilterOutputStream 的任何流操作,出了异常,都会被标记为 error true.
        private boolean hasErrors;
        //是否提交完成标记
        private boolean committed;
    
        private Editor(Entry entry) {
          this.entry = entry;
          this.written = (entry.readable) ? null : new boolean[valueCount];
        }
    
        /**
         * Returns an unbuffered input stream to read the last committed value,
         * or null if no value has been committed.
         *  inputStream 需要外部自己close.
         */
        public InputStream newInputStream(int index) throws IOException {
          synchronized (DiskLruCache.this) {
            if (entry.currentEditor != this) {
              throw new IllegalStateException();
            }
            //数据没有被写入过.
            if (!entry.readable) {
              return null;
            }
            //数据被写入过, 直接返回  文件流 对象.
            try {
              return new FileInputStream(entry.getCleanFile(index));
            } catch (FileNotFoundException e) {
              return null;
            }
          }
        }
    
        /**
         * Returns the last committed value as a string, or null if no value
         * has been committed.
         */
        public String getString(int index) throws IOException {
          InputStream in = newInputStream(index);
          return in != null ? inputStreamToString(in) : null;
        }
    
        /**
         * Returns a new unbuffered output stream to write the value at
         * {@code index}. If the underlying output stream encounters errors
         * when writing to the filesystem, this edit will be aborted when
         * {@link #commit} is called. The returned output stream does not throw
         * IOExceptions.
         */
        /**
         * index 指的是在用户传入的 一个key 对应几个 文件的下标,
         */
        public OutputStream newOutputStream(int index) throws IOException {
          if (index < 0 || index >= valueCount) {
            throw new IllegalArgumentException("Expected index " + index + " to "
                    + "be greater than 0 and less than the maximum value count "
                    + "of " + valueCount);
          }
          synchronized (DiskLruCache.this) {
            if (entry.currentEditor != this) {
              throw new IllegalStateException();
            }
            //当对应的  entry 对应的 KEY 之前没有被写入, 那么 WRITTEN[i] 会被置为 true.
            if (!entry.readable) {
              written[index] = true;
            }
    
            //获取 outPut 写入文件的存放位置在 脏文件中,(.tmp), 其中是根据 index 来命名的.
            File dirtyFile = entry.getDirtyFile(index);
            FileOutputStream outputStream;
            //两次打开文件, 第一次可以判断文件夹不存在的时候, 会创建文件夹,
            //然后, 再次打开文件,
            //如果还是报错误了, 会返回一个对 Write 无处理的的 output,
            try {
              outputStream = new FileOutputStream(dirtyFile);
            } catch (FileNotFoundException e) {
              // Attempt to recreate the cache directory.
              directory.mkdirs();
              try {
                outputStream = new FileOutputStream(dirtyFile);
              } catch (FileNotFoundException e2) {
                // We are unable to recover. Silently eat the writes.
                return NULL_OUTPUT_STREAM;
              }
            }
            //返回正常情况下的 OutPutStream.,
            //数据操作中的  OutputWriter 操作的数据会被直接写入到脏文件中保存.
            return new FaultHidingOutputStream(outputStream);
          }
        }
    
        /** Sets the value at {@code index} to {@code value}. */
        public void set(int index, String value) throws IOException {
          //单独创建一个 Writer 对像, 将数据保存到指定文件中.
          Writer writer = null;
          try {
            writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
            writer.write(value);
          } finally {
            Util.closeQuietly(writer);
          }
        }
    
        /**
         * Commits this edit so it is visible to readers.  This releases the
         * edit lock so another edit may be started on the same key.
         *
         * 提交数据到硬盘上,当 hasErrors 不为 true 时,
         * 会调用  completeEdit(this, true) 函数将 数据写入到指定位置,
         *
         *  将tmp 文件写成 clean 文件.
         */
        public void commit() throws IOException {
          if (hasErrors) {
            completeEdit(this, false);
            remove(entry.key); // The previous entry is stale.
          } else {
            completeEdit(this, true);
          }
          committed = true;
        }
    
        /**
         * Aborts this edit. This releases the edit lock so another edit may be
         * started on the same key.
         *
         * 将temp 文件删除掉.
         */
        public void abort() throws IOException {
          completeEdit(this, false);
        }
    
        //在提交中, 想要中断提交.
        public void abortUnlessCommitted() {
          if (!committed) {
            try {
              abort();
            } catch (IOException ignored) {
            }
          }
        }
    
        //输出流的写函数都被 try/catch, 一旦报错,就在commit 的时候, 将 DIRTY 文件删除掉 
        //并将对应的 KEY 从对应的 LUR 数组中移除.
        private class FaultHidingOutputStream extends FilterOutputStream {
          private FaultHidingOutputStream(OutputStream out) {
            super(out);
          }
    
          @Override public void write(int oneByte) {
            try {
              out.write(oneByte);
            } catch (IOException e) {
              hasErrors = true;
            }
          }
    
          @Override public void write(byte[] buffer, int offset, int length) {
            try {
              out.write(buffer, offset, length);
            } catch (IOException e) {
              hasErrors = true;
            }
          }
    
          @Override public void close() {
            try {
              out.close();
            } catch (IOException e) {
              hasErrors = true;
            }
          }
    
          @Override public void flush() {
            try {
              out.flush();
            } catch (IOException e) {
              hasErrors = true;
            }
          }
        }
      }
    
      private final class Entry {
        //entry 对应的KEY值, 唯一标识符
        private final String key;
    
        /** Lengths of this entry's files.
         *  一个 KEY 对应的文件个数, 使用lengths 来表示.
         * */
        private final long[] lengths;
    
        /** True if this entry has ever been published.
         * 设置当前文件是否 可读, 在被成功写入时 "CLEAR" 会伴随这对应的 entry.readable 被标记为true.
         * readJournalLine() 时, 当前的标志为 CLEAN时, 会被标记为可读.
         * */
        private boolean readable;
    
        /** The ongoing edit or null if this entry is not being edited.
         *  当前编辑, 当状态为  Clear 是, 这个对象为 null,
         *  当为脏数据时,  currentEditor 不为空.
         * */
        private Editor currentEditor;
    
        /** The sequence number of the most recently committed edit to this entry. */
        //最近提交的序列号, 主要的用处是 Snapshot 对象获取快照, 但是原本的文件被改动了, 就直接return null.
        private long sequenceNumber;
    
        //输入一个新 KEY, 并创建一个 长度为 valueCount 的 int 数组 lengths
        private Entry(String key) {
          this.key = key;
          this.lengths = new long[valueCount];
        }
    
        //根据 lengths 的个数,返回  String, 使用分隔符 ' ' 分割.
        public String getLengths() throws IOException {
          StringBuilder result = new StringBuilder();
          for (long size : lengths) {
            result.append(' ').append(size);
          }
          return result.toString();
        }
    
        /** Set lengths using decimal numbers like "10123".
         *  这里的 valueCount 是指的起初 open() 设置进来的一个 Key 对应几个文件,
         *  其中每个文件以 0,1,2,3,... 来区分.
         *
         *  将对应文件的长度设置进来.
         * */
        private void setLengths(String[] strings) throws IOException {
          if (strings.length != valueCount) {
            throw invalidLengths(strings);
          }
    
          //保存 lengths 到 Entry 的 length 上面,
          try {
            for (int i = 0; i < strings.length; i++) {
              lengths[i] = Long.parseLong(strings[i]);
            }
          } catch (NumberFormatException e) {
            throw invalidLengths(strings);
          }
        }
    
        private IOException invalidLengths(String[] strings) throws IOException {
          throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
        }
    
        //对应的文件使用 test.0, test.1 ... 来存放
        public File getCleanFile(int i) {
          return new File(directory, key + "." + i);
        }
    
        //对应的文件使用 test.0, test.1 ... 来存放
        public File getDirtyFile(int i) {
          return new File(directory, key + "." + i + ".tmp");
        }
      }
    
  4. 对 outPutSrream 操作的关键操作, 调用 edit的commit 函数才会被调用, 在editor 参数被调用的 时候, 返回的 OutputStream 指向的是 DIRTY 文件, 不存在,创建.

    
        /**
         * 1. 保存/删除 缓存文件 --> success. success = true 时, 将脏数据文件 写成对应的 clean 文件, 并删除原本的脏数据文件
         *                                  success = false时, 将脏数据直接删除. 不保存成 clean文件,
         * 2. 多余的操作++ (DIRTY).
         *
         * 3. 检查是否关于  valueCount 长度的 output 都同事被操作聊(第一次的时候) , 不然直接报错了.  readable(只有已经被写入的文件才会有这个标记,)
         *
         * 4.
         *
         */
      private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
        Entry entry = editor.entry;
        if (entry.currentEditor != editor) {
          throw new IllegalStateException();
        }
    
        // If this edit is creating the entry for the first time, every index must have a value.
        //第一次写入文件,必须每个对应的index 都有值, 不然这个地方会 报错,
        // 当 valueCount = 1 的时候, 这个比较好理解,
        //当 valueCount = 3 的时候, 每次提交新的 KEY 的时候, 对应的 index 必须在 commit() 函数之前先调用,
        //不然在这个位置会报错误,
        if (success && !entry.readable) {
          for (int i = 0; i < valueCount; i++) {
            //written 这个数组是在 readable 为 false 的时候才会被置为 true.
            //也就是第一次使用对应KEY 时才会被置为TRUE.
            if (!editor.written[i]) {
              editor.abort();
                                             //新创建的条目没有为索引创建价值
              throw new IllegalStateException("Newly created entry didn't create value for index " + i);
            }
    
            //获取使用 newOutPutStream 操作的那个脏文件是否存在,
            //不存在时, 直接 return , 并记那个 success 置为false.
            if (!entry.getDirtyFile(i).exists()) {
              //删除对应的脏文件, 并重新写入对应的  CLEAN / REMOVE 的记录.
              editor.abort();
              return;
            }
          }
        }
    
        //将对应的脏文件修改为CLEAN  文件.
        for (int i = 0; i < valueCount; i++) {
          File dirty = entry.getDirtyFile(i);
          if (success) {
            //对应的 index 文件存在的时候,才会将文件 rename.
            if (dirty.exists()) {
              File clean = entry.getCleanFile(i);
              dirty.renameTo(clean);
    
              //重新设置 size 大小, 相对脏文件.
              long oldLength = entry.lengths[i];
              long newLength = clean.length();
              entry.lengths[i] = newLength;
              size = size - oldLength + newLength;
            }
          } else {
            //success = false 时, 将删除掉 脏数据文件.
            deleteIfExists(dirty);
          }
        }
    
        redundantOpCount++;
        entry.currentEditor = null;
        //当 readable 为true (之前被写入过), 或者 success时, 将重新写入 SIZE.
        if (entry.readable | success) {
          entry.readable = true;
          //重新写入 文件lengths 长度, 当对应的文件没有数据时, 长度为0.
          journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
          //当重新写入了 CLEAN 时, entry 会被重新写入 sequenceNumber.
          if (success) {
            entry.sequenceNumber = nextSequenceNumber++;
          }
        } else {
          //删除 LUR 里面的 KEY , 并写入 REMOVE 操作.
          lruEntries.remove(entry.key);
          journalWriter.write(REMOVE + ' ' + entry.key + '\n');
        }
        //关闭写入操作
        journalWriter.flush();
    
        //重新计算大小.
        if (size > maxSize || journalRebuildRequired()) {
          executorService.submit(cleanupCallable);
        }
      }
    
  5. 保证文件大小限制大小操作

     //执行的时机:
      //1. 在使用 get() 函数获取 Snapshot 对象的时候, 可能会触发一次    if (journalRebuildRequired())
      //2. 重新设置 MaxSize  setMaxSize(maxSize)                     if( journalRebuildRequired() )
      //3. 调用数据提交时: completeEdit();                            if (size > maxSize || journalRebuildRequired())
      //4. 调用 remove(key) 函数时.                                   if( journalRebuildRequired() )
      // journalRebuildRequired() 函数, 判断的是 :
      //  redundantOpCount 参数: 多余的操作次数: 1. 在调用 readJournal() 的时候会只有 clean() 并没有被移除的条目会被添加到 LUR 中,
      // 然后其他的多余的操作还有 remove() 会写入 REMOVE 字段,
      // completeEdit() 的时候会写入 DIRTY 字段,
      // get() 的时候会显示 READ 参数.
      private final Callable<Void> cleanupCallable = new Callable<Void>() {
        public Void call() throws Exception {
          synchronized (DiskLruCache.this) {
            if (journalWriter == null) {
              return null; // Closed.
            }
    
            //当 当前文件 SIZE 大于设置进来的 maxSize 值, 会移除不常使用的文件,
            //保证保存的文件是 最常使用的, 并全部长度加起来 < maxSize.
            //LUR 算法的优势.
            trimToSize();
            if (journalRebuildRequired()) {
    
              rebuildJournal();
              redundantOpCount = 0;
            }
          }
          return null;
        }
      };
    
  6. 检测文件大小操作

     //只有当 多余操作大于 2000 并且多余操作大于等于 缓存数据的长度.
      private boolean journalRebuildRequired() {
        final int redundantOpCompactThreshold = 2000;
        return redundantOpCount >= redundantOpCompactThreshold //
            && redundantOpCount >= lruEntries.size();
      }
    
  7. 检查 SIZE 是否大于 maxSize, 大于的时候,会删除不常用的 文件, 调用 remove 函数, 删除 KEY对应的文件.

    调用时机:

    • cleanupCallable() 被调用
    • close() 被调用时
    • flash() 被调用时
    /**
       *  借助 LUR 算法的帮助, 函数 trimToSize() 会删除最近不常使用的 key.
       *  http://blog.csdn.net/justloveyou_/article/details/71713781
       */
    private void trimToSize() throws IOException {
        while (size > maxSize) {
            Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
            remove(toEvict.getKey());
        }
    }
    

读操作

  1. 示例

    try {  
        String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
        String key = hashKeyForDisk(imageUrl);  
        DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);  
        if (snapShot != null) {  
            InputStream is = snapShot.getInputStream(0);  
            Bitmap bitmap = BitmapFactory.decodeStream(is);  
            mImage.setImageBitmap(bitmap);  
        }  
    } catch (IOException e) {  
        e.printStackTrace();  
    } 
    
  2. get() 函数

 /**
   * Returns a snapshot of the entry named {@code key}, or null if it doesn't
   * exist is not currently readable. If a value is returned, it is moved to
   * the head of the LRU queue.
   *
   * 1. 通过 KEY 拿到 readable 的 entry 对象, 可以被读写的 CLEAN 标记的 文件
   * 2. 申请一个 长度为 valueCount 的 InputStream 数据, 并将对应  entry 的 clean 文件赋值给 inputStream
   * 3. 为 KEY 写入 READ 标记, 多余的操作++
   * 4. 返回一个 Snapshot对象.
   */
  public synchronized Snapshot get(String key) throws IOException {
    checkNotClosed();
    validateKey(key);
    Entry entry = lruEntries.get(key);
    if (entry == null) {
      return null;
    }

    if (!entry.readable) {
      return null;
    }

    // Open all streams eagerly to guarantee that we see a single published
    // snapshot. If we opened streams lazily then the streams could come
    // from different edits.
    InputStream[] ins = new InputStream[valueCount];
    try {
      for (int i = 0; i < valueCount; i++) {
        ins[i] = new FileInputStream(entry.getCleanFile(i));
      }
    } catch (FileNotFoundException e) {
      // A file must have been deleted manually!
      for (int i = 0; i < valueCount; i++) {
        if (ins[i] != null) {
          Util.closeQuietly(ins[i]);
        } else {
          break;
        }
      }
      return null;
    }

    redundantOpCount++;
    journalWriter.append(READ + ' ' + key + '\n');
    if (journalRebuildRequired()) {
      executorService.submit(cleanupCallable);
    }

    return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
  }

其他操作

  1. remove(key)

     /**
       * Drops the entry for {@code key} if it exists and can be removed. Entries
       * actively being edited cannot be removed.
       *
       * @return true if an entry was removed.
       *
       * 1. 判断当前entry 的 currentEditor 是否为null, 为null 证明没有在操作,
       *    在被操作, 直接返回 false.
       * 2. 根据 valueCount 长度, 循环减去 将要被删除的文件长度大小, 更新 size 大小,
       *     重置 entry lengths 全部为0.
       *
       * 3. 多余的操作++ , 从 LUR 中删除 key, 和 检查当前多余操作是否过多,
       *       如果过多会被指向重写生成 journal文件,删除文件中 REMORE, READ 操作, 和已经被删除文件的
       *       KEY 操作
       */
      public synchronized boolean remove(String key) throws IOException {
        checkNotClosed();  //判断 写入流 是否为空,
        validateKey(key);
        Entry entry = lruEntries.get(key);
        //当当前文件还在被编辑, 或者当前的 entry 不被保存在 LUR 里面, 会直接返回 false.
        if (entry == null || entry.currentEditor != null) {
          return false;
        }
        //删除 对应的cleanFile 文件, 一个 key 对应对个文件.
        for (int i = 0; i < valueCount; i++) {
          File file = entry.getCleanFile(i);
          if (file.exists() && !file.delete()) {
            throw new IOException("failed to delete " + file);
          }
          //实时改变 size 大小
          size -= entry.lengths[i];
          //修改entry 的长度 length 大小
          entry.lengths[i] = 0;
        }
    
        redundantOpCount++;
        //写入 remove 操作条例到文件中.
        journalWriter.append(REMOVE + ' ' + key + '\n');
        //删除key.
        lruEntries.remove(key);
    
        //检测多余的操作数 > 2000 && > lur.size().
        if (journalRebuildRequired()) {
          //重新  rebuilder.
          executorService.submit(cleanupCallable);
        }
    
        return true;
      }
    
    
  2. public File getDirectory() : 返回当前缓存数据的目录

  3. public synchronized long getMaxSize() : 获取设置的缓存的最大大小

  4. public synchronized long size() : //获取当前存储的 占用 硬盘空间, byte.

  5. public synchronized boolean isClosed() : 判断写 日志文件的 journalWriter是否被 close() 掉了, 置为null

  6. public synchronized void flush() : 调用 trimToSize() 函数, 和关闭 调用 journalWriter 的 flush() 函数

  7. public synchronized void close() : 关闭当前写 LOG 文件的 journalWriter , 并停止所有的 写操作, 中断

  8. public void delete() : 删除传入的 缓存目录, 递归删除全部的文件 .

Done.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容