Android開源框架源碼鑑賞:LruCache與DiskLruCache

關於做者java

郭孝星,程序員,吉他手,主要從事Android平臺基礎架構方面的工做,歡迎交流技術方面的問題,能夠去個人Github提issue或者發郵件至guoxiaoxingse@163.com與我交流。android

文章目錄git

  • 一 Lru算法
  • 二 LruCache原理分析
    • 2.1 寫入緩存
    • 2.2 讀取緩存
    • 2.3 刪除緩存
  • 三 DiskLruCache原理分析
    • 3.1 寫入緩存
    • 3.2 讀取緩存
    • 3.3 刪除緩存

更多Android開源框架源碼分析文章請參見Android open framework analysis程序員

一 Lru算法

在分析LruCache與DiskLruCache以前,咱們先來簡單的瞭解下LRU算法的核心原理。github

LRU算法能夠用一句話來描述,以下所示:算法

LRU是Least Recently Used的縮寫,最近最久未使用算法,從它的名字就能夠看出,它的核心原則是若是一個數據在最近一段時間沒有使用到,那麼它在未來被 訪問到的可能性也很小,則這類數據項會被優先淘汰掉。shell

LRU算法流程圖以下所示:數組

瞭解了算法原理,咱們來思考一下若是是咱們來作,應該如何實現這個算法。從上圖能夠看出,雙向鏈表是一個好主意。緩存

假設咱們從表尾訪問數據,在表頭刪除數據,當訪問的數據項在鏈表中存在時,則將該數據項移動到表尾,不然在表尾新建一個數據項。當鏈表容量超過必定閾值,則移除表頭的數據。安全

好,以上即是整個Lru算法的原理,咱們接着來分析LruCache與DiskLruCache的實現。

二 LruCache原理分析

理解了Lru算法的原理,咱們接着從LruCache的使用入手,逐步分析LruCache的源碼實現。

👉 LruCache.java

在分析LruCache的源碼實現以前,咱們先來看看LruCache的簡單使用,以下所示:

int maxMemorySize = (int) (Runtime.getRuntime().totalMemory() / 1024);
int cacheMemorySize = maxMemorySize / 8;
LruCache<String, Bitmap> lrucache = new LruCache<String, Bitmap>(cacheMemorySize) {

    @Override
    protected int sizeOf(String key, Bitmap value) {
        return getBitmapSize(value);
    }

    @Override
    protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
        super.entryRemoved(evicted, key, oldValue, newValue);
    }

    @Override
    protected Bitmap create(String key) {
        return super.create(key);
    }
};
複製代碼

注:getBitmapSize()用來計算圖片佔內存的大小,具體方法參見附錄。

能夠發現,在使用LruCache的過程當中,須要咱們關注的主要有三個方法:

  • sizeOf():覆寫此方法實現本身的一套定義計算entry大小的規則。
  • V create(K key):若是key對象緩存被移除了,則調用次方法重建緩存。
  • entryRemoved(boolean evicted, K key, V oldValue, V newValue) :當key對應的緩存被刪除時回調該方法。

咱們來看看這三個方法的默認實現,以下所示:

public class LruCache<K, V> {
    
    //該方法默認返回1,也就是以entry的數量來計算entry的大小,這一般不符合咱們的需求,因此咱們通常會覆寫此方法。
    protected int sizeOf(K key, V value) {
        return 1;
    }
    protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
    protected V create(K key) {
        return null;
    }
}
複製代碼

能夠發現entryRemoved()方法爲空實現,create()方法也默認返回null。sizeOf()方法默認返回1,也就是以entry的數量來計算entry的大小,這一般不符合咱們的需求,因此咱們通常會覆寫此方法。

咱們前面提到,要實現Lru算法,能夠利用雙向鏈表。

假設咱們從表尾訪問數據,在表頭刪除數據,當訪問的數據項在鏈表中存在時,則將該數據項移動到表尾,不然在表尾新建一個數據項。當鏈表容量超過必定閾值,則移除表頭的數據。

LruCache使用的是LinkedHashMap,爲何會選擇LinkedHashMap呢?🤔

