Andoird LruCache和DiskLruCache緩存詳解

參考:http://www.2cto.com/kf/201606/517802.html

            http://blog.csdn.net/guolin_blog/article/details/28863651

            http://blog.csdn.net/zxw136511485/article/details/52196400

            http://www.cnblogs.com/whoislcj/p/5547758.html


DiskLruCache GitHub地址:https://github.com/JakeWharton/DiskLruCache/



前言:先說一個Bitmap和Drawable佔用內存的比較:

Drawable、Bitmap佔用內存探討

之前一直使用過Afinal 和Xutils 熟悉這兩框架的都知道,兩者出自同一人,Xutils是Afina的升級版,AFinal中的圖片內存緩存使用的是Bitmap 而後來爲何Xutils將內存緩存的對象改成了Drawable了呢?我們一探究竟

寫個測試程序:

複製代碼
        List<Bitmap> bitmaps = new ArrayList<>();
        start = System.currentTimeMillis();
        for (int i = 0; i < testMaxCount; i++) {
            Bitmap bitmap = BitmapUtils.readBitMap(this, R.mipmap.ic_app_center_banner);
            bitmaps.add(bitmap);
            Log.e(TAG, "BitmapFactory Bitmap--num-->" + i);
        }
        end = System.currentTimeMillis();
        Log.e(TAG, "BitmapFactory Bitmap--time-->" + (end - start));

        List<Drawable> drawables = new ArrayList<>();
        
        start = System.currentTimeMillis();
        for (int i = 0; i < testMaxCount; i++) {
            Drawable drawable = getResources().getDrawable(R.mipmap.ic_app_center_banner);
            drawables.add(drawable);
            Log.e(TAG, "BitmapFactory Drawable--num-->" + i);
        }
        end = System.currentTimeMillis();
        Log.e(TAG, "BitmapFactory Drawable--time-->" + (end - start));
複製代碼

測試數據1000 同一張圖片

 Bitmap 直接70條數據的時候掛掉

Drawable 輕鬆1000條數據通過

從測試說明Drawable 相對Bitmap有很大的內存佔用優勢。這也是爲啥現在主流的圖片緩存框架內存緩存那一層採用Drawable作爲緩存對象的原因。


開始說正題:

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  

2、Android緩存機制

Android緩存分爲內存緩存和文件緩存(磁盤緩存)。在早期,各大圖片緩存框架流行之前,常用的內存緩存方式是軟引用(SoftReference)和弱引用(WeakReference),如大部分的使用方式:HashMap> 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的一個構造方法如下:
1
2
3
4
5
6
7
8
9
10
11
/*
     * 初始化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中提供了下面三個關鍵字段:
1
2
3
4
5
6
//核心數據結構
    private final LinkedHashMap<k, v=""> map;
    // 當前緩存數據所佔的大小
    private int size;
    //緩存空間總容量
    private int maxSize;</k,>
要注意的是size字段,因爲map中可以存放各種類型的數據,這些數據的大小測量方式也是不一樣的,比如Bitmap類型的數據和String類型的數據計算他們的大小方式肯定不同,因此,LruCache中在計算放入數據大小的方法sizeOf中,只是簡單的返回了1,需要我們重寫這個方法,自己去定義數據的測量方式。因此,我們在使用LruCache的時候,經常會看到這種方式:
1
2
3
4
5
6
7
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數據大小的計算方式
        }
    };</string,bitmap></string,bitmap>
(2)構造方法
1
2
3
4
5
6
7
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);
}</k,>
LruCache只有一個唯一的構造方法,在構造方法中,給定了緩存空間的總大小,初始化了LinkedHashMap核心數據結構,在LinkedHashMap中的第三個參數指定爲true,也就設置了accessOrder=true,表示這個LinkedHashMap將是基於數據的訪問順序進行排序。 (3)sizeOf()和safeSizeOf()方法根據上面的解釋,由於各種數據類型大小測量的標準不統一,具體測量的方法應該由使用者來實現,如上面給出的一個在實現LruCache時重寫sizeOf的一種常用實現方式。通過多態的性質,再具體調用sizeOf時會調用我們重寫的方法進行測量,LruCache對sizeOf()的調用進行一層封裝,如下:
1
2
3
4
5
6
7
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方法緩存數據首先看一下它的源碼實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<em>/**
</em><em>   * 給對應key緩存value,並且將該value移動到鏈表的尾部。
</em><em>   */
</em>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()來對緩存空間進行管理控制。如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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;
            }
