天天看點

Android緩存機制&一個緩存架構推薦1、先推薦一個輕量級緩存架構——ACache(ASimpleCache)2、Android緩存機制

1、先推薦一個輕量級緩存架構——ACache(ASimpleCache)

ACache介紹: ACache類似于SharedPreferences,但是比 SharedPreferences功能更加強大,SharedPreferences隻能儲存一些基本資料類型、Serializable、Bundle等資料, 而Acache可以緩存如下資料: 普通的字元串、JsonObject、JsonArray、Bitmap、Drawable、序列化的java對象,和 byte資料。

主要特色:

  • 1:輕,輕到隻有一個JAVA檔案。
  • 2:可配置,可以配置緩存路徑,緩存大小,緩存數量等。
  • 3:可以設定緩存逾時時間,緩存逾時自動失效,并被删除。
  • 4:支援多程序。

應用場景:

  • 1、替換SharePreference當做配置檔案
  • 2、可以緩存網絡請求資料,比如oschina的android用戶端可以緩存http請求的新聞内容,緩存時間假設為1個小時,逾時後自動失效,讓用戶端重新請求新的資料,減少用戶端流量,同時減少伺服器并發量。
  • 3、您來說...

下載下傳連結: https://github.com/yangfuhai/ASimpleCache 架構分析: http://blog.csdn.net/zhoubin1992/article/details/46379055

2、Android緩存機制

Android緩存分為 記憶體緩存和 檔案緩存(磁盤緩存)。在早期,各大圖檔緩存架構流行之前,常用的記憶體緩存方式是軟引用(SoftReference)和弱引用(WeakReference),如大部分的使用方式:HashMap<String url, SoftReference<Drawable>> imageCache;這種形式。從Android 2.3(Level 9)開始,垃圾回收器更傾向于回收 SoftReference或WeakReference對象,這使得SoftReference和WeakReference變得不是那麼實用有效。同時,到了Android 3.0(Level 11)之後,圖檔資料Bitmap被放置到了記憶體的堆區域,而堆區域的記憶體是由GC管理的,開發者也就不需要進行圖檔資源的釋放工作,但這也使得圖檔資料的釋放無法預知,增加了造成OOM的可能。是以,在Android3.1以後,Android推出了LruCache這個記憶體緩存類,LruCache中的對象是強引用的。

2.1 記憶體緩存——LruCache源碼分析

    2.1.1 LRU

    LRU,全稱Least Rencetly Used,即最近最少使用,是一種非常常用的置換算法,也即淘汰最長時間未使用的對象。LRU在作業系統中的頁面置換算法中廣泛使用,我們的記憶體或緩存空間是有限的,當新加入一個對象時,造成我們的緩存空間不足了,此時就需要根據某種算法對緩存中原有資料進行淘汰貨删除,而LRU選擇的是将最長時間未使用的對象進行淘汰。     

   2.1.2 LruCache實作原理

    根據LRU算法的思想,要實作LRU最核心的是要有一種資料結構能夠基于 通路順序來儲存緩存中的對象,這樣我們就能夠很友善的知道哪個對象是最近通路的,哪個對象是最長時間未通路的。LruCache選擇的是LinkedHashMap這個資料結構, LinkedHashMap是一個雙向循環連結清單,在構造LinkedHashMap時,通過一個boolean值來指定LinkedHashMap中儲存資料的方式,LinkedHashMap的一個構造方法如下:     

/*
     * 初始化LinkedHashMap
     * 第一個參數:initialCapacity,初始大小
     * 第二個參數:loadFactor,負載因子=0.75f
     * 第三個參數:accessOrder=true,基于通路順序;accessOrder=false,基于插入順序
     */
    public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor);
        init();
        this.accessOrder = accessOrder;
    }
           