這跟LinkedHashMap的特性有關,LinkedHashMap的構造函數裏有個布爾參數accessOrder,當它爲true時,LinkedHashMap會以訪問順序爲序排列元素,不然以插入順序爲序排序元素。

public class LruCache<K, V> {
   public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
       super(initialCapacity, loadFactor);
       this.accessOrder = accessOrder;
   } 
}
複製代碼

咱們來寫個小例子驗證一下。

Map<Integer, Integer> map = new LinkedHashMap<>(5, 0.75F, true);
map.put(1, 1);
map.put(2, 2);
map.put(3, 3);
map.put(4, 4);
map.put(5, 5);

Log.d(TAG, "before visit");

for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
    Log.d(TAG, String.valueOf(entry.getValue()));
}

//訪問3,4兩個元素
map.get(3);
map.get(4);

Log.d(TAG, "after visit");

for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
    Log.d(TAG, String.valueOf(entry.getValue()));
}
複製代碼

程序輸入Log:

注:在LinkedHashMap中最近被方位的元素會被移動到表尾,LruCache也是從從表尾訪問數據,在表頭刪除數據,

能夠發現,最後訪問的數據就會被移動最尾端,這是符合咱們的預期的。全部在LruCache的構造方法中構造了一個這樣的LinkedHashMap。

public LruCache(int maxSize) {
    if (maxSize <= 0) {
        throw new IllegalArgumentException("maxSize <= 0");
    }
    this.maxSize = maxSize;
    this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
複製代碼

咱們再來看看LruCache是如何進行緩存的寫入、獲取和刪除的。

2.1 寫入緩存

寫入緩存是經過LruCache的put()方法實現的,以下所示:

public class LruCache<K, V> {
    
    public final V put(K key, V value) {
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;
        //加鎖,線程安全
        synchronized (this) {
            //插入的數量自增
            putCount++;
            //利用咱們提供的sizeOf()方法計算當前項的大小,並增長已有緩存size的大小
            size += safeSizeOf(key, value);
            //插入當前項、
            previous = map.put(key, value);
            //previous若是不爲空,則說明該項在原來的鏈表中以及存在,已有緩存大小size恢復到
            //之前的大小
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

        //回調entryRemoved()方法
        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }

        //調整緩存大小,若是緩存滿了,則按照Lru算法刪除對應的項。
        trimToSize(maxSize);
        return previous;
    }
    
    public void trimToSize(int maxSize) {
        //開啓死循環,知道緩存不滿爲止
        while (true) {
            K key;
            V value;
            synchronized (this) {
                //參數檢查
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }

                //若是緩存爲滿,直接返回 
                if (size <= maxSize) {
                    break;
                }

                //返回最近最久未使用的元素,也就是鏈表的表頭元素
                Map.Entry<K, V> toEvict = map.eldest();
                if (toEvict == null) {
                    break;
                }

                key = toEvict.getKey();
                value = toEvict.getValue();
                //刪除該表頭元素
                map.remove(key);
                //減小總緩存大小
                size -= safeSizeOf(key, value);
                //被刪除的項的數量自增
                evictionCount++;
            }
            //回到entryRemoved()方法
            entryRemoved(true, key, value, null);
        }
    }
}
複製代碼

整個插入元素的方法put()實現邏輯是很簡單的,以下所示:

  1. 插入元素,並相應增長當前緩存的容量。
  2. 調用trimToSize()開啓一個死循環,不斷的從表頭刪除元素,直到當前緩存的容量小於最大容量爲止。

2.2 讀取緩存

讀取緩存是經過LruCache的get()方法實現的,以下所示:

public class LruCache<K, V> {
    