<em>            /**
</em><em>             * 執行到這,表示當前緩存數據已超過了總容量,需要執行LRU,即將最近最少使用的數據清除掉,直到數據所佔緩存空間沒有超標;
</em><em>             * 根據前面的原理分析,知道,在鏈表中,鏈表的頭結點是最近最少使用的數據,因此,最先清除掉鏈表前面的結點
</em><em>             */
</em>            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);
    }
}</k,>
trimToSize()方法的作用就是爲了保證當前數據的緩存大小不能超過我們指定的緩存總大小,如果超過了,就會開始移除最近最少使用的數據,直到size符合要求。trimToSize()方法在put()的時候一定會調用,在get()的時候有可能會調用。 (6)get方法獲取緩存數據get方法源碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<em>/**
</em><em> * </em><em>根據</em><em>key</em><em>查詢緩存,如果該</em><em>key</em><em>對應的</em><em>value</em><em>存在於緩存,直接返回</em><em>value</em><em>;
</em><em>* </em><em>訪問到這個結點時,</em><em>LinkHashMap</em><em>會將它移動到雙向循環鏈表的的尾部。
</em><em>* </em><em>如果如果沒有緩存的值,則返回</em><em>null</em><em>。(如果開發者重寫了</em><em>create()</em><em>的話,返回創建的</em><em>value</em><em>)
</em><em>*/
</em>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;
    }
 
<em>    /***************************
</em><em>     * </em><em>不覆寫</em><em>create</em><em>方法走不到下面 </em><em>*
</em><em>     ***************************/</em>
    /*
     * 正常情況走不到這裏
     * 走到這裏的話 說明 實現了自定義的 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
2
3
4
5
6
7
8
9
10
<em>/**
</em><em> * 1.</em><em>當被回收或者刪掉時調用。該方法當</em><em>value</em><em>被回收釋放存儲空間時被</em><em>remove</em><em>調用
</em><em>* </em><em>或者替換條目值時</em><em>put</em><em>調用,默認實現什麼都沒做。
</em><em>* 2.</em><em>該方法沒用同步調用,如果其他線程訪問緩存時,該方法也會執行。
</em><em>* 3.evicted=true</em><em>:如果該條目被刪除空間 (表示 進行了</em><em>trimToSize or remove</em><em>)  </em><em>evicted=false</em><em>:</em><em>put</em><em>衝突後 或 </em><em>get</em><em>裏成功</em><em>create</em><em>後
</em><em>* </em><em>導致
</em><em>* 4.newValue!=null</em><em>,那麼則被</em><em>put()</em><em>或</em><em>get()</em><em>調用。
</em><em>*/
</em>protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
}
可以發現entryRemoved方法是一個空方法,說明這個也是讓開發者自己根據需求去重寫的。entryRemoved()主要作用就是在結點數據value需要被刪除或回收的時候,給開發者的回調。開發者就可以在這個方法裏面實現一些自己的邏輯:(1)可以進行資源的回收;(2)可以實現二級內存緩存,可以進一步提高性能,思路如下:重寫LruCache的entryRemoved()函數,把刪除掉的item,再次存入另外一個LinkedHashMap>中,這個數據結構當做二級緩存,每次獲得圖片的時候,先判斷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,緩存目錄中是什麼樣子,如下圖:\可以看到,緩存目錄中有一堆文件名很長的文件,這些文件就是我們緩存的一張張圖片數據,在最後有一個文件名journal的文件,這個journal文件是DiskLruCache的一個日誌文件,即保存着每張緩存圖片的操作記錄,journal文件正是實現DiskLruCache的核心。看到出現了journal文件,基本可以說明這個APP使用了DiskLruCache緩存策略。根據對LruCache的分析,要實現LRU,最重要的是要有一種數據結構能夠基於訪問順序來保存緩存中的對象,LinkedHashMap是一種非常合適的數據結構,爲此,DiskLruCache也選擇了LinkedHashMap作爲維護訪問順序的數據結構,但是,對於DiskLruCache來說,單單LinkedHashMap是不夠的,因爲我們不能像LruCache一樣,直接將數據放置到LinkedHashMap的value中,也就是處於內存當中,在DiskLruCache中,數據是緩存到了本地文件,這裏的LinkedHashMap中的value只是保存的是value的一些簡要信息Entry,如唯一的文件名稱、大小、是否可讀等信息,如:
1
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
private final class Entry {
private final String key;
<em>/** Lengths of this entry's files. */</em>
private final long[] lengths;
<em>/** True if this entry has ever been published */</em>
private boolean readable;
<em>/** The ongoing edit or null if this entry is not being edited. */</em>
private Editor currentEditor;
<em>/** The sequence number of the most recently committed edit to this entry. */</em>
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();
}
 
