[关闭]
@946898963 2018-05-18T12:15:08.000000Z 字数 4307 阅读 1239

LruCache源码解析

Android源码分析


本文转载自他人,为本人备忘学习所用,原文链接

上一篇分析了LinkedHashMap源码,这个Map集合除了拥有HashMap的大部分特性之外,还拥有链表的特点,即可以保持遍历顺序与插入顺序一致。另外,当我们将accessOrder设置为true时,可以使遍历顺序和访问顺序一致,其内部双向链表将会按照近期最少访问到近期最多访问的顺序排列Entry对象,这可以用来做缓存。

这篇文章分析的LruCache就是利用LinkedHashMap实现的缓存。

LruCache是Android3.1所提供的一个缓存类,通过support-v4兼容包可以兼容到早期的Android版本。
LruCache是一个泛型类,是线程安全的,内部采用LinkedHashMap以强引用的方式存储外界缓存对象。
提供get(String key)和put(String key , V value)方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早的使用的缓存对象
LruCache初始化时需重写sizeOf(String key ,V value)方法,用于计算缓存对象的大小。

下面开始分析LruCache。
注:下面LruCache源码来自support.v4包。
首先是这个类的成员变量:

  1. private final LinkedHashMap<K, V> map;
  2. /** Size of this cache in units. Not necessarily the number of elements. */
  3. private int size;//当前大小
  4. private int maxSize;//最大容量
  5. private int putCount;//put次数
  6. private int createCount;//create次数
  7. private int evictionCount;//回收次数
  8. private int hitCount;//命中次数
  9. private int missCount;//丢失次数

LinkedHashMap的初始化放在构造器中:

  1. public LruCache(int maxSize) {
  2. if (maxSize <= 0) {
  3. throw new IllegalArgumentException("maxSize <= 0");
  4. }
  5. this.maxSize = maxSize;
  6. this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
  7. }

这里将LinkedHashMap的accessOrder设置为true。
接下来看两个最重要的方法,put和get。首先是put方法:

  1. public final V put(K key, V value) {
  2. if (key == null || value == null) {//键值不允许为空
  3. throw new NullPointerException("key == null || value == null");
  4. }
  5. V previous;
  6. synchronized (this) {//线程安全
  7. putCount++;
  8. size += safeSizeOf(key, value);
  9. previous = map.put(key, value);
  10. if (previous != null) {//之前已经插入过相同的key
  11. size -= safeSizeOf(key, previous);//那么减去该entry的容量,因为发生覆盖
  12. }
  13. }
  14. if (previous != null) {
  15. entryRemoved(false, key, previous, value);//这个方法默认空实现
  16. }
  17. trimToSize(maxSize);//若容量超过maxsize,将会删除最近很少访问的entry
  18. return previous;
  19. }

put方法无非就是调用LinkedHashMap的put方法,但是这里在调用LinkedHashMap的put方法之前,判断了key和value是否为空,也就是说LruCache不允许空键值。除此之外,put操作被加锁了,所以是线程安全的!
既然是缓存,那么必然能够动态删除一些不常用的键值对,这个工作是由trimToSize方法完成的:

  1. public void trimToSize(int maxSize) {
  2. while (true) {//不断删除linkedHashMap头部entry,也就是最近最少访问的条目,直到size小于最大容量
  3. K key;
  4. V value;
  5. synchronized (this) {//线程安全
  6. if (size < 0 || (map.isEmpty() && size != 0)) {
  7. throw new IllegalStateException(getClass().getName()
  8. + ".sizeOf() is reporting inconsistent results!");
  9. }
  10. if (size <= maxSize || map.isEmpty()) {//直到容量小于最大容量为止
  11. break;
  12. }
  13. Map.Entry<K, V> toEvict = map.entrySet().iterator().next();//指向链表头
  14. key = toEvict.getKey();
  15. value = toEvict.getValue();
  16. map.remove(key);//删除最少访问的entry
  17. size -= safeSizeOf(key, value);
  18. evictionCount++;
  19. }
  20. entryRemoved(true, key, value, null);
  21. }
  22. }