    public final V get(K key) {
          if (key == null) {
              throw new NullPointerException("key == null");
          }
  
          V mapValue;
          synchronized (this) {
              //調用LinkedHashMap的get()方法,注意若是該元素存在,且accessOrder爲true,這個方法會
              //將該元素移動到表尾
              mapValue = map.get(key);
              if (mapValue != null) {
                  hitCount++;
                  return mapValue;
              }
              //
              missCount++;
          }
            
          //前面咱們就提到過,能夠覆寫create()方法,當獲取不到和key對應的元素時,嘗試調用create()方法
          //建立建元素,如下就是建立的過程,和put()方法流程相同。
          V createdValue = create(key);
          if (createdValue == null) {
              return null;
          }
  
          synchronized (this) {
              createCount++;
              mapValue = map.put(key, createdValue);
  
              if (mapValue != null) {
                  // There was a conflict so undo that last put
                  map.put(key, mapValue);
              } else {
                  size += safeSizeOf(key, createdValue);
              }
          }
  
          if (mapValue != null) {
              entryRemoved(false, key, createdValue, mapValue);
              return mapValue;
          } else {
              trimToSize(maxSize);
              return createdValue;
          }
      }
}
複製代碼

獲取元素的邏輯以下所示:

  1. 調用LinkedHashMap的get()方法,注意若是該元素存在,且accessOrder爲true,這個方法會將該元素移動到表尾.
  2. 當獲取不到和key對應的元素時,嘗試調用create()方法建立建元素,如下就是建立的過程,和put()方法流程相同。

2.3 刪除緩存

刪除緩存是經過LruCache的remove()方法實現的,以下所示:

public class LruCache<K, V> {
    
    public final V remove(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V previous;
        synchronized (this) {
            //調用對應LinkedHashMap的remove()方法刪除對應元素
            previous = map.remove(key);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            entryRemoved(false, key, previous, null);
        }

        return previous;
    }

}    
複製代碼

刪除元素的邏輯就比較簡單了,調用對應LinkedHashMap的remove()方法刪除對應元素。

三 DiskLruCache原理分析

👉 DiskLruCache.java

在分析DiskLruCache的實現原理以前,咱們先來寫個簡單的小例子,從例子出發去分析DiskLruCache的實現原理。

File directory = getCacheDir();
int appVersion = 1;
int valueCount = 1;
long maxSize = 10 * 1024;
DiskLruCache diskLruCache = DiskLruCache.open(directory, appVersion, valueCount, maxSize);

DiskLruCache.Editor editor = diskLruCache.edit(String.valueOf(System.currentTimeMillis()));
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(editor.newOutputStream(0));
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.scenery);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bufferedOutputStream);

editor.commit();
diskLruCache.flush();
diskLruCache.close();
複製代碼

這個就是DiskLruCache的大體使用流程,咱們來看看這個入口方法的實現,以下所示:

public final class DiskLruCache implements Closeable {
    
     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");
        }
    
        File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
        //若是備份文件存在
        if (backupFile.exists()) {
          File journalFile = new File(directory, JOURNAL_FILE);
          // 若是journal文件存在,則把備份文件journal.bkp是刪了
          if (journalFile.exists()) {
            backupFile.delete();
          } else {
            //若是journal文件不存在,則將備份文件命名爲journal
            renameTo(backupFile, journalFile, false);
          }
        }
    
        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        
        //判斷journal文件是否存在
        if (cache.journalFile.exists()) {
          //若是日誌文件以及存在
          try {
            //讀取journal文件,根據記錄中不一樣的操做類型進行相應的處理。
            cache.readJournal();
            //計算當前緩存容量的大小
            cache.processJournal();
            cache.journalWriter = new BufferedWriter(
                new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));
            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);
        //調用新的方法創建新的journal文件
        cache.rebuildJournal();
        return cache;
      }
}
複製代碼

先來講一下這個入口方法的四個參數的含義:

  • File directory:緩存目錄。
  • int appVersion:應用版本號。
  • int valueCount:一個key對應的緩存文件的數目,若是咱們傳入的參數大於1,那麼緩存文件後綴就是.0,.1等。
  • long maxSize:緩存容量上限。

DiskLruCache的構造方法並無作別的事情,只是簡單的將對應成員變量進行初始化,open()方法主要圍繞着journal文件的建立與讀寫而展開的,以下所示:

  • readJournal():讀取journal文件,主要是讀取文件頭裏的信息進行檢驗,而後調用readJournalLine()逐行去讀取,根據讀取的內容,執行相應的緩存 添加、移除等操做。
  • rebuildJournal():重建journal文件,重建journal文件主要是寫入文件頭(上面提到的journal文件都有的前面五行的內容)。
  • rocessJournal():計算當前緩存容量的大小。