<em>/**</em>
<em>* Set lengths using decimal numbers like "10123".</em>
<em>*/</em>
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.<em>parseLong</em>(strings[i]);
}
} catch (NumberFormatException e) {
throw invalidLengths(strings);
}
}
 
private IOException invalidLengths(String[] strings) throws IOException {
throw new IOException("unexpected journal line: " + Arrays.<em>toString</em>(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定義如下:
1
2
private final LinkedHashMap<string, entry=""> lruEntries
    = new LinkedHashMap<string, entry="">(0, 0.75f, true);</string,></string,>
在LruCache中,由於數據是直接緩存中內存中,map中數據的建立是在使用LruCache緩存的過程中逐步建立的,而對於DiskLruCache,由於數據是緩存在本地文件,相當於是持久保存下來的一個文件,即使程序退出文件還在,因此,map中數據的建立,除了在使用DiskLruCache過程中建立外,map還應該包括之前已經存在的緩存文件,因此,在獲取DiskLruCache的實例時,DiskLruCache會去讀取journal這個日誌文件,根據這個日誌文件中的信息,建立map的初始數據,同時,會根據journal這個日誌文件,維護本地的緩存文件。構造DiskLruCache的方法如下:
1
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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),<em>IO_BUFFER_SIZE</em>);
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的版本號,恆爲11 //第三行,APP的版本號2 //第四行,一個key,可以存放多少條數據valueCount//第五行,空行分割行DIRTY335c4c6028171cfddfbaae1a9c313c52CLEAN335c4c6028171cfddfbaae1a9c313c523934REMOVE335c4c6028171cfddfbaae1a9c313c52DIRTY1ab96a171faeeee38496d8b330771a7aCLEAN1ab96a171faeeee38496d8b330771a7a1600234READ335c4c6028171cfddfbaae1a9c313c52READ3400330d1dfc7f3f7f4b8d4d803dfcf6 前五行稱爲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()方法來獲取實例,如下所示:

publicEditoredit(Stringkey)throwsIOException

 

在寫入完成後,需要進行commit()。如下一個簡單示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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()方法實現的,如下一個簡單示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
try
    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)當獲取緩存文件時,先嚐試從內存緩存中獲取,如果存在,則直接返回該文件;如果不存在,則從磁盤緩存中獲取,如果磁盤緩存中還沒有,那就只能從網絡獲取,獲取到數據後,同時在內存和磁盤中進行緩存。



轉載請註明出處:http://blog.csdn.net/guolin_blog/article/details/28863651


概述


記得在很早之前,我有寫過一篇文章Android高效加載大圖、多圖解決方案,有效避免程序OOM,這篇文章是翻譯自Android Doc的,其中防止多圖OOM的核心解決思路就是使用LruCache技術。但LruCache只是管理了內存中圖片的存儲與釋放,如果圖片從內存中被移除的話,那麼又需要從網絡上重新加載一次圖片,這顯然非常耗時。對此,Google又提供了一套硬盤緩存的解決方案:DiskLruCache(非Google官方編寫,但獲得官方認證)。只可惜,Android Doc中並沒有對DiskLruCache的用法給出詳細的說明,而網上關於DiskLruCache的資料也少之又少,因此今天我準備專門寫一篇博客來詳細講解DiskLruCache的用法,以及分析它的工作原理,這應該也是目前網上關於DiskLruCache最詳細的資料了。


