Android 带你从源码看LruCache的实现原理(Android10&JDK1.8)

简介

LRULeast Recently Used),即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。常见的 页面置换算法详解(10种)

LruCache是一个采用LRU算法的缓存类。在Android应用开发中应用比较广泛,谷歌为我们做了基本实现的封装,但通常为了满足我们项目的需求,还需要实现自己的LRU缓存类,那么就要求我们对LruCache这个类有更深一层次的了解,不仅限于使用,本文从源码去解读LruCache的实现原理。

SDK中关于该类的介绍:

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类的主要成员有三个:

/**
 * LruCache中数据的主存储队列
 */
private final LinkedHashMap<K, V> map;
/**
 * 缓存的大小(单位)。不一定是元素的数量。
 */
private int size;
/**
 * 对于没有覆盖{@link #sizeOf}的缓存,maxSize 是缓存中的最大条目数。
 * 对于所有其他缓存,这是该缓存中条目大小的最大和。
 */
private int maxSize;

关于 Java LinkedHashMap的实现(Android10&JDK1.8)这里只简单描述下。

LinkedHashMap是一个实现了哈希表和链表的映射类,是HashMap的子类,继承了HashMap所有的可选操作。不同之处在于,它维护一个贯穿其所有条目的双链接列表,有可预测的迭代顺序(插入顺序访问顺序)。

  • 插入顺序的模式下,LinkedHashMap每次插入元素时(不论key是否存在)都按照插入的先后顺序排列。
  • 访问顺序的模式下,LinkedHashMap每次访问元素时(get或put都视为访问)将访问的元素从原位置移动到链表的尾部。

LruCache提供了唯一的有参构造方法,LinkedHashMap被构造为一个按照访问顺序排序的链式哈希表。

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);
}

存储

put(key, value)

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) {
			size -= safeSizeOf(key, previous);
		}
	}

	if (previous != null) {
		entryRemoved(false, key, previous, value);
	}

	trimToSize(maxSize);
	return previous;
}

方法使用synchronized 来保证线程安全,map.put(key, value)会将该key-value节点插入到链表尾部。当然,LinkedHashMap作为一个双向链表的数据结构,也可以说头部即尾部,尾部即头部。Java LinkedHashMap的实现(Android10&JDK1.8)

数据大小

safeSizeOf(key, value)

计算插入当前数据后,缓存空间的总大小。safeSizeOf的取值为sizeOf的返回值:


protected int sizeOf(K key, V value) {
	return 1;
}

用户定义的单位返回keyvalue的条目大小。默认实现返回的是1,也就是说size代表条目的数量,maxSize是条目的最大数量。而在实际应用中,我们通常以在不同的单元中调整缓存的大小来返回。

举个例子,我们做一个图片的位图缓存,这个缓存限制为4MB:

int cacheSize = 4 * 1024 * 1024; // 4MB
LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
	protected int sizeOf(String key, Bitmap value) {
		return value.getByteCount();
	}
}

移除数据

entryRemoved(boolean, key, oldValue, newValue)

/**
 * @param evicted 如果移除条目以腾出空间,则为true;如果移除是由put或remove引起的,则为false。
 * @param newValue 如果存在的话就是key的新值。如果这个值非空,则此删除是由put引起的。否则,它是由驱逐或remove引起的。
 */
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}

调用已被驱逐或删除的项。当一个值被逐出以创建空间时,或通过调用remove删除时,或通过调用put替换时,将调用此方法。默认实现不执行任何操作。调用该方法时也不需要同步,在此方法执行时,其他线程可能访问缓存。

如果缓存的值包含需要显式释放的资源,就需要重写此方法。

整理缓存空间

trimToSize(maxSize)

private 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) {
				break;
			}

			// BEGIN LAYOUTLIB CHANGE
			// get the last item in the linked list.
			// This is not efficient, the goal here is to minimize the changes
			// compared to the platform version.
			Map.Entry<K, V> toEvict = null;
			for (Map.Entry<K, V> entry : map.entrySet()) {
				toEvict = entry;
			}
			// END LAYOUTLIB CHANGE

			if (toEvict == null) {
				break;
			}

			key = toEvict.getKey();
			value = toEvict.getValue();
			map.remove(key);
			size -= safeSizeOf(key, value);
			evictionCount++;
		}

		entryRemoved(true, key, value, null);
	}
}