咱們接着來分析什麼是journal文件,以及它的建立與讀寫流程。

3.1 journal文件的建立

在前面分析的open()方法中,主要圍繞着journal文件的建立和讀寫來展開的,那麼journal文件是什麼呢?🤔

咱們若是去打開緩存目錄,就會發現除了緩存文件,還會發現一個journal文件,journal文件用來記錄緩存的操做記錄的,以下所示:

libcore.io.DiskLruCache
1
1
1

DIRTY 1517126350519
CLEAN 1517126350519 5325928
REMOVE 1517126350519
複製代碼

注:這裏的緩存目錄是應用的緩存目錄/data/data/pckagename/cache,未root的手機能夠經過如下命令進入到該目錄中或者將該目錄總體拷貝出來:

//進入/data/data/pckagename/cache目錄
adb shell
run-as com.your.packagename 
cp /data/data/com.your.packagename/

//將/data/data/pckagename目錄拷貝出來
adb backup -noapk com.your.packagename
複製代碼

咱們來分析下這個文件的內容:

  • 第一行:libcore.io.DiskLruCache,固定字符串。
  • 第二行:1,DiskLruCache源碼版本號。
  • 第三行:1,App的版本號,經過open()方法傳入進去的。
  • 第四行:1,每一個key對應幾個文件,通常爲1.
  • 第五行:空行
  • 第六行及後續行:緩存操做記錄。

第六行及後續行表示緩存操做記錄,關於操做記錄,咱們須要瞭解如下三點:

  1. DIRTY 表示一個entry正在被寫入。寫入分兩種狀況,若是成功會緊接着寫入一行CLEAN的記錄;若是失敗,會增長一行REMOVE記錄。注意單獨只有DIRTY狀態的記錄是非法的。
  2. 當手動調用remove(key)方法的時候也會寫入一條REMOVE記錄。
  3. READ就是說明有一次讀取的記錄。
  4. CLEAN的後面還記錄了文件的長度,注意可能會一個key對應多個文件,那麼就會有多個數字。

這幾種操做對應到DiskLruCache源碼中,以下所示:

private static final String CLEAN = "CLEAN";
private static final String DIRTY = "DIRTY";
private static final String REMOVE = "REMOVE";
private static final String READ = "READ";

複製代碼

那麼構建一個新的journal文件呢?上面咱們也說過這是調用rebuildJournal()方法來完成的。

rebuildJournal()

public final class DiskLruCache implements Closeable {
    
     static final String MAGIC = "libcore.io.DiskLruCache";
    