那麼我們先來看一下有哪些應用程序已經使用了DiskLruCache技術。在我所接觸的應用範圍裏,Dropbox、Twitter、網易新聞等都是使用DiskLruCache來進行硬盤緩存的,其中Dropbox和Twitter大多數人應該都沒用過,那麼我們就從大家最熟悉的網易新聞開始着手分析,來對DiskLruCache有一個最初的認識吧。


初探


相信所有人都知道,網易新聞中的數據都是從網絡上獲取的,包括了很多的新聞內容和新聞圖片,如下圖所示:




但是不知道大家有沒有發現,這些內容和圖片在從網絡上獲取到之後都會存入到本地緩存中,因此即使手機在沒有網絡的情況下依然能夠加載出以前瀏覽過的新聞。而使用的緩存技術不用多說,自然是DiskLruCache了,那麼首先第一個問題,這些數據都被緩存在了手機的什麼位置呢?


其實DiskLruCache並沒有限制數據的緩存位置,可以自由地進行設定,但是通常情況下多數應用程序都會將緩存的位置選擇爲 /sdcard/Android/data/<application package>/cache 這個路徑。選擇在這個位置有兩點好處:第一,這是存儲在SD卡上的,因此即使緩存再多的數據也不會對手機的內置存儲空間有任何影響,只要SD卡空間足夠就行。第二,這個路徑被Android系統認定爲應用程序的緩存路徑,當程序被卸載的時候,這裏的數據也會一起被清除掉,這樣就不會出現刪除程序之後手機上還有很多殘留數據的問題。


那麼這裏還是以網易新聞爲例,它的客戶端的包名是com.netease.newsreader.activity,因此數據緩存地址就應該是 /sdcard/Android/data/com.netease.newsreader.activity/cache ,我們進入到這個目錄中看一下,結果如下圖所示:




可以看到有很多個文件夾,因爲網易新聞對多種類型的數據都進行了緩存,這裏簡單起見我們只分析圖片緩存就好,所以進入到bitmap文件夾當中。然後你將會看到一堆文件名很長的文件,這些文件命名沒有任何規則,完全看不懂是什麼意思,但如果你一直向下滾動,將會看到一個名爲journal的文件,如下圖所示:




那麼這些文件到底都是什麼呢?看到這裏相信有些朋友已經是一頭霧水了,這裏我簡單解釋一下。上面那些文件名很長的文件就是一張張緩存的圖片,每個文件都對應着一張圖片,而journal文件是DiskLruCache的一個日誌文件,程序對每張圖片的操作記錄都存放在這個文件中,基本上看到journal這個文件就標誌着該程序使用DiskLruCache技術了。


下載


好了,對DiskLruCache有了最初的認識之後,下面我們來學習一下DiskLruCache的用法吧。由於DiskLruCache並不是由Google官方編寫的,所以這個類並沒有被包含在Android API當中,我們需要將這個類從網上下載下來,然後手動添加到項目當中。DiskLruCache的源碼在Google Source上,地址如下:

android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java


如果Google Source打不開的話,也可以點擊這裏下載DiskLruCache的源碼。下載好了源碼之後,只需要在項目中新建一個libcore.io包,然後將DiskLruCache.Java文件複製到這個包中即可。


打開緩存


這樣的話我們就把準備工作做好了,下面看一下DiskLruCache到底該如何使用。首先你要知道,DiskLruCache是不能new出實例的,如果我們要創建一個DiskLruCache的實例,則需要調用它的open()方法,接口如下所示:

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

open()方法接收四個參數,第一個參數指定的是數據的緩存地址,第二個參數指定當前應用程序的版本號,第三個參數指定同一個key可以對應多少個緩存文件,基本都是傳1,第四個參數指定最多可以緩存多少字節的數據。