这里代码有个问题。我们知道access-order模式下的LinkedHashMap中元素按照访问的顺序由 最早访问->最后访问的顺序排序,最近访问的数据将被置于链表尾部。而此方法中的代码找到的明显是链表尾部的元素,即最近被访问数据。

for (Map.Entry<K, V> entry : map.entrySet()) {
	toEvict = entry;	
}

显然这与LruCache的设计描述是不符的。而通过我们实际的使用发现他最终的结果确实又是正确的。为什么会出现这种明显的错误呢,其实在类注释的一开始就有写道:

BEGIN LAYOUTLIB CHANGE
This is a custom version that doesn’t use the non standard LinkedHashMap#eldest.
END LAYOUTLIB CHANGE

LAYOUTLIB 是什么?>> LAYOUTLIB

“Layoutlib is a custom version of the android View framework designed to run inside Eclipse.
The goal of the library is to provide layout rendering in Eclipse that are very very close to their rendering on devices.
None of the com.android. or android. classes in layoutlib run on devices.”

大概意思就是:Layoutlibandroid视图框架的定制版本,这个库的目标是在Eclipse中提供布局呈现。实际运行在我们设备里时,运行的并不是LAYOUTLIB标记里的那段代码,而是下面这段代码:

Map.Entry<K, V> toEvict = map.eldest();
if (toEvict == null) {
    break;
}

这段代码可以在Android23 SDKLruCache源码中找到。eldest()LinkedHashMap中的方法。

// Android-added: eldest(), for internal use in LRU caches
/**
 * Returns the eldest entry in the map, or {@code null} if the map is empty.
 * @hide
 */
public Map.Entry<K, V> eldest() {
	return head;
}

所以,被踢出缓存队列的还是链表的头部节点,即最久未被访问的节点。

取值

get(key)

/**
 * Returns the value for {@code key} if it exists in the cache or can be
 * created by {@code #create}. If a value was returned, it is moved to the
 * head of the queue. This returns null if a value is not cached and cannot
 * be created.
 * 
 * 如果key存在于缓存中,返回它的value,或者返回由create创建的值。
 * 如果返回一个值,它将被移动到队列的尾部。如果未缓存或无法创建值,则返回null。
 */
public final V get(K key) {
	if (key == null) {
		throw new NullPointerException("key == null");
	}

	V mapValue;
	synchronized (this) {
		mapValue = map.get(key);//获取key的值,并将它置于链表尾部
		if (mapValue != null) {
			hitCount++;
			return mapValue;
		}
		missCount++;
	}

	/*
	 * 如果需要根据相应键的需要计算缓存找不到的值,请重写create()。
	 * create()默认返回null了,如果在create()中向map添加了一个冲突的值,会将该值保留在map中并释放创建的值。
	 * 调用该方法时不需要同步:在此方法执行时,其他线程可能访问缓存。
	 * 如果该方法返回时缓存中存在key的值,则创建的值将通过entryremove释放并丢弃。
	 * 当多个线程同时请求相同的键值(导致创建多个值),或者当一个线程调用put,而另一个线程正在为相同的键值创建值时,就会发生这种情况。
	 */
	V createdValue = create(key);
	if (createdValue == null) {
		return null;
	}

	synchronized (this) {
		createCount++;
		mapValue = map.put(key, createdValue);

		if (mapValue != null) {
			// 有冲突,所以撤销createdValue,并将原来的值保留
			map.put(key, mapValue);
		} else {
			size += safeSizeOf(key, createdValue);
		}
	}

	if (mapValue != null) {
		entryRemoved(false, key, createdValue, mapValue);
		return mapValue;
	} else {
		trimToSize(maxSize);
		return createdValue;
	}
}

源码中方法的注释还是说将访问的数据置于队列头部,实际上仍然是置于尾部,关于LinkedHashMap的访问顺序戳这里:Java LinkedHashMap的实现(Android10&JDK1.8)

总结

  • LruCache是一个采用最近最少使用算法的缓存类。
  • LruCache中维护了一个以访问顺序排序的LinkedHashMap对象。
  • LinkedHashMap中元素以最早访问到最后访问的顺序排序,每putget一个元素都会将其置于链表尾部。
  • LruCache超出容量时会优先移除链表头部的数据,即最早访问的数据。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值