顯然,在LruCache中選擇的是 accessOrder = true;此時,當accessOrder 設定為 true時,每當我們更新(即調用put方法)或通路(即調用get方法)map中的結點時,LinkedHashMap内部都會将這個結點移動到連結清單的尾部,是以,在連結清單的尾部是最近剛剛使用的結點,在連結清單的頭部是是最近最少使用的結點,當我們的緩存空間不足時,就應該持續把連結清單頭部結點移除掉,直到有剩餘空間放置新結點。 可以看到,LinkedHashMap完成了LruCache中的核心功能,那LruCache中剩下要做的就是定義緩存空間總容量,目前儲存資料已使用的容量,對外提供put、get方法。     

    2.1.3 LruCache源碼分析

    在了解了LruCache的核心原理之後,就可以開始分析 LruCache的源碼了。      (1)關鍵字段     根據上面的分析,首先要有總容量、已使用容量、linkedHashMap這幾個關鍵字段,LruCache中提供了下面三個關鍵字段:     

//核心資料結構
    private final LinkedHashMap<K, V> map;
    // 目前緩存資料所占的大小
    private int size;
    //緩存空間總容量
    private int maxSize;
           

      要注意的是size字段,因為map中可以存放各種類型的資料,這些資料的大小測量方式也是不一樣的,比如Bitmap類型的資料和String類型的資料計算他們的大小方式肯定不同,是以,LruCache中在計算放入資料大小的方法sizeOf中,隻是簡單的傳回了1,需要我們重寫這個方法,自己去定義資料的測量方式。是以,我們在使用LruCache的時候,經常會看到這種方式:     

private static final int CACHE_SIZE = 4 * 1024 * 1024;//4Mib
    LruCache<String,Bitmap> bitmapCache = new LruCache<String,Bitmap>(CACHE_SIZE){
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getByteCount();//自定義Bitmap資料大小的計算方式
        }
    };
           

     (2)構造方法  

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隻有一個唯一的構造方法,在構造方法中,給定了緩存空間的總大小,初始化了 LinkedHashMap核心資料結構,在LinkedHashMap中的第三個參數指定為true,也就設定了accessOrder=true,表示這個LinkedHashMap将是基于資料的通路順序進行排序。

     (3)sizeOf()和safeSizeOf()方法     根據上面的解釋,由于各種資料類型大小測量的标準不統一,具體測量的方法應該由使用者來實作,如上面給出的一個在實作LruCache時重寫sizeOf的一種常用實作方式。通過多态的性質,再具體調用sizeOf時會調用我們重寫的方法進行測量,LruCache對sizeOf()的調用進行一層封裝,如下:     

private int safeSizeOf(K key, V value) {
    int result = sizeOf(key, value);
    if (result < 0) {
        throw new IllegalStateException("Negative size: " + key + "=" + value);
    }
    return result;
}      

裡面其實就是調用sizeOf()方法,傳回sizeOf計算的大小。 上面就是LruCache的基本内容,下面就需要提供 LruCache的核心功能了。

     (4)put方法緩存資料     首先看一下它的源碼實作:     

/**
   * 給對應key緩存value,并且将該value移動到連結清單的尾部。
   */
public final V put(K key, V value) {
    if (key == null || value == null) {
        throw new NullPointerException("key == null || value == null");
    }

      V previous;
      synchronized (this) {
        // 記錄 put 的次數
        putCount++;
        // 通過鍵值對,計算出要儲存對象value的大小,并更新目前緩存大小
        size += safeSizeOf(key, value);
        /*
         * 如果 之前存在key,用新的value覆寫原來的資料, 并傳回 之前key 的value
         * 記錄在 previous
         */
        previous = map.put(key, value);
        // 如果之前存在key,并且之前的value不為null
        if (previous != null) {
            // 計算出 之前value的大小,因為前面size已經加上了新的value資料的大小,此時,需要再次更新size,減去原來value的大小
            size -= safeSizeOf(key, previous);
        }
      }

    // 如果之前存在key,并且之前的value不為null
    if (previous != null) {
        /*
         * previous值被剔除了,此次添加的 value 已經作為key的 新值
         * 告訴 自定義 的 entryRemoved 方法
         */
        entryRemoved(false, key, previous, value);
    }
    //裁剪緩存容量(在目前緩存資料大小超過了總容量maxSize時,才會真正去執行LRU)
    trimToSize(maxSize);
      return previous;
}      