其中緩存地址前面已經說過了,通常都會存放在 /sdcard/Android/data/<application package>/cache 這個路徑下面,但同時我們又需要考慮如果這個手機沒有SD卡,或者SD正好被移除了的情況,因此比較優秀的程序都會專門寫一個方法來獲取緩存地址,如下所示:

  1. public File getDiskCacheDir(Context context, String uniqueName) {  
  2.     String cachePath;  
  3.     if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())  
  4.             || !Environment.isExternalStorageRemovable()) {  
  5.         cachePath = context.getExternalCacheDir().getPath();  
  6.     } else {  
  7.         cachePath = context.getCacheDir().getPath();  
  8.     }  
  9.     return new File(cachePath + File.separator + uniqueName);  
  10. }  

可以看到,當SD卡存在或者SD卡不可被移除的時候,就調用getExternalCacheDir()方法來獲取緩存路徑,否則就調用getCacheDir()方法來獲取緩存路徑。前者獲取到的就是 /sdcard/Android/data/<application package>/cache 這個路徑,而後者獲取到的是 /data/data/<application package>/cache 這個路徑。


接着又將獲取到的路徑和一個uniqueName進行拼接,作爲最終的緩存路徑返回。那麼這個uniqueName又是什麼呢?其實這就是爲了對不同類型的數據進行區分而設定的一個唯一值,比如說在網易新聞緩存路徑下看到的bitmap、object等文件夾。


接着是應用程序版本號,我們可以使用如下代碼簡單地獲取到當前應用程序的版本號:

  1. public int getAppVersion(Context context) {  
  2.     try {  
  3.         PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);  
  4.         return info.versionCode;  
  5.     } catch (NameNotFoundException e) {  
  6.         e.printStackTrace();  
  7.     }  
  8.     return 1;  
  9. }  
需要注意的是,每當版本號改變,緩存路徑下存儲的所有數據都會被清除掉,因爲DiskLruCache認爲當應用程序有版本更新的時候,所有的數據都應該從網上重新獲取。


後面兩個參數就沒什麼需要解釋的了,第三個參數傳1,第四個參數通常傳入10M的大小就夠了,這個可以根據自身的情況進行調節。


因此,一個非常標準的open()方法就可以這樣寫:

  1. DiskLruCache mDiskLruCache = null;  
  2. try {  
  3.     File cacheDir = getDiskCacheDir(context, "bitmap");  
  4.     if (!cacheDir.exists()) {  
  5.         cacheDir.mkdirs();  
  6.     }  
  7.     mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 110 * 1024 * 1024);  
  8. catch (IOException e) {  
  9.     e.printStackTrace();  
  10. }  

首先調用getDiskCacheDir()方法獲取到緩存地址的路徑,然後判斷一下該路徑是否存在,如果不存在就創建一下。接着調用DiskLruCache的open()方法來創建實例,並把四個參數傳入即可。


有了DiskLruCache的實例之後,我們就可以對緩存的數據進行操作了,操作類型主要包括寫入、訪問、移除等,我們一個個進行學習。


寫入緩存


先來看寫入,比如說現在有一張圖片,地址是https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg,那麼爲了將這張圖片下載下來,就可以這樣寫:

  1. private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {  
  2.     HttpURLConnection urlConnection = null;  
  3.     BufferedOutputStream out = null;  
  4.     BufferedInputStream in = null;  
  5.     try {  
  6.         final URL url = new URL(urlString);  
  7.         urlConnection = (HttpURLConnection) url.openConnection();  
  8.         in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);  
  9.         out = new BufferedOutputStream(outputStream, 8 * 1024);  
  10.         int b;  
  11.         while ((b = in.read()) != -1) {  
  12.             out.write(b);  
  13.         }  
  14.         return true;  
  15.     } catch (final IOException e) {  
  16.         e.printStackTrace();  
  17.     } finally {  
  18.         if (urlConnection != null) {  
  19.             urlConnection.disconnect();  
  20.         }  
  21.         try {  
  22.             if (out != null) {  
  23.                 out.close();  
  24.             }  
  25.             if (in != null) {  
  26.                 in.close();  
  27.             }  
  28.         } catch (final IOException e) {  
  29.             e.printStackTrace();  
  30.         }  
  31.     }  
  32.     return false;  
  33. }  