     private synchronized void rebuildJournal() throws IOException {
        if (journalWriter != null) {
          journalWriter.close();
        }
    
        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");
    
          for (Entry entry : lruEntries.values()) {
            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()) {
          renameTo(journalFile, journalFileBackup, true);
        }
        renameTo(journalFileTmp, journalFile, false);
        journalFileBackup.delete();
    
        journalWriter = new BufferedWriter(
            new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
      }
}
複製代碼

你能夠發現,構建一個新的journal文件過程就是寫入文件頭的過程,文件頭內容包含前面咱們說的appVersion、valueCount、空行等五行內容。

咱們再來看看如何讀取journal文件裏的內容。

readJournal()

public final class DiskLruCache implements Closeable {
   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 {
             //開啓死循環,逐行讀取journal內容
             readJournalLine(reader.readLine());
             //文件以及讀取的行數
             lineCount++;
           } catch (EOFException endOfJournal) {
             break;
           }
         }
         //lineCount表示文件總行數,lruEntries.size()表示最終緩存的個數,redundantOpCount
         //就表示非法緩存記錄的個數,這些非法緩存記錄會被移除掉。
         redundantOpCount = lineCount - lruEntries.size();
       } finally {
         Util.closeQuietly(reader);
       }
     }
   
     private void readJournalLine(String line) throws IOException {
       //每行記錄都是用空格開分隔的,這裏取第一個空格出現的位置
       int firstSpace = line.indexOf(' ');
       //若是沒有空格,則說明是非法的記錄
       if (firstSpace == -1) {
         throw new IOException("unexpected journal line: " + line);
       }
   
       //第一個空格前面就是CLEAN、READ這些操做類型,接下來針對不一樣的操做類型進行
       //相應的處理
       int keyBegin = firstSpace + 1;
       int secondSpace = line.indexOf(' ', keyBegin);
       final String key;
       if (secondSpace == -1) {
         key = line.substring(keyBegin);
         //1. 若是該條記錄以REMOVE爲開頭,則執行刪除操做。
         if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
           lruEntries.remove(key);
           return;
         }
       } else {
         key = line.substring(keyBegin, secondSpace);
       }
   
       //2. 若是該key不存在,則新建Entry並加入lruEntries。
       Entry entry = lruEntries.get(key);
       if (entry == null) {
         entry = new Entry(key);
         lruEntries.put(key, entry);
       }
   
       //3. 若是該條記錄以CLEAN爲開頭,則初始化entry,並設置entry.readable爲true、設置entry.currentEditor爲
       //null,初始化entry長度。
       //CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
       if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
         //數組中實際上是數字,其實就是文件的大小。由於能夠經過valueCount來設置一個key對應的value的個數,
         //因此文件大小也是有valueCount個
         String[] parts = line.substring(secondSpace + 1).split(" ");
         entry.readable = true;
         entry.currentEditor = null;
         entry.setLengths(parts);
       }
       //4. 若是該條記錄以DIRTY爲開頭。則設置currentEditor對象。
       //DIRTY 335c4c6028171cfddfbaae1a9c313c52
       else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
         entry.currentEditor = new Editor(entry);
       } 
       //5. 若是該條記錄以READ爲開頭,則什麼也不作。
       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);
       }
     } 
}
複製代碼

新來講一下這個lruEntries是什麼,以下所示:

private final LinkedHashMap<String, Entry> lruEntries =
  new LinkedHashMap<String, Entry>(0, 0.75f, true);
複製代碼

就跟上面的LruCache同樣,它也是一個以訪問順序爲序的LinkedHashMap,能夠用它來實現Lru算法。

該方法的邏輯就是根據記錄中不一樣的操做類型進行相應的處理,以下所示:

  1. 若是該條記錄以REMOVE爲開頭,則執行刪除操做。
  2. 若是該key不存在,則新建Entry並加入lruEntries。
  3. 若是該條記錄以CLEAN爲開頭,則初始化entry,並設置entry.readable爲true、設置entry.currentEditor爲null,初始化entry長度。
  4. 若是該條記錄以DIRTY爲開頭。則設置currentEditor對象。
  5. 若是該條記錄以READ爲開頭,則什麼也不作。

說了這麼多,readJournalLine()方法主要是經過讀取journal文件的每一行,而後封裝成entry對象,放到了LinkedHashMap集合中。而且根據每一行不一樣的開頭,設置entry的值。也就是說經過讀取這 個文件,咱們把全部的在本地緩存的文件的key都保存到了集合中,這樣咱們用的時候就能夠經過集合來操做了。

processJournal()

public final class DiskLruCache implements Closeable {
    
      private void processJournal() throws IOException {
        //刪除journal.tmp臨時文件
        deleteIfExists(journalFileTmp);
        //變量緩存集合裏的全部元素
        for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
          Entry entry = i.next();
          //若是當前元素entry的currentEditor不爲空,則計算該元素的總大小,並添加到總緩存容量size中去
          if (entry.currentEditor == null) {
            for (int t = 0; t < valueCount; t++) {
              size += entry.lengths[t];
            }
          } 
          //若是當前元素entry的currentEditor不爲空,表明該元素時非法緩存記錄,該記錄以及對應的緩存文件
          //都會被刪除掉。
          else {
            entry.currentEditor = null;
            for (int t = 0; t < valueCount; t++) {
              deleteIfExists(entry.getCleanFile(t));
              deleteIfExists(entry.getDirtyFile(t));
            }
            i.remove();
          }
        }
      }
}
複製代碼