可以看到,put()方法主要有以下幾步: 1)key和value判空,說明LruCache中不允許key和value為null; 2)通過safeSizeOf()擷取要加入對象資料的大小,并更新目前緩存資料的大小; 3)将新的對象資料放入到緩存中,即調用 LinkedHashMap的put方法,如果原來存在該key時,直接替換掉原來的value值,并傳回之前的value值,得到之前value的大小,更新目前緩存資料的size大小;如果原來不存在該key,則直接加入緩存即可; 4)清理緩存空間,如下;

     (5)trimToSize()清理緩存空間     當我們加入一個資料時(put),為了保證目前資料的緩存所占大小沒有超過我們指定的總大小,通過調用trimToSize()來對緩存空間進行管理控制。如下:

public void trimToSize(int maxSize) {
    /*
     * 循環進行LRU,直到目前所占容量大小沒有超過指定的總容量大小
     */
    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 || map.isEmpty()) {
                break;
            }
            /**
             * 執行到這,表示目前緩存資料已超過了總容量,需要執行LRU,即将最近最少使用的資料清除掉,直到資料所占緩存空間沒有超标;
             * 根據前面的原理分析,知道,在連結清單中,連結清單的頭結點是最近最少使用的資料,是以,最先清除掉連結清單前面的結點
             */
            Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
            key = toEvict.getKey();
            value = toEvict.getValue();
            map.remove(key);
            // 移除掉後,更新目前資料緩存的大小
            size -= safeSizeOf(key, value);
            // 更新移除的結點數量
            evictionCount++;
        }
        /*
         * 通知某個結點被移除,類似于回調
         */
        entryRemoved(true, key, value, null);
    }
}      

trimToSize()方法的作用就是為了保證目前資料的緩存大小不能超過我們指定的緩存總大小,如果超過了,就會開始移除最近最少使用的資料,直到size符合要求。trimToSize()方法在put()的時候一定會調用,在get()的時候有可能會調用。

    (6)get方法擷取緩存資料     get方法源碼如下:  