這段代碼相當基礎,相信大家都看得懂,就是訪問urlString中傳入的網址,並通過outputStream寫入到本地。有了這個方法之後,下面我們就可以使用DiskLruCache來進行寫入了,寫入的操作是藉助DiskLruCache.Editor這個類完成的。類似地,這個類也是不能new的,需要調用DiskLruCache的edit()方法來獲取實例,接口如下所示:
  1. public Editor edit(String key) throws IOException  

可以看到,edit()方法接收一個參數key,這個key將會成爲緩存文件的文件名,並且必須要和圖片的URL是一一對應的。那麼怎樣才能讓key和圖片的URL能夠一一對應呢?直接使用URL來作爲key?不太合適,因爲圖片URL中可能包含一些特殊字符,這些字符有可能在命名文件時是不合法的。其實最簡單的做法就是將圖片的URL進行MD5編碼,編碼後的字符串肯定是唯一的,並且只會包含0-F這樣的字符,完全符合文件的命名規則。


那麼我們就寫一個方法用來將字符串進行MD5編碼,代碼如下所示:

  1. public String hashKeyForDisk(String key) {  
  2.     String cacheKey;  
  3.     try {  
  4.         final MessageDigest mDigest = MessageDigest.getInstance("MD5");  
  5.         mDigest.update(key.getBytes());  
  6.         cacheKey = bytesToHexString(mDigest.digest());  
  7.     } catch (NoSuchAlgorithmException e) {  
  8.         cacheKey = String.valueOf(key.hashCode());  
  9.     }  
  10.     return cacheKey;  
  11. }  
  12.   
  13. private String bytesToHexString(byte[] bytes) {  
  14.     StringBuilder sb = new StringBuilder();  
  15.     for (int i = 0; i < bytes.length; i++) {  
  16.         String hex = Integer.toHexString(0xFF & bytes[i]);  
  17.         if (hex.length() == 1) {  
  18.             sb.append('0');  
  19.         }  
  20.         sb.append(hex);  
  21.     }  
  22.     return sb.toString();  
  23. }  
代碼很簡單,現在我們只需要調用一下hashKeyForDisk()方法,並把圖片的URL傳入到這個方法中,就可以得到對應的key了。


因此,現在就可以這樣寫來得到一個DiskLruCache.Editor的實例:

  1. String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
  2. String key = hashKeyForDisk(imageUrl);  
  3. DiskLruCache.Editor editor = mDiskLruCache.edit(key);  
有了DiskLruCache.Editor的實例之後,我們可以調用它的newOutputStream()方法來創建一個輸出流,然後把它傳入到downloadUrlToStream()中就能實現下載並寫入緩存的功能了。注意newOutputStream()方法接收一個index參數,由於前面在設置valueCount的時候指定的是1,所以這裏index傳0就可以了。在寫入操作執行完之後,我們還需要調用一下commit()方法進行提交才能使寫入生效,調用abort()方法的話則表示放棄此次寫入。


因此,一次完整寫入操作的代碼如下所示:

  1. new Thread(new Runnable() {  
  2.     @Override  
  3.     public void run() {  
  4.         try {  
  5.             String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
  6.             String key = hashKeyForDisk(imageUrl);  
  7.             DiskLruCache.Editor editor = mDiskLruCache.edit(key);  
  8.             if (editor != null) {  
  9.                 OutputStream outputStream = editor.newOutputStream(0);  
  10.                 if (downloadUrlToStream(imageUrl, outputStream)) {  
  11.                     editor.commit();  
  12.                 } else {  
  13.                     editor.abort();  
  14.                 }  
  15.             }  
  16.             mDiskLruCache.flush();  
  17.         } catch (IOException e) {  
  18.             e.printStackTrace();  
  19.         }  
  20.     }  
  21. }).start();  
由於這裏調用了downloadUrlToStream()方法來從網絡上下載圖片,所以一定要確保這段代碼是在子線程當中執行的。注意在代碼的最後我還調用了一下flush()方法,這個方法並不是每次寫入都必須要調用的,但在這裏卻不可缺少,我會在後面說明它的作用。


