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閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容