/**
 * 根據key查詢緩存,如果該key對應的value存在于緩存,直接傳回value;
* 通路到這個結點時,LinkHashMap會将它移動到雙向循環連結清單的的尾部。
* 如果如果沒有緩存的值,則傳回null。(如果開發者重寫了create()的話,傳回建立的value)
*/
public final V get(K key) {
    if (key == null) {
        throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized (this) {
        // LinkHashMap 如果設定按照通路順序的話,這裡每次get都會重整資料順序
        mapValue = map.get(key);
        // 計算 命中次數
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        // 計算 丢失次數
        missCount++;
    }

    /*
     * 官方解釋:
     * 嘗試建立一個值,這可能需要很長時間,并且Map可能在create()傳回的值時有所不同。如果在create()執行的時
     * 候,用這個key執行了put方法,那麼此時就發生了沖突,我們在Map中删除這個建立的值,釋放被建立的值,保留put進去的值。
     */
    V createdValue = create(key);
    if (createdValue == null) {
        return null;
    }

    /***************************
     * 不覆寫create方法走不到下面 *
     ***************************/
    /*
     * 正常情況走不到這裡
     * 走到這裡的話 說明 實作了自定義的 create(K key) 邏輯
     * 因為預設的 create(K key) 邏輯為null
     */
    synchronized (this) {
        // 記錄 create 的次數
        createCount++;
        // 将自定義create建立的值,放入LinkedHashMap中,如果key已經存在,會傳回 之前相同key 的值
        mapValue = map.put(key, createdValue);

        // 如果之前存在相同key的value,即有沖突。
        if (mapValue != null) {
            /*
             * 有沖突
             * 是以 撤銷 剛才的 操作
             * 将 之前相同key 的值 重新放回去
             */
            map.put(key, mapValue);
        } else {
            // 拿到鍵值對,計算出在容量中的相對長度,然後加上
            size += safeSizeOf(key, createdValue);
        }
    }

    // 如果上面 判斷出了 将要放入的值發生沖突
    if (mapValue != null) {
        /*
         * 剛才create的值被删除了,原來的 之前相同key 的值被重新添加回去了
         * 告訴 自定義 的 entryRemoved 方法
         */
        entryRemoved(false, key, createdValue, mapValue);
        return mapValue;
    } else {
        // 上面 進行了 size += 操作 是以這裡要重整長度
        trimToSize(maxSize);
        return createdValue;
    }
}      

   get()方法的思路就是:    1)先嘗試從map緩存中擷取value,即mapVaule = map.get(key);如果mapVaule != null,說明緩存中存在該對象,直接傳回即可;    2)如果mapVaule == null,說明緩存中不存在該對象,大多數情況下會直接傳回null;但是如果我們重寫了create()方法,在緩存沒有該資料的時候自己去建立一個,則會繼續往下走,中間可能會出現沖突,看注釋;    3)注意:在我們通過LinkedHashMap進行get(key)或put(key,value)時都會對連結清單進行調整,即将剛剛通路get或加入put的結點放入到連結清單尾部。

    (7)entryRemoved()     entryRemoved的源碼如下:     

/**
 * 1.當被回收或者删掉時調用。該方法當value被回收釋放存儲空間時被remove調用
* 或者替換條目值時put調用,預設實作什麼都沒做。
* 2.該方法沒用同步調用,如果其他線程通路緩存時,該方法也會執行。
* 3.evicted=true:如果該條目被删除空間 (表示 進行了trimToSize or remove)  evicted=false:put沖突後 或 get裡成功create後
* 導緻
* 4.newValue!=null,那麼則被put()或get()調用。
*/
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
}      

    可以發現entryRemoved方法是一個空方法,說明這個也是讓開發者自己根據需求去重寫的。entryRemoved()主要作用就是在結點資料value需要被删除或回收的時候,給開發者的回調。開發者就可以在這個方法裡面實作一些自己的邏輯: (1)可以進行資源的回收; (2)可以實作二級記憶體緩存,可以進一步提高性能,思路如下: 重寫LruCache的entryRemoved()函數,把删除掉的item,再次存入另外一個LinkedHashMap<String, SoftWeakReference<Bitmap>>中,這個資料結構當做二級緩存,每次獲得圖檔的時候,先判斷LruCache中是否緩存,沒有的話,再判斷這個二級緩存中是否有,如果都沒有再從sdcard上擷取。sdcard上也沒有的話,就從網絡伺服器上拉取。     entryRemoved()在LruCache中有四個地方進行了調用:put()、get()、trimToSize()、remove()中進行了調用。

     (8)LruCache的線程安全性         LruCache是線程安全的,因為在put、get、trimToSize、remove的方法中都加入synchronized進行同步控制。

 2.1.4 LruCache的使用

上面就是整個LruCache中比較核心的的原理和方法,對于LruCache的使用者來說,我們其實主要注意下面幾個點: (1)在構造LruCache時提供一個總的緩存大小; (2)重寫sizeOf方法,對存入map的資料大小進行自定義測量; (3)根據需要,決定是否要重寫entryRemoved()方法; (4)使用LruCache提供的put和get方法進行資料的緩存