現在的話緩存應該是已經成功寫入了,我們進入到SD卡上的緩存目錄裏看一下,如下圖所示:




可以看到,這裏有一個文件名很長的文件,和一個journal文件,那個文件名很長的文件自然就是緩存的圖片了,因爲是使用了MD5編碼來進行命名的。


讀取緩存


緩存已經寫入成功之後,接下來我們就該學習一下如何讀取了。讀取的方法要比寫入簡單一些,主要是藉助DiskLruCache的get()方法實現的,接口如下所示:

  1. public synchronized Snapshot get(String key) throws IOException  
很明顯,get()方法要求傳入一個key來獲取到相應的緩存數據,而這個key毫無疑問就是將圖片URL進行MD5編碼後的值了,因此讀取緩存數據的代碼就可以這樣寫:
  1. String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
  2. String key = hashKeyForDisk(imageUrl);  
  3. DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);  
很奇怪的是,這裏獲取到的是一個DiskLruCache.Snapshot對象,這個對象我們該怎麼利用呢?很簡單,只需要調用它的getInputStream()方法就可以得到緩存文件的輸入流了。同樣地,getInputStream()方法也需要傳一個index參數,這裏傳入0就好。有了文件的輸入流之後,想要把緩存圖片顯示到界面上就輕而易舉了。所以,一段完整的讀取緩存,並將圖片加載到界面上的代碼如下所示:
  1. try {  
  2.     String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
  3.     String key = hashKeyForDisk(imageUrl);  
  4.     DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);  
  5.     if (snapShot != null) {  
  6.         InputStream is = snapShot.getInputStream(0);  
  7.         Bitmap bitmap = BitmapFactory.decodeStream(is);  
  8.         mImage.setImageBitmap(bitmap);  
  9.     }  
  10. catch (IOException e) {  
  11.     e.printStackTrace();  
  12. }  

我們使用了BitmapFactory的decodeStream()方法將文件流解析成Bitmap對象,然後把它設置到ImageView當中。如果運行一下程序,將會看到如下效果:




OK,圖片已經成功顯示出來了。注意這是我們從本地緩存中加載的,而不是從網絡上加載的,因此即使在你手機沒有聯網的情況下,這張圖片仍然可以顯示出來。


移除緩存


學習完了寫入緩存和讀取緩存的方法之後,最難的兩個操作你就都已經掌握了,那麼接下來要學習的移除緩存對你來說也一定非常輕鬆了。移除緩存主要是藉助DiskLruCache的remove()方法實現的,接口如下所示:

  1. public synchronized boolean remove(String key) throws IOException  