這裏提到了一個很是緩存記錄,那麼什麼是非法緩存記錄呢?🤔

DIRTY 表示一個entry正在被寫入。寫入分兩種狀況,若是成功會緊接着寫入一行CLEAN的記錄;若是失敗,會增長一行REMOVE記錄。注意單獨只有DIRTY狀態的記錄是非法的。

該方法主要用來計算當前的緩存總容量,並刪除非法緩存記錄以及該記錄對應的文件。

理解了journal文件的建立以及讀寫流程,咱們來看看硬盤緩存的寫入、讀取和刪除的過程。

3.2 寫入緩存

DiskLruCache緩存的寫入是經過edit()方法來完成的,以下所示:

public final class DiskLruCache implements Closeable {
    
    private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
        checkNotClosed();
        validateKey(key);
        //從以前的緩存中讀取對應的entry
        Entry entry = lruEntries.get(key);
        //當前沒法寫入磁盤緩存
        if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
            || entry.sequenceNumber != expectedSequenceNumber)) {
          return null; // Snapshot is stale.
        }
        
        //若是entry爲空,則新建一個entry對象加入到緩存集合中
        if (entry == null) {
          entry = new Entry(key);
          lruEntries.put(key, entry);
        } 
        //currentEditor不爲空,表示當前有別的插入操做在執行
        else if (entry.currentEditor != null) {
          return null; // Another edit is in progress.
        }
    
        //爲當前建立的entry知道新建立的editor
        Editor editor = new Editor(entry);
        entry.currentEditor = editor;
    
        //向journal寫入一行DIRTY + 空格 + key的記錄,表示這個key對應的緩存正在處於被編輯的狀態。
        journalWriter.write(DIRTY + ' ' + key + '\n');
        //刷新文件裏的記錄
        journalWriter.flush();
        return editor;
      }
}
複製代碼

這個方法構建了一個Editor對象,它主要作了兩件事情:

  1. 從集合中找到對應的實例(若是沒有建立一個放到集合中),而後建立一個editor,將editor和entry關聯起來。
  2. 向journal中寫入一行操做數據(DITTY 空格 和key拼接的文字),表示這個key當前正處於編輯狀態。

咱們在前面的DiskLruCache的使用例子中,調用了Editor的newOutputStream()方法建立了一個OutputStream來寫入緩存文件。以下所示:

public final class DiskLruCache implements Closeable {
    
    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;
        }
      }
    }
}
複製代碼

這個方法的形參index就是咱們開始在open()方法裏傳入的valueCount,這個valueCount表示了一個key對應幾個value,也就是說一個key對應幾個緩存文件。那麼如今傳入的這個index就表示 要緩存的文件時對應的第幾個value。

有了輸出流,咱們在接着調用Editor的commit()方法就能夠完成緩存文件的寫入了,以下所示:

public final class DiskLruCache implements Closeable {
     public void commit() throws IOException {
         //若是經過輸出流寫入緩存文件出錯了就把集合中的緩存移除掉
          if (hasErrors) {
            completeEdit(this, false);
            remove(entry.key); // The previous entry is stale.
          } else {
            //調用completeEdit()方法完成緩存寫入。
            completeEdit(this, true);
          }
          committed = true;
        }
}
複製代碼

能夠看到該方法調用DiskLruCache的completeEdit()方法來完成緩存寫入,以下所示:

public final class DiskLruCache implements Closeable {
    
    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.
        if (success && !entry.readable) {
          for (int i = 0; i < valueCount; i++) {
            if (!editor.written[i]) {
              editor.abort();
              throw new IllegalStateException("Newly created entry didn't create value for index " + i);
            }
            if (!entry.getDirtyFile(i).exists()) {
              editor.abort();
              return;
            }
          }
        }
    
        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;
          //向journal寫入一行CLEAN開頭的記錄,表示緩存成功寫入到磁盤。
          journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
          if (success) {
            entry.sequenceNumber = nextSequenceNumber++;
          }
        } else {
          //若是不成功,則從集合中刪除掉這個緩存
          lruEntries.remove(entry.key);
          //向journal文件寫入一行REMOVE開頭的記錄,表示刪除了緩存
          journalWriter.write(REMOVE + ' ' + entry.key + '\n');
        }
        journalWriter.flush();
    
        //若是緩存總大小已經超過了設定的最大緩存大小或者操做次數超過了2000次,
        // 就開一個線程將集合中的數據刪除到小於最大緩存大小爲止並從新寫journal文件
        if (size > maxSize || journalRebuildRequired()) {
          executorService.submit(cleanupCallable);
        }
      }
}
複製代碼

