概述
LRU (Least Recently Used) 的意思就是近期最少使用算法,它的核心思想就是會優(yōu)先淘汰那些近期最少使用的緩存對象。
在我們?nèi)粘i_發(fā)中,UI 界面進行網(wǎng)絡(luò)圖片加載是很正常的一件事情,但是當界面上的圖片過于多的時候扑眉,不可能每次都從網(wǎng)絡(luò)上進行圖片的獲取,一方面效率會很低赖钞,另一方面腰素,也會非常耗費用戶的流量。
Android 為我們提供了 LruCache 類雪营,使用它我們可以進行圖片的內(nèi)存緩存弓千,今天我們就一起學習一下吧。
使用 LruCache 進行圖片加載
1. 編寫 MyImageLoader 類献起,實現(xiàn)圖片緩存功能洋访。
package com.keven.jianshu.part6;
import android.graphics.Bitmap;
import android.util.LruCache;
/**
* Created by keven on 2019/5/28.
*/
public class MyImageLoader {
private LruCache<String, Bitmap> mLruCache;
/**
* 構(gòu)造函數(shù)
*/
public MyImageLoader() {
//設(shè)置最大緩存空間為運行時內(nèi)存的 1/8
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheSize = maxMemory / 8;
mLruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
//計算一個元素的緩存大小
return value.getByteCount();
}
};
}
/**
* 添加圖片到 LruCache
*
* @param key
* @param bitmap
*/
public void addBitmap(String key, Bitmap bitmap) {
if (getBitmap(key) == null) {
mLruCache.put(key, bitmap);
}
}
/**
* 從緩存中獲取圖片
*
* @param key
* @return
*/
public Bitmap getBitmap(String key) {
return mLruCache.get(key);
}
/**
* 從緩存中刪除指定的 Bitmap
*
* @param key
*/
public void removeBitmapFromMemory(String key) {
mLruCache.remove(key);
}
}
至于代碼的具體含義,注釋已經(jīng)進行了詮釋谴餐。
2. 在 Activity 中進行圖片的緩存及加載
public class Part6ImageActivity extends AppCompatActivity {
private static String imgUrl = "https://ss0.bdstatic.com/94oJfD_bAAcT8t7mm9GUKT-xh_/timg?image&quality=100&size=b4000_4000&sec=1559013549&di=41b6aa8d219f05d44708d296dbf96b5f&src=http://img5.duitang.com/uploads/item/201601/03/20160103233143_4KLWs.jpeg";
private static final int SUCCESS = 0x0001;
private static final int FAIL = 0x0002;
private MyHandler mHandler;
private static ImageView mImageView;
private static MyImageLoader mImageLoader;
private Button mBt_load;
static class MyHandler extends Handler {
//創(chuàng)建一個類繼承 Handler
WeakReference<AppCompatActivity> mWeakReference;
public MyHandler(AppCompatActivity activity) {
mWeakReference = new WeakReference<>(activity);
}
//在 handleMessage 方法中對網(wǎng)絡(luò)下載的圖片進行處理
@Override
public void handleMessage(Message msg) {
final AppCompatActivity appCompatActivity = mWeakReference.get();
if (appCompatActivity != null) {
switch (msg.what) {
case SUCCESS://成功
byte[] Picture = (byte[]) msg.obj;
Bitmap bitmap = BitmapFactory.decodeByteArray(Picture, 0, Picture.length);
mImageLoader.addBitmap(ImageUtils.hashKeyForCache(imgUrl), bitmap);
mImageView.setImageBitmap(bitmap);
break;
case FAIL://失敗
break;
}
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_part6_image);
//創(chuàng)建 Handler
mHandler = new MyHandler(this);
mImageView = findViewById(R.id.iv_lrucache);
//創(chuàng)建自定義的圖片加載類
mImageLoader = new MyImageLoader();
mBt_load = findViewById(R.id.bt_load);
//點擊按鈕進行圖片加載
mBt_load.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Bitmap bitmap = getBitmapFromCache();
if (bitmap != null) {//有緩存
LogUtils.e("從緩存中取出圖片");
mImageView.setImageBitmap(bitmap);
} else {//沒有緩存
LogUtils.e("從網(wǎng)絡(luò)下載圖片");
downLoadBitmap();
}
}
});
}
/**
* 從緩存中獲取圖片
*
* @return
*/
private Bitmap getBitmapFromCache() {
return mImageLoader.getBitmap(ImageUtils.hashKeyForCache(imgUrl));
}
/**
* 從網(wǎng)絡(luò)上下載圖片
* 使用 OKHttp 進行圖片的下載
*/
private void downLoadBitmap() {
OkHttpClient okHttpClient = new OkHttpClient();
Request request = new Request.Builder()
.url(imgUrl)
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
byte[] Picture_bt = response.body().bytes();
Message message = mHandler.obtainMessage();
message.obj = Picture_bt;
message.what = SUCCESS;
mHandler.sendMessage(message);
}
});
}
}
其中的布局文件就很簡單姻政,一個按鈕 + 一個 Imageview
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".part6.Part6ImageActivity">
<Button
android:id="@+id/bt_load"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:text="加載圖片"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/iv_lrucache"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>
代碼中還用到了一個工具類,主要用于將圖片的 url 轉(zhuǎn)換為 md5 編碼后的字符串岂嗓,用作緩存文件的 key 進行存儲汁展,保證其獨一性
public class ImageUtils {
public static String hashKeyForCache(String key) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
private static String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
}
3. 實際使用
我們進行加載圖片按鈕的多次點擊,通過 log 進行查看是否正常緩存
com.keven.jianshu E/TAG: 從網(wǎng)絡(luò)下載圖片
com.keven.jianshu E/TAG: 從緩存中取出圖片
com.keven.jianshu E/TAG: 從緩存中取出圖片
可以看出厌殉,除了第一次圖片是從網(wǎng)絡(luò)上進行下載食绿,之后都是從緩存中進行獲取。
LruCache 原理解析
LruCache 的文檔描述
A cache that holds strong references to a limited number of values. Each time a value is accessed, it is moved to the head of a queue. When a value is added to a full cache, the value at the end of that queue is evicted and may become eligible for garbage collection.
一個包含有限數(shù)量強引用的緩存公罕,每次訪問一個值器紧,它都會被移動到隊列的頭部,將一個新的值添加到已經(jīng)滿了的緩存隊列時楼眷,該隊列末尾的值將會被逐出铲汪,并且可能會被垃圾回收機制進行回收。
LruCache 構(gòu)造函數(shù)
創(chuàng)建了一個 LinkedHashMap摩桶,三個參數(shù)分別為 初始容量桥状、加載因子和訪問順序,當 accessOrder 為 true 時硝清,這個集合的元素順序就會是訪問順序辅斟,也就是訪問了之后就會將這個元素放到集合的最后面。
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);
}
有些人可能會有疑問芦拿,初始容量傳 0 的話士飒,那豈不是沒辦法進行存儲了查邢,那么創(chuàng)建這個 LinkedHashMap 還有什么意義呢?
其實要解答這個問題并不難酵幕,看下源碼你就會發(fā)現(xiàn)
- 其實第一個參數(shù)是你要設(shè)置的初始大腥排骸;而程序內(nèi)部實際的初始大小是1芳撒;
- 如果你設(shè)置的初始大小(initialCapacity)小于1, 那么map大小就是默認的1邓深;
- 否則會不斷左移(乘2)直到capacity大于你設(shè)置的initialCapacity;
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);//這里調(diào)用父類HashMap的構(gòu)造方法笔刹;
this.accessOrder = accessOrder;
}
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// Find a power of 2 >= initialCapacity
int capacity = 1; // 默認是1
while (capacity < initialCapacity)//不斷翻倍直到大于人為設(shè)置的大小
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);//的確如你所言芥备,后面如果需要增大長度,按照capacity*loadFactor取整后增長舌菜;
table = new Entry[capacity];
init();
}
LruCache 的 put 方法
其中的 trimToSize() 方法用于判斷加入元素后是否超過最大緩存數(shù)萌壳,如果超過就清除掉最少使用的元素。
public final V put(K key, V value) {
// 如果 key 或者 value 為 null日月,則拋出異常
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized(this) {
// 加入元素的數(shù)量袱瓮,在 putCount() 用到
putCount++;
// 回調(diào)用 sizeOf(K key, V value) 方法,這個方法用戶自己實現(xiàn)爱咬,默認返回 1
size += safeSizeOf(key, value);
// 返回之前關(guān)聯(lián)過這個 key 的值尺借,如果沒有關(guān)聯(lián)過則返回 null
previous = map.put(key, value);
if (previous != null) {
// safeSizeOf() 默認返回 1
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
// 該方法默認方法體為空
entryRemoved(false, key, previous, value);
}
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!");
}
// 直到緩存大小 size 小于或等于最大緩存大小 maxSize,則停止循環(huán)
if (size <= maxSize) {
break;
}
// 取出 map 中第一個元素
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(true, key, value, null);
}
}
public Map.Entry<K, V> eldest() {
return head;
}
LruCache 的 get 方法
LruCahche 的 get() 方法源碼
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
//從 LinkedHashMap 中獲取值
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
LinkedHashMap 的 get() 方法源碼
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
//如果訪問順序設(shè)置為 true,則執(zhí)行 afterNodeAccess(e) 方法
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
afterNodeAccess() 方法源碼
// 這個方法的作用就是將剛訪問過的元素放到集合的最后一位
void afterNodeAccess(Node < K, V > e) {
LinkedHashMap.Entry < K, V > last;
if (accessOrder && (last = tail) != e) {
// 將 e 轉(zhuǎn)換成 LinkedHashMap.Entry
// b 就是這個節(jié)點之前的節(jié)點
// a 就是這個節(jié)點之后的節(jié)點
LinkedHashMap.Entry < K, V > p = (LinkedHashMap.Entry < K, V > ) e, b = p.before, a = p.after;
// 將這個節(jié)點之后的節(jié)點置為 null
p.after = null;
// b 為 null台颠,則代表這個節(jié)點是第一個節(jié)點褐望,將它后面的節(jié)點置為第一個節(jié)點
if (b == null) head = a;
// 如果不是,則將 a 上前移動一位
else b.after = a;
// 如果 a 不為 null串前,則將 a 節(jié)點的元素變?yōu)?b
if (a != null) a.before = b;
else last = b;
if (last == null) head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
LruCache 的 remove 方法
從緩存中刪除內(nèi)容,并更新緩存大小
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;
}
總結(jié)
- 當緩存滿了之后实蔽,LruCache 是最近最少使用的元素會被移除
- 內(nèi)部使用了 LinkedHashMap 進行存儲
- 總緩存大小一般為可用內(nèi)存的 1/8
- 當使用 get() 訪問元素后荡碾,會將該元素移動到 LinkedHashMap 的尾部