相信你已經相當熟悉了,remove()方法中要求傳入一個key,然後會刪除這個key對應的緩存圖片,示例代碼如下:
  1. try {  
  2.     String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";    
  3.     String key = hashKeyForDisk(imageUrl);    
  4.     mDiskLruCache.remove(key);  
  5. catch (IOException e) {  
  6.     e.printStackTrace();  
  7. }  

用法雖然簡單,但是你要知道,這個方法我們並不應該經常去調用它。因爲你完全不需要擔心緩存的數據過多從而佔用SD卡太多空間的問題,DiskLruCache會根據我們在調用open()方法時設定的緩存最大值來自動刪除多餘的緩存。只有你確定某個key對應的緩存內容已經過期,需要從網絡獲取最新數據的時候才應該調用remove()方法來移除緩存。


其它API


除了寫入緩存、讀取緩存、移除緩存之外,DiskLruCache還提供了另外一些比較常用的API,我們簡單學習一下。


1. size()

這個方法會返回當前緩存路徑下所有緩存數據的總字節數,以byte爲單位,如果應用程序中需要在界面上顯示當前緩存數據的總大小,就可以通過調用這個方法計算出來。比如網易新聞中就有這樣一個功能,如下圖所示:




2.flush()

這個方法用於將內存中的操作記錄同步到日誌文件(也就是journal文件)當中。這個方法非常重要,因爲DiskLruCache能夠正常工作的前提就是要依賴於journal文件中的內容。前面在講解寫入緩存操作的時候我有調用過一次這個方法,但其實並不是每次寫入緩存都要調用一次flush()方法的,頻繁地調用並不會帶來任何好處,只會額外增加同步journal文件的時間。比較標準的做法就是在Activity的onPause()方法中去調用一次flush()方法就可以了。


3.close()

這個方法用於將DiskLruCache關閉掉,是和open()方法對應的一個方法。關閉掉了之後就不能再調用DiskLruCache中任何操作緩存數據的方法,通常只應該在Activity的onDestroy()方法中去調用close()方法。


4.delete()

這個方法用於將所有的緩存數據全部刪除,比如說網易新聞中的那個手動清理緩存功能,其實只需要調用一下DiskLruCache的delete()方法就可以實現了。


解讀journal


前面已經提到過,DiskLruCache能夠正常工作的前提就是要依賴於journal文件中的內容,因此,能夠讀懂journal文件對於我們理解DiskLruCache的工作原理有着非常重要的作用。那麼journal文件中的內容到底是什麼樣的呢?我們來打開瞧一瞧吧,如下圖所示:





由於現在只緩存了一張圖片,所以journal中並沒有幾行日誌,我們一行行進行分析。第一行是個固定的字符串“libcore.io.DiskLruCache”,標誌着我們使用的是DiskLruCache技術。第二行是DiskLruCache的版本號,這個值是恆爲1的。第三行是應用程序的版本號,我們在open()方法裏傳入的版本號是什麼這裏就會顯示什麼。第四行是valueCount,這個值也是在open()方法中傳入的,通常情況下都爲1。第五行是一個空行。前五行也被稱爲journal文件的頭,這部分內容還是比較好理解的,但是接下來的部分就要稍微動點腦筋了。


第六行是以一個DIRTY前綴開始的,後面緊跟着緩存圖片的key。通常我們看到DIRTY這個字樣都不代表着什麼好事情,意味着這是一條髒數據。沒錯,每當我們調用一次DiskLruCache的edit()方法時,都會向journal文件中寫入一條DIRTY記錄,表示我們正準備寫入一條緩存數據,但不知結果如何。然後調用commit()方法表示寫入緩存成功,這時會向journal中寫入一條CLEAN記錄,意味着這條“髒”數據被“洗乾淨了”,調用abort()方法表示寫入緩存失敗,這時會向journal中寫入一條REMOVE記錄。也就是說,每一行DIRTY的key,後面都應該有一行對應的CLEAN或者REMOVE的記錄,否則這條數據就是“髒”的,會被自動刪除掉。


如果你足夠細心的話應該還會注意到,第七行的那條記錄,除了CLEAN前綴和key之外,後面還有一個152313,這是什麼意思呢?其實,DiskLruCache會在每一行CLEAN記錄的最後加上該條緩存數據的大小,以字節爲單位。152313也就是我們緩存的那張圖片的字節數了,換算出來大概是148.74K,和緩存圖片剛剛好一樣大,如下圖所示:




前面我們所學的size()方法可以獲取到當前緩存路徑下所有緩存數據的總字節數,其實它的工作原理就是把journal文件中所有CLEAN記錄的字節數相加,求出的總合再把它返回而已。


除了DIRTY、CLEAN、REMOVE之外,還有一種前綴是READ的記錄,這個就非常簡單了,每當我們調用get()方法去讀取一條緩存數據時,就會向journal文件中寫入一條READ記錄。因此,像網易新聞這種圖片和數據量都非常大的程序,journal文件中就可能會有大量的READ記錄。


那麼你可能會擔心了,如果我不停頻繁操作的話,就會不斷地向journal文件中寫入數據,那這樣journal文件豈不是會越來越大?這倒不必擔心,DiskLruCache中使用了一個redundantOpCount變量來記錄用戶操作的次數,每執行一次寫入、讀取或移除緩存的操作,這個變量值都會加1,當變量值達到2000的時候就會觸發重構journal的事件,這時會自動把journal中一些多餘的、不必要的記錄全部清除掉,保證journal文件的大小始終保持在一個合理的範圍內。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章