一开始研究Android LruCache源码的时候,我是在Android studio中直接看源码的,当时的配置compileSdkVersion 28,也就是9.0版本的,看到驱逐逻辑(trimToSize)的时候,发现一个问题,具体看看代码
/**
* @param maxSize the maximum size of the cache before returning. May be -1
* to evict even 0-sized elements.
*/
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);
}
}
trimToSize本身的逻辑很简单,就是在一个死循环中,如果已缓存大小size大于设置的最大值maxSize,就把最近最少使用的元素删除掉,直到size小于等于maxSize就退出循环,trimToSize执行结束。
问题的“最近最少使用元素”是怎么获取到的,上面的代码是遍历LinkedHashMap,获取最后一个元素,认为此元素就是lru,这个是不对的,我们知道最近最少使用元素是放在链表头的,而不是尾部。后来我翻了一下9.0 的Android源码
187 /**
188 * Remove the eldest entries until the total of remaining entries is at or
189 * below the requested size.
190 *
191 * @param maxSize the maximum size of the cache before returning. May be -1
192 * to evict even 0-sized elements.
193 */
194 public void trimToSize(int maxSize) {
195 while (true) {
196 K key;
197 V value;
198 synchronized (this) {
199 if (size < 0 || (map.isEmpty() && size != 0)) {
200 throw new IllegalStateException(getClass().getName()
201 + ".sizeOf() is reporting inconsistent results!");
202 }
203
204 if (size <= maxSize) {
205 break;
206 }
207
208 Map.Entry<K, V> toEvict = map.eldest();
209 if (toEvict == null) {
210 break;
211 }
212
213 key = toEvict.getKey();
214 value = toEvict.getValue();
215 map.remove(key);
216 size -= safeSizeOf(key, value);
217 evictionCount++;
218 }
219
220 entryRemoved(true, key, value, null);
221 }
222 }
是通过map.eldest()获取最近最少使用元素的,看看eldest源码
491 // Android-added: eldest(), for internal use in LRU caches
492 /**
493 * Returns the eldest entry in the map, or {@code null} if the map is empty.
494 * @hide
495 */
496 public Map.Entry<K, V> eldest() {
497 return head;
498 }
eldest就是直接获取链表头,说明是正确的。
所以,我分析,本身9.0的源码是正确的,只是Android studio中28的辅助编译库android.jar中的源码有问题。
我们看看23(6.0)是什么情况,我们把compileSdkVersion 改成23,分别看看android.jar和6.0中LruCache的源码
我们发现android.jar和6.0中LruCache的trimToCache源码是一样的,都是采用的map.eldest()。我们看看6.0 LinkedHashMap eldest源码
/**
* Returns the eldest entry in the map, or {@code null} if the map is empty.
* @hide
*/
public Entry<K, V> eldest() {
LinkedEntry<K, V> eldest = header.nxt;
return eldest != header ? eldest : null;
}
发现6.0和9.0中LinkedHashMap源码居然不一样,虽然实现有些出入,但是功能是一样的。
网上也有人发现了LruCache这一个奇怪现象(Android LruCache 的 Bug),不过不是他说的5.0之前不对,5.0之后就对了,人家Android本身源码没问题,只是提供编译的android.jar中有些问题,不会在项目中导致bug的。