这个方法不断循环删除链表首部元素,也就是最近最少访问的元素,直到容量不超过预先定义的最大值为止。
注:LruCache在android.util包中也有一个LruCache类,但是我发现这个类的trimToSize方法是错误的:

  1. private void trimToSize(int maxSize) {
  2. while (true) {
  3. K key;
  4. V value;
  5. synchronized (this) {
  6. if (size < 0 || (map.isEmpty() && size != 0)) {
  7. throw new IllegalStateException(getClass().getName()
  8. + ".sizeOf() is reporting inconsistent results!");
  9. }
  10. if (size <= maxSize) {
  11. break;
  12. }
  13. Map.Entry<K, V> toEvict = null;
  14. for (Map.Entry<K, V> entry : map.entrySet()) {
  15. toEvict = entry;
  16. }
  17. if (toEvict == null) {
  18. break;
  19. }
  20. key = toEvict.getKey();
  21. value = toEvict.getValue();
  22. map.remove(key);
  23. size -= safeSizeOf(key, value);
  24. evictionCount++;
  25. }
  26. entryRemoved(true, key, value, null);
  27. }
  28. }

这里的代码将会循环删除链表尾部,也就是最近访问最多的元素,这是不正确的!所以大家在做内存缓存的时候一定要注意,看trimToSize方法是否有问题。

接下来是get方法:

  1. public final V get(K key) {
  2. if (key == null) {//不允许空键
  3. throw new NullPointerException("key == null");
  4. }
  5. V mapValue;
  6. synchronized (this) {//线程安全
  7. mapValue = map.get(key);//调用LinkedHashMap的get方法
  8. if (mapValue != null) {
  9. hitCount++;//命中次数加1
  10. return mapValue;//返回value
  11. }
  12. missCount++;//未命中
  13. }
  14. V createdValue = create(key);//默认返回为false
  15. if (createdValue == null) {
  16. return null;
  17. }
  18. synchronized (this) {
  19. createCount++;//如果创建成功,那么create次数加1
  20. mapValue = map.put(key, createdValue);//放到哈希表中
  21. if (mapValue != null) {
  22. // There was a conflict so undo that last put
  23. map.put(key, mapValue);
  24. } else {
  25. size += safeSizeOf(key, createdValue);
  26. }
  27. }
  28. if (mapValue != null) {
  29. entryRemoved(false, key, createdValue, mapValue);
  30. return mapValue;
  31. } else {
  32. trimToSize(maxSize);
  33. return createdValue;
  34. }
  35. }

get方法即根据key在LinkedHashMap中寻找对应的value,此方法也是线程安全的。

以上就是LruCache最重要的部分,下面再看下其他方法:
remove:

  1. public final V remove(K key) {
  2. if (key == null) {
  3. throw new NullPointerException("key == null");
  4. }
  5. V previous;
  6. synchronized (this) {
  7. previous = map.remove(key);//调用LinkedHashMap的remove方法
  8. if (previous != null) {
  9. size -= safeSizeOf(key, previous);
  10. }
  11. }
  12. if (previous != null) {
  13. entryRemoved(false, key, previous, null);
  14. }
  15. return previous;//返回value
  16. }

sizeof:这个方法用于计算每个条目的大小,子类必须得复写这个类。

  1. protected int sizeOf(K key, V value) {//用于计算每个条目的大小
  2. return 1;
  3. }

snapshot方法,返回当前缓存中所有的条目集合

  1. public synchronized final Map<K, V> snapshot() {
  2. return new LinkedHashMap<K, V>(map);
  3. }

总结:
1.LruCache封装了LinkedHashMap,提供了LRU缓存的功能;
2.LruCache通过trimToSize方法自动删除最近最少访问的键值对;
3.LruCache不允许空键值;
4.LruCache线程安全;
5.继承LruCache时,必须要复写sizeof方法,用于计算每个条目的大小。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注