編輯:關於Android編程
今天與大家分享一下圖片的緩存技術,利用它可以提高UI的流暢性、響應速度,給用戶好的體驗。
如何在內存中做緩存?
通過內存緩存可以快速加載緩存圖片,但會消耗應用的內存空間。LruCache類(通過兼容包可以支持到sdk4)很適合做圖片緩存,它通過LinkedHashMap保持圖片的強引用方式存儲圖片,當緩存空間超過設置定的限值時會釋放掉早期的緩存。
注:在過去,常用的內存緩存實現是通過SoftReference或WeakReference,但不建議這樣做。從Android2.3(API等級9)垃圾收集器開始更積極收集軟/弱引用,這使得它們相當無效。此外,在Android 3.0(API等級11)之前,存儲在native內存中的可見的bitmap不會被釋放,可能會導致應用程序暫時地超過其內存限制並崩潰。
什麼是LruCache?
LruCache實現原理是什麼?
要回答這個兩個問題,先要知道什麼是LRU。
LRU是Least Recently Used 的縮寫,翻譯過來就是“最近最少使用”,LRU緩存就是使用這種原理實現,簡單的說就是緩存一定量的數據,當超過設定的阈值時就把一些過期的數據刪除掉,比如我們緩存100M的數據,當總數據小於100M時可以隨意添加,當超過100M時就需要把新的數據添加進來,同時要把過期數據刪除,以確保我們最大緩存100M,那怎麼確定刪除哪條過期數據呢,采用LRU算法實現的話就是將最老的數據刪掉。利用LRU緩存,我們能夠提高系統的performance.
LruCache.java是 android.support.v4包引入的一個類,其實現原理就是基於LRU緩存算法,
要想實現LRU緩存,我們首先要用到一個類 LinkedHashMap。 用這個類有兩大好處:一是它本身已經實現了按照訪問順序的存儲,也就是說,最近讀取的會放在最前面,最不常讀取的會放在最後(當然,它也可以實現按照插入順序存儲)。第二,LinkedHashMap本身有一個方法用於判斷是否需要移除最不常讀取的數,但是,原始方法默認不需要移除(這是,LinkedHashMap相當於一個linkedlist),所以,我們需要override這樣一個方法,使得當緩存裡存放的數據個數超過規定個數後,就把最不常用的移除掉。LinkedHashMap的API寫得很清楚,推薦大家可以先讀一下。
要基於LinkedHashMap來實現LRU緩存,可以選擇inheritance, 也可以選擇 delegation, android源碼選擇的是delegation,而且寫得很漂亮。下面,就來剖析一下源碼的實現方法:
public class LruCache {
//緩存 map 集合,要用LinkedHashMap
private final LinkedHashMap map;
private int size; //已經存儲的大小
private int maxSize; //規定的最大存儲空間
private int putCount; //put的次數
private int createCount; //create的次數
private int evictionCount; //回收的次數
private int hitCount; //命中的次數
private int missCount; //丟失的次數
//實例化 Lru,需要傳入緩存的最大值,這個最大值可以是個數,比如對象的個數,也可以是內存的大小
//比如,最大內存只能緩存5兆
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap(0, 0.75f, true);
}
//重置最大存儲空間
public void resize(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
synchronized (this) {
this.maxSize = maxSize;
}
trimToSize(maxSize);
}
//通過key返回相應的item,或者創建返回相應的item。相應的item會移動到隊列的頭部,
// 如果item的value沒有被cache或者不能被創建,則返回null。
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
//如果丟失了就試圖創建一個item
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;
}
}
//創建cache項,並將創建的項放到隊列的頭部
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++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) { //返回的先前的value值
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
//清空cache空間
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 || map.isEmpty()) {
break;
}
Map.Entry toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
//刪除key相應的cache項,返回相應的value
public final V remove(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V previous;
synchronized (this) {
previous = map.remove(key);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}
/**
* Called for entries that have been evicted or removed. This method is
* invoked when a value is evicted to make space, removed by a call to
* {@link #remove}, or replaced by a call to {@link #put}. The default
* implementation does nothing.
* 當item被回收或者刪掉時調用。改方法當value被回收釋放存儲空間時被remove調用,
* 或者替換item值時put調用,默認實現什麼都沒做
*
The method is called without synchronization: other threads may * access the cache while this method is executing. * * @param evicted true if the entry is being removed to make space, false * if the removal was caused by a {@link #put} or {@link #remove}. * true---為釋放空間被刪除;false---put或remove導致 * @param newValue the new value for {@code key}, if it exists. If non-null, * this removal was caused by a {@link #put}. Otherwise it was caused by * an eviction or a {@link #remove}. */ protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {} //當某Item丟失時會調用到,返回計算的相應的value或者null protected V create(K key) { return null; } private int safeSizeOf(K key, V value) { int result = sizeOf(key, value); if (result < 0) { throw new IllegalStateException("Negative size: " + key + "=" + value); } return result; } //這個方法要特別注意,跟我們實例化LruCache的maxSize要呼應,怎麼做到呼應呢,比如maxSize的大小為緩存 //的個數,這裡就是return 1就ok,如果是內存的大小,如果5M,這個就不能是個數了,就需要覆蓋這個方法,返回每個緩存 //value的size大小,如果是Bitmap,這應該是bitmap.getByteCount(); protected int sizeOf(K key, V value) { return 1; } //清空cacke public final void evictAll() { trimToSize(-1); // -1 will evict 0-sized elements } /** * For caches that do not override {@link #sizeOf}, this returns the number * of entries in the cache. For all other caches, this returns the sum of * the sizes of the entries in this cache. */ public synchronized final int size() { return size; } public synchronized final int maxSize() { return maxSize; } /** * Returns the number of times {@link #get} returned a value that was * already present in the cache. */ public synchronized final int hitCount() { return hitCount; } /** * Returns the number of times {@link #get} returned null or required a new * value to be created. */ public synchronized final int missCount() { return missCount; } public synchronized final int createCount() { return createCount; } public synchronized final int putCount() { return putCount; } //返回被回收的數量 public synchronized final int evictionCount() { return evictionCount; } //返回當前cache的副本,從最近最少訪問到最多訪問 public synchronized final Map
從源代碼中,我們可以清晰的看出LruCache的緩存機制。
此外,還有一個開源的使用磁盤緩存的方法DiskLruCache,源代碼:https://github.com/JakeWharton/DiskLruCache
其詳細的使用方法可以參考鏈接:http://www.tuicool.com/articles/JB7RNj
根據上面的代碼,當我們用LruCache來緩存圖片時,一定要重寫protected int sizeOf(K key, V value) {}
方法,否則,最大緩存的是數量而不是占用內存大小。重寫protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
方法,在裡面處理內存回收。
下面例子繼承LruCache,實現相關方法:
public class BitmapCache extends LruCache{
private BitmapRemovedCallBack mEnterRemovedCallBack;
public BitmapCache(int maxSize, BitmapRemovedCallBack callBack) {
super(maxSize);
mEnterRemovedCallBack = callBack;
}
//當緩存大於我們設定的最大值時,會調用這個方法,我們在這裡面做內存釋放操作
@Override
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
if (evicted && oldValue != null){
//在回收bitmap之前,務必要先在下面的回調方法中將bitmap設給的View的bitmapDrawable設為null
//否則,bitmap被回收後,很容易出現cannot draw recycled bitmap的報錯。切記!
mEnterRemovedCallBack.onBitmapRemoved(key);
oldValue.recycle();
}
}
//獲取每個 value 的大小
@Override
protected int sizeOf(K key, V value) {
int size = 0;
if (value != null) {
size = value.getByteCount();
}
return size;
}
public interface BitmapRemovedCallBack{
void onBitmapRemoved(K key);
}
}
使用BitmapCache時,在構造方法中傳入最大緩存量和一個回掉方法就行:
private BitmapCache mMemoryCache;
private BitmapCache.BitmapRemovedCallBack mEnteryRemovedCallBack =
new BitmapCache.BitmapRemovedCallBack() {
@Override
public void onBitmapRemoved(String key) {
//處理回收bitmap前,清空相關view的bitmap操作
}
};
Override
protected void onCreate(Bundle savedInstanceState) {
// 獲取到可用內存的最大值,使用內存超出這個值會引起OutOfMemory異常。
// BitmapCache通過構造函數傳入緩存值,以bit為單位。
int memClass = ((ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();
// 使用單個應用最大可用內存值的1/8作為緩存的大小。
int cacheSize = 1024 * 1024 * memClass / 8;
mMemoryCache = new BitmapCache(cacheSize, mEnteryRemovedCallBack);
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
當向 ImageView 中加載一張圖片時,首先會在 BitmapCache 的緩存中進行檢查。如果找到了相應的鍵值,則會立刻更新ImageView ,否則開啟一個後台線程來加載這張圖片:
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
} else {
imageView.setImageResource(R.drawable.image_placeholder);
BitmapLoadingTask task = new BitmapLoadingTask(imageView);
task.execute(resId);
}
}
BitmapLoadingTask後台線程,把新加載出來的圖片以鍵值對形式放到緩存中:
class BitmapLoadingTask extends AsyncTask {
// 在後台加載圖片。
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100);
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
}
1、LruCache 是基於 Lru算法實現的一種緩存機制;
2、Lru算法的原理是把近期最少使用的數據給移除掉,當然前提是當前數據的量大於設定的最大值。
3、LruCache 沒有真正的釋放內存,只是從 Map中移除掉數據,真正釋放內存還是要用戶手動釋放。
4、手動釋放bitmap的內存時,需要先清除相關view中的bitmap。
前言最近因為公司項目需求,需要遠程調度啟動客戶端輸入法輸入內容。這就是大致的需求流程,這篇首先講遠程與服務控制端通訊。首先控制服務端定義好一個Service,且在Serv
啦啦啦,這是山寨UC浏覽器的下拉刷新效果的第二篇,第一篇請移步Android 自定義View UC下拉刷新效果(一)我們看圖說話:主要工作1.下拉刷新的圓形向回首頁的圓形
前面我們介紹過了HTTP協議和Socket,這一篇我們來介紹一下Android的一個網絡控件:WebView-網頁視圖。我們知道,現在移動端有兩種開發方向:原生開發和H5
上一篇文章中我們講解了關於Android開發過程中常見的內存洩露場景與檢測方案。Android系統為每個應用程序分配的內存是有限的,當一個應用中產生的內存洩漏的情況比較多