小結:

  • LruCache 自身并沒有釋放記憶體,隻是 LinkedHashMap中将資料移除了,如果資料還在别的地方被引用了,還是有洩漏問題,還需要手動釋放記憶體;
  • 覆寫 

    entryRemoved

     方法能知道 LruCache 資料移除是是否發生了沖突(沖突是指在map.put()的時候,對應的key中是否存在原來的值),也可以去手動釋放資源;

2.2 磁盤緩存(檔案緩存)——DiskLruCache分析

    LruCache是一種記憶體緩存政策,但是當存在大量圖檔的時候,我們指定的緩存記憶體空間可能很快就會用完,這個時候,LruCache就會頻繁的進行trimToSize()操作,不斷的将最近最少使用的資料移除,當再次需要該資料時,又得從網絡上重新加載。為此,Google提供了一種磁盤緩存的解決方案——DiskLruCache( DiskLruCache并沒有內建到Android源碼中,在Android Doc的例子中有講解)。

    2.2.1 DiskLruCache實作原理

    我們可以先來直覺看一下,使用了DiskLruCache緩存政策的APP,緩存目錄中是什麼樣子,如下圖:     

Android緩存機制&amp;一個緩存架構推薦1、先推薦一個輕量級緩存架構——ACache(ASimpleCache)2、Android緩存機制

    可以看到,緩存目錄中有一堆檔案名很長的檔案,這些檔案就是我們緩存的一張張圖檔資料,在最後有一個檔案名journal的檔案,這個 journal檔案是DiskLruCache的一個日志檔案,即儲存着每張緩存圖檔的操作記錄,journal檔案正是實作DiskLruCache的核心。看到出現了journal檔案,基本可以說明這個APP使用了DiskLruCache緩存政策。     根據對LruCache的分析,要實作LRU,最重要的是要有 一種資料結構能夠基于 通路順序 來儲存緩存中的對象,LinkedHashMap是一種非常合适的資料結構,為此,DiskLruCache也選擇了LinkedHashMap作為維護通路順序的資料結構,但是,對于DiskLruCache來說,單單LinkedHashMap是不夠的,因為我們不能像LruCache一樣,直接将資料放置到LinkedHashMap的value中,也就是處于記憶體當中,在DiskLruCache中,資料是緩存到了本地檔案,這裡的LinkedHashMap中的value隻是儲存的是value的一些簡要資訊Entry,如唯一的檔案名稱、大小、是否可讀等資訊,如:

private final class Entry {
    private final String key;
    /** Lengths of this entry's files. */
    private final long[] lengths;
    /** True if this entry has ever been published */
    private boolean readable;
    /** The ongoing edit or null if this entry is not being edited. */
    private Editor currentEditor;
    /** The sequence number of the most recently committed edit to this entry. */
    private long sequenceNumber;
    private Entry(String key) {
        this.key = key;
        this.lengths = new long[valueCount];
    }
    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".
     */
    private void setLengths(String[] strings) throws IOException {
        if (strings.length != valueCount) {
            throw invalidLengths(strings);
        }

        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: " + Arrays.toString(strings));
    }

    public File getCleanFile(int i) {
        return new File(directory, key + "." + i);
    }

    public File getDirtyFile(int i) {
        return new File(directory, key + "." + i + ".tmp");
    }
}      

DiskLruCache中對于LinkedHashMap定義如下:

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

在LruCache中,由于資料是直接緩存中記憶體中,map中資料的建立是在使用LruCache緩存的過程中逐漸建立的,而對于DiskLruCache,由于資料是緩存在本地檔案,相當于是持久儲存下來的一個檔案,即使程式退出檔案還在,是以,map中資料的建立,除了在使用 DiskLruCache過程中建立外,map還應該包括之前已經存在的緩存檔案,是以,在擷取DiskLruCache的執行個體時,DiskLruCache會去讀取journal這個日志檔案,根據這個日志檔案中的資訊,建立map的初始資料,同時,會根據journal這個日志檔案,維護本地的緩存檔案。構造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");
    }

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

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

其中, cache.readJournal();     cache.processJournal(); 正是去讀取journal日志檔案,建立起map中的初始資料,同時維護緩存檔案。

那journal日志檔案到底儲存了什麼資訊呢,一個标準的journal日志檔案資訊如下:

libcore.io.DiskLruCache    //第一行,固定内容,聲明 1                                        //第二行,cache的版本号,恒為1 1                                        //第三行,APP的版本号 2                                        //第四行,一個key,可以存放多少條資料valueCount                                                //第五行,空行分割行 DIRTY 335c4c6028171cfddfbaae1a9c313c52 CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 REMOVE 335c4c6028171cfddfbaae1a9c313c52 DIRTY 1ab96a171faeeee38496d8b330771a7a CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 READ 335c4c6028171cfddfbaae1a9c313c52 READ 3400330d1dfc7f3f7f4b8d4d803dfcf6

前五行稱為journal日志檔案的頭,下面部分的每一行會以四種字首之一開始: DIRTY、CLEAN、REMOVE、READ。

以一個DIRTY字首開始的,後面緊跟着緩存圖檔的key。以DIRTY這個這個字首開頭,意味着這是一條髒資料。每當我們調用一次DiskLruCache的edit()方法時,都會向journal檔案中寫入一條DIRTY記錄,表示我們正準備寫入一條緩存資料,但不知結果如何。然後調用commit()方法表示寫入緩存成功,這時會向journal中寫入一條CLEAN記錄,意味着這條“髒”資料被“洗幹淨了”,調用abort()方法表示寫入緩存失敗,這時會向journal中寫入一條REMOVE記錄。也就是說,每一行DIRTY的key,後面都應該有一行對應的CLEAN或者REMOVE的記錄,否則這條資料就是“髒”的,會被自動删除掉。

在CLEAN字首和key後面還有一個數值,代表的是該條緩存資料的大小。

是以,我們可以總結DiskLruCache中的工作流程:

1)初始化:通過open()方法,擷取DiskLruCache的執行個體,在open方法中通過readJournal(); 方法讀取journal日志檔案,根據journal日志檔案資訊建立map中的初始資料;然後再調用processJournal();方法對剛剛建立起的map資料進行分析,分析的工作,一個是計算目前有效緩存檔案(即被CLEAN的)的大小,一個是清理無用緩存檔案;

2)資料緩存與擷取緩存:上面的初始化工作完成後,我們就可以在程式中進行資料的緩存功能和擷取緩存的功能了;

緩存資料的操作是借助DiskLruCache.Editor這個類完成的,這個類也是不能new的,需要調用DiskLruCache的edit()方法來擷取執行個體,如下所示: 

  public Editor edit(String key) throws IOException

在寫入完成後,需要進行commit()。如下一個簡單示例:

new Thread(new Runnable() {  
    @Override  
    public void run() {  
        try {  
            String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
            String key = hashKeyForDisk(imageUrl);  //MD5對url進行加密,這個主要是為了獲得統一的16位字元
            DiskLruCache.Editor editor = mDiskLruCache.edit(key);  //拿到Editor,往journal日志中寫入DIRTY記錄
            if (editor != null) {  
                OutputStream outputStream = editor.newOutputStream(0);  
                if (downloadUrlToStream(imageUrl, outputStream)) {  //downloadUrlToStream方法為下載下傳圖檔的方法,并且将輸出流放到outputStream
                    editor.commit();  //完成後記得commit(),成功後,再往journal日志中寫入CLEAN記錄
                } else {  
                    editor.abort();  //失敗後,要remove緩存檔案,往journal檔案中寫入REMOVE記錄
                }  
            }  
            mDiskLruCache.flush();  //将緩存操作同步到journal日志檔案,不一定要在這裡就調用
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
}).start(); 
           

注意每次調用edit()時,會向journal日志檔案寫入DIRTY為字首的一條記錄;檔案儲存成功後,調用commit()時,也會向journal日志中寫入一條CLEAN為字首的一條記錄,如果失敗,需要調用abort(),abort()裡面會向journal檔案寫入一條REMOVE為字首的記錄。

擷取緩存資料是通過get()方法實作的,如下一個簡單示例:

try {  
    String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
    String key = hashKeyForDisk(imageUrl);  //MD5對url進行加密,這個主要是為了獲得統一的16位字元
     //通過get拿到value的Snapshot,裡面封裝了輸入流、key等資訊,調用get會向journal檔案寫入READ為字首的記錄
    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();  
} 
           

3)合适的地方進行flush() 在上面進行資料緩存或擷取緩存的時候,調用不同的方法會往journal中寫入不同字首的一行記錄,記錄寫入是通過IO下的Writer寫入的,要真正生效,還需要調用writer的flush()方法,而DiskLruCache中的flush()方法中封裝了writer.flush()的操作,是以,我們隻需要在合适地方調用DiskLruCache中的flush()方法即可。其作用也就是将操作記錄同步到journal檔案中,這是一個消耗效率的IO操作,我們不用每次一往journal中寫資料後就調用flush,這樣對效率影響較大,可以在Activity的onPause()中調用一下即可。

小結&注意: (1)我們可以在在UI線程中檢測記憶體緩存,即主線程中可以直接使用LruCache; (2)使用DiskLruCache時,由于緩存或擷取都需要對本地檔案進行操作,是以需要另開一個線程,在子線程中檢測磁盤緩存、儲存緩存資料,磁盤操作從來不應該在UI線程中實作; (3)LruCache記憶體緩存的核心是LinkedHashMap,而DiskLruCache的核心是LinkedHashMap和journal日志檔案,相當于把journal看作是一塊“記憶體”,LinkedHashMap的value隻儲存檔案的簡要資訊,對緩存檔案的所有操作都會記錄在journal日志檔案中。

DiskLruCache可能的優化方案:     DiskLruCache是基于日志檔案journal的,這就決定了每次對緩存檔案的操作都需要進行日志檔案的記錄,我們可以不用journal檔案,在第一次構造DiskLruCache的時候, 直接從程式通路緩存目錄下的緩存檔案,并将每個緩存檔案的通路時間作為初始值記錄在map的value中,每次通路或儲存緩存都更新相應key對應的緩存檔案的通路時間,這樣就避免了頻繁的IO操作,這種情況下就需要使用單例模式對DiskLruCache進行構造了,上面的Acache輕量級的資料緩存類就是這種實作方式。

2.3 二級緩存

    LruCache記憶體緩存在解決資料量不是很大的情況下效果不錯,當資料很大時,比圖需要加載大量圖檔,LruCache指定的緩存容量可能很快被耗盡,此時LruCache頻繁的替換移除淘汰檔案,又頻繁要進行網絡請求,很有可能出現OOM,為此,在大量資料的情況下,我們可以将磁盤緩存DiskLruCache作為一個二級緩存的模式,優化緩存方案。     流程就是,     (1)當我們需要緩存資料的時候,既在記憶體中緩存,也将檔案緩存到磁盤;     (2)當擷取緩存檔案時,先嘗試從記憶體緩存中擷取,如果存在,則直接傳回該檔案;如果不存在,則從磁盤緩存中擷取,如果磁盤緩存中還沒有,那就隻能從網絡擷取,擷取到資料後,同時在記憶體和磁盤中進行緩存。

下一篇準備根據上面的内容寫一個輕量級的資料緩存架構,架構将以LruCache和DiskLruCache結合的政策進行設計,盡請期待。

參考文章: http://www.binkery.com/archives/561.html http://www.jianshu.com/p/bdbfdfd0641b http://blog.csdn.net/guolin_blog/article/details/28863651 LruCache源碼解析

繼續閱讀