Android快取機制
阿新 • • 發佈:2019-01-31
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中的物件是強引用的。
?
顯然,在LruCache中選擇的是accessOrder = true;此時,當accessOrder 設定為 true時,每當我們更新(即呼叫put方法)或訪問(即呼叫get方法)map中的結點時,LinkedHashMap內部都會將這個結點移動到連結串列的尾部,因此,在連結串列的尾部是最近剛剛使用的結點,在連結串列的頭部是是最近最少使用的結點,當我們的快取空間不足時,就應該持續把連結串列頭部結點移除掉,直到有剩餘空間放置新結點。
可以看到,LinkedHashMap完成了LruCache中的核心功能,那LruCache中剩下要做的就是定義快取空間總容量,當前儲存資料已使用的容量,對外提供put、get方法。
在瞭解了LruCache的核心原理之後,就可以開始分析LruCache的原始碼了。
(1)關鍵欄位
根據上面的分析,首先要有總容量、已使用容量、linkedHashMap這幾個關鍵欄位,LruCache中提供了下面三個關鍵欄位:
?
要注意的是size欄位,因為map中可以存放各種型別的資料,這些資料的大小測量方式也是不一樣的,比如Bitmap型別的資料和String型別的資料計算他們的大小方式肯定不同,因此,LruCache中在計算放入資料大小的方法sizeOf中,只是簡單的返回了1,需要我們重寫這個方法,自己去定義資料的測量方式。因此,我們在使用LruCache的時候,經常會看到這種方式:
?
(2)構造方法
?
LruCache只有一個唯一的構造方法,在構造方法中,給定了快取空間的總大小,初始化了LinkedHashMap核心資料結構,在LinkedHashMap中的第三個引數指定為true,也就設定了accessOrder=true,表示這個LinkedHashMap將是基於資料的訪問順序進行排序。
(3)sizeOf()和safeSizeOf()方法
根據上面的解釋,由於各種資料型別大小測量的標準不統一,具體測量的方法應該由使用者來實現,如上面給出的一個在實現LruCache時重寫sizeOf的一種常用實現方式。通過多型的性質,再具體呼叫sizeOf時會呼叫我們重寫的方法進行測量,LruCache對sizeOf()的呼叫進行一層封裝,如下:
?
裡面其實就是呼叫sizeOf()方法,返回sizeOf計算的大小。
上面就是LruCache的基本內容,下面就需要提供LruCache的核心功能了。
(4)put方法快取資料
首先看一下它的原始碼實現:
?
可以看到,put()方法主要有以下幾步:
1)key和value判空,說明LruCache中不允許key和value為null;
2)通過safeSizeOf()獲取要加入物件資料的大小,並更新當前快取資料的大小;
3)將新的物件資料放入到快取中,即呼叫LinkedHashMap的put方法,如果原來存在該key時,直接替換掉原來的value值,並返回之前的value值,得到之前value的大小,更新當前快取資料的size大小;如果原來不存在該key,則直接加入快取即可;
4)清理快取空間,如下;
(5)trimToSize()清理快取空間
當我們加入一個數據時(put),為了保證當前資料的快取所佔大小沒有超過我們指定的總大小,通過呼叫trimToSize()來對快取空間進行管理控制。如下:
?
trimToSize()方法的作用就是為了保證當前資料的快取大小不能超過我們指定的快取總大小,如果超過了,就會開始移除最近最少使用的資料,直到size符合要求。trimToSize()方法在put()的時候一定會呼叫,在get()的時候有可能會呼叫。
(6)get方法獲取快取資料
get方法原始碼如下:
?
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的原始碼如下:
?
可以發現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進行同步控制。
?
DiskLruCache中對於LinkedHashMap定義如下:
?
在LruCache中,由於資料是直接快取中記憶體中,map中資料的建立是在使用LruCache快取的過程中逐步建立的,而對於DiskLruCache,由於資料是快取在本地檔案,相當於是持久儲存下來的一個檔
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; }
|
2.1.3 LruCache原始碼分析
1 2 3 4 5 6 |
//核心資料結構
private
final LinkedHashMap<k, v= "" > map;
// 當前快取資料所佔的大小
private
int size;
//快取空間總容量
private
int maxSize;</k,>
|
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>
|
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,>
|
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;
}
|
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;
}
|
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,>
|
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;
}
}
|
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) {
}
|
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" );
}
}
|
1 2 |
private
final LinkedHashMap<string, entry= "" > lruEntries
=
new LinkedHashMap<string, entry= "" >( 0 ,
0 .75f, true );</string,></string,>
|