這個方法一共作了如下幾件事情:

  1. 若是輸出流寫入數據成功,就把寫入的臨時文件重命名爲正式的緩存文件
  2. 從新設置當前總緩存的大小
  3. 向journal文件寫入一行CLEAN開頭的字符(包括key和文件的大小,文件大小可能存在多個 使用空格分開的)
  4. 若是輸出流寫入失敗,就刪除掉寫入的臨時文件,而且把集合中的緩存也刪除
  5. 向journal文件寫入一行REMOVE開頭的字符
  6. 從新比較當前緩存和最大緩存的大小,若是超過最大緩存或者journal文件的操做大於2000條,就把集合中的緩存刪除一部分,直到小於最大緩存,從新創建新的journal文件

到這裏,緩存的插入流程就完成了。

3.3 讀取緩存

讀取緩存是由DiskLruCache的get()方法來完成的,以下所示:

public final class DiskLruCache implements Closeable {
    
      public synchronized Snapshot get(String key) throws IOException {
        checkNotClosed();
        validateKey(key);
        //獲取對應的entry
        Entry entry = lruEntries.get(key);
        if (entry == null) {
          return null;
        }
    
        //若是entry不可讀,說明可能在編輯,則返回空。
        if (!entry.readable) {
          return null;
        }
    
        //打開全部緩存文件的輸入流,等待被讀取。
        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++;
        //向journal寫入一行READ開頭的記錄,表示執行了一次讀取操做
        journalWriter.append(READ + ' ' + key + '\n');
        
         
        //若是緩存總大小已經超過了設定的最大緩存大小或者操做次數超過了2000次,
        // 就開一個線程將集合中的數據刪除到小於最大緩存大小爲止並從新寫journal文件
        if (journalRebuildRequired()) {
          executorService.submit(cleanupCallable);
        }
        
        //返回一個緩存文件快照,包含緩存文件大小,輸入流等信息。
        return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
      }
}
複製代碼

讀取操做主要完成了如下幾件事情:

  1. 獲取對應的entry。
  2. 打開全部緩存文件的輸入流,等待被讀取。
  3. 向journal寫入一行READ開頭的記錄,表示執行了一次讀取操做。
  4. 若是緩存總大小已經超過了設定的最大緩存大小或者操做次數超過了2000次,就開一個線程將集合中的數據刪除到小於最大緩存大小爲止並從新寫journal文件。
  5. 返回一個緩存文件快照,包含緩存文件大小,輸入流等信息。

該方法最終返回一個緩存文件快照,包含緩存文件大小,輸入流等信息。利用這個快照咱們就能夠讀取緩存文件了。

3.4 刪除緩存

刪除緩存是由DiskLruCache的remove()方法來完成的,以下所示:

public final class DiskLruCache implements Closeable {
    
      public synchronized boolean remove(String key) throws IOException {
        checkNotClosed();
        validateKey(key);
        //獲取對應的entry
        Entry entry = lruEntries.get(key);
        if (entry == null || entry.currentEditor != null) {
          return false;
        }
    
        //刪除對應的緩存文件,並將緩存大小置爲0.
        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 -= entry.lengths[i];
          entry.lengths[i] = 0;
        }
    
        redundantOpCount++;
        //向journal文件添加一行REMOVE開頭的記錄,表示執行了一次刪除操做。
        journalWriter.append(REMOVE + ' ' + key + '\n');
        lruEntries.remove(key);
    
    
        //若是緩存總大小已經超過了設定的最大緩存大小或者操做次數超過了2000次,
        // 就開一個線程將集合中的數據刪除到小於最大緩存大小爲止並從新寫journal文件
        if (journalRebuildRequired()) {
          executorService.submit(cleanupCallable);
        }
    
