概述
LRU (Least Recently Used) 的意思就是近期最少使用算法,它的核心思想就是会优先淘汰那些近期最少使用的缓存对象。
在我们日常开发中,UI 界面进行网络图片加载是很正常的一件事情,但是当界面上的图片过于多的时候,不可能每次都从网络上进行图片的获取,一方面效率会很低,另一方面,也会非常耗费用户的流量。
Android 为我们提供了 LruCache 类,使用它我们可以进行图片的内存缓存,今天我们就一起学习一下吧。
使用 LruCache 进行图片加载
1. 编写 MyImageLoader 类,实现图片缓存功能。
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 mLruCache;
/**
* 构造函数
*/
public MyImageLoader() {
//设置最大缓存空间为运行时内存的 1/8
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheSize = maxMemory / 8;
mLruCache = new LruCache(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);
}
}
至于代码的具体含义,注释已经进行了诠释。
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 {
//创建一个类继承 Handler
WeakReference mWeakReference;
public MyHandler(AppCompatActivity activity) {
mWeakReference = new WeakReference<>(activity);
}
//在 handleMessage 方法中对网络下载的图片进行处理
@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);
//创建 Handler
mHandler = new MyHandler(this);
mImageView = findViewById(R.id.iv_lrucache);
//创建自定义的图片加载类
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("从网络下载图片");
downLoadBitmap();
}
}
});
}
/**
* 从缓存中获取图片
*
* @return
*/
private Bitmap getBitmapFromCache() {
return mImageLoader.getBitmap(ImageUtils.hashKeyForCache(imgUrl));
}
/**
* 从网络上下载图片
* 使用 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
代码中还用到了一个工具类,主要用于将图片的 url 转换为 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: 从网络下载图片
com.keven.jianshu E/TAG: 从缓存中取出图片
com.keven.jianshu E/TAG: 从缓存中取出图片
可以看出,除了第一次图片是从网络上进行下载,之后都是从缓存中进行获取。
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.
一个包含有限数量强引用的缓存,每次访问一个值,它都会被移动到队列的头部,将一个新的值添加到已经满了的缓存队列时,该队列末尾的值将会被逐出,并且可能会被垃圾回收机制进行回收。
LruCache 构造函数
创建了一个 LinkedHashMap,三个参数分别为 初始容量、加载因子和访问顺序,当 accessOrder 为 true 时,这个集合的元素顺序就会是访问顺序,也就是访问了之后就会将这个元素放到集合的最后面。
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap(0, 0.75f, true);
}
有些人可能会有疑问,初始容量传 0 的话,那岂不是没办法进行存储了,那么创建这个 LinkedHashMap 还有什么意义呢?
其实要解答这个问题并不难,看下源码你就会发现
- 其实第一个参数是你要设置的初始大小;而程序内部实际的初始大小是1;
- 如果你设置的初始大小(initialCapacity)小于1, 那么map大小就是默认的1;
- 否则会不断左移(乘2)直到capacity大于你设置的initialCapacity;
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);//这里调用父类HashMap的构造方法;
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)//不断翻倍直到大于人为设置的大小
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);//的确如你所言,后面如果需要增大长度,按照capacity*loadFactor取整后增长;
table = new Entry[capacity];
init();
}
LruCache 的 put 方法
其中的 trimToSize() 方法用于判断加入元素后是否超过最大缓存数,如果超过就清除掉最少使用的元素。
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) {
// 加入元素的数量,在 putCount() 用到
putCount++;
// 回调用 sizeOf(K key, V value) 方法,这个方法用户自己实现,默认返回 1
size += safeSizeOf(key, value);
// 返回之前关联过这个 key 的值,如果没有关联过则返回 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,则停止循环
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 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 e;
if ((e = getNode(hash(key), key)) == null)
return null;
//如果访问顺序设置为 true,则执行 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 转换成 LinkedHashMap.Entry
// b 就是这个节点之前的节点
// a 就是这个节点之后的节点
LinkedHashMap.Entry < K, V > p = (LinkedHashMap.Entry < K, V > ) e, b = p.before, a = p.after;
// 将这个节点之后的节点置为 null
p.after = null;
// b 为 null,则代表这个节点是第一个节点,将它后面的节点置为第一个节点
if (b == null) head = a;
// 如果不是,则将 a 上前移动一位
else b.after = a;
// 如果 a 不为 null,则将 a 节点的元素变为 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 方法
从缓存中删除内容,并更新缓存大小
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;
}
总结
- 当缓存满了之后,LruCache 是最近最少使用的元素会被移除
- 内部使用了 LinkedHashMap 进行存储
- 总缓存大小一般为可用内存的 1/8
- 当使用 get() 访问元素后,会将该元素移动到 LinkedHashMap 的尾部