        return true;
      }   
}
複製代碼

刪除操做主要作了如下幾件事情:

  1. 獲取對應的entry。
  2. 刪除對應的緩存文件,並將緩存大小置爲0.
  3. 向journal文件添加一行REMOVE開頭的記錄,表示執行了一次刪除操做。
  4. 若是緩存總大小已經超過了設定的最大緩存大小或者操做次數超過了2000次,就開一個線程將集合中的數據刪除到小於最大緩存大小爲止並從新寫journal文件。

好,到這裏LrcCache和DiskLruCache的實現原理都講完了,這兩個類在主流的圖片框架Fresco、Glide和網絡框架Okhttp等都有着普遍的應用,後續的文章後繼續分析LrcCache和DiskLruCache 在這些框架裏的應用。

附錄

圖片佔用內存大小的計算

Android裏面緩存應用最多的場景就是圖片緩存了,誰讓圖片在內存裏是個大胖子呢,在作緩存的時候咱們常常會去計算圖片展內存的大小。

那麼如何去獲取一張圖片佔用內存的大小呢?🤔

private int getBitmapSize(Bitmap bitmap) {
    //API 19
    if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
        return bitmap.getAllocationByteCount();
    }
    //API 12
    if (Build.VERSION.SDK_INT == Build.VERSION_CODES.HONEYCOMB_MR1) {
        return bitmap.getByteCount();
    }
    // Earlier Version
    return bitmap.getRowBytes() * bitmap.getHeight();
}
複製代碼

那麼這三個方法處了版本上的差別,具體有什麼區別呢?

getRowBytes()返回的是每行的像素值,乘以高度就是總的像素數,也就是佔用內存的大小。 getAllocationByteCount()與getByteCount()的返回值通常狀況下都是相等的。只是在圖片 複用的時候,getAllocationByteCount()返回的是複用圖像所佔內存的大小,getByteCount()返回的是新解碼圖片佔用內存的大小。

咱們來寫一個小例子驗證一下,以下所示:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inDensity = 320;
options.inTargetDensity = 320;
//要實現複用,圖像必須是可變的,也就是inMutable爲true。
options.inMutable = true;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.scenery, options);
Log.d(TAG, "bitmap.getAllocationByteCount(): " + String.valueOf(bitmap.getAllocationByteCount()));
Log.d(TAG, "bitmap.getByteCount(): " + String.valueOf(bitmap.getByteCount()));
Log.d(TAG, "bitmap.getRowBytes() * bitmap.getHeight(): " + String.valueOf(bitmap.getRowBytes() * bitmap.getHeight()));

BitmapFactory.Options reuseOptions = new BitmapFactory.Options();
reuseOptions.inDensity = 320;
reuseOptions.inTargetDensity = 320;
//要複用的Bitmap
reuseOptions.inBitmap = bitmap;
//要實現複用,圖像必須是可變的,也就是inMutable爲true。
reuseOptions.inMutable = true;
Bitmap reuseBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.scenery_reuse, reuseOptions);
Log.d(TAG, "reuseBitmap.getAllocationByteCount(): " + String.valueOf(reuseBitmap.getAllocationByteCount()));
Log.d(TAG, "reuseBitmap.getByteCount(): " + String.valueOf(reuseBitmap.getByteCount()));
Log.d(TAG, "reuseBitmap.getRowBytes() * reuseBitmap.getHeight(): " + String.valueOf(reuseBitmap.getRowBytes() * reuseBitmap.getHeight()));
複製代碼

運行的log以下所示:

能夠發現reuseBitmap的getAllocationByteCount()和getByteCount()返回不同,getAllocationByteCount()返回的是複用bitmap佔用內存的大小, getByteCount()返回的是新的reuseOptions實際解碼佔用的內存大小。

注意在複用圖片的時候,options.inMutable必須設置爲true,不然沒法進行復用,以下所示:

相關文章
相關標籤/搜索