LruCache - Picasso的实现

所谓LRU,即 Least Recently Used,“最近使用”。
Picasso的LruCache实现了其自定义的一个interface:Cache,刚开始以为是extends关系,看了源码才发现不是。作为一个依赖Module,设计应当是让上层调用更容易扩展而无需影响底层实现的。细节值得学习。
和v4包里的LruCache一样,内部存储容器也是LinkedHashMap,双向链表结构。

final LinkedHashMap map;

先看构造函数:

public LruCache(@NonNull Context context) {
   this(Utils.calculateMemoryCacheSize(context));
}

static int calculateMemoryCacheSize(Context context) {
   ActivityManager am = getService(context, ACTIVITY_SERVICE);
   boolean largeHeap = (context.getApplicationInfo().flags & FLAG_LARGE_HEAP) != 0;
   int memoryClass = largeHeap ? am.getLargeMemoryClass() : am.getMemoryClass();
   // Target ~15% of the available heap.
   return (int) (1024L * 1024L * memoryClass / 7);
 }

先计算内存缓存的空间大小,看一下到ActivityManager层的调用:

static public int staticGetMemoryClass() {
  String vmHeapSize = SystemProperties.get("dalvik.vm.heapgrowthlimit", "");
  if (vmHeapSize != null && !"".equals(vmHeapSize)) {
    return Integer.parseInt(vmHeapSize.substring(0,   vmHeapSize.length()-1));
  }
  return staticGetLargeMemoryClass();
}

static public int staticGetLargeMemoryClass() {
  String vmHeapSize = SystemProperties.get("dalvik.vm.heapsize", "16m");
  return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length() - 1));
}

不同设备的虚拟机为每个进程分配的内存是不一样的,可以在 /system/build.prop 中查看,例如这样的:

dalvik.vm.heapsize=24m
dalvik.vm.heapgrowthlimit=16m

当在manifest里指定android:largeHeap为true时,会申请获得最大的内存,即heapsize的大小24m,一般情况为false时为16m,Picasso里取1/7用为内存缓存的空间大小。
获得了内存缓存空间 size 后,又调用了以下构造方法:

public LruCache(int maxSize) {
    this.maxSize = maxSize;
    this.map = new LinkedHashMap<>(0, 0.75f, true);
  }

LinkedHashMap的构造函数:

Parameters:
initialCapacity the initial capacity
loadFactor the load factor
accessOrder the ordering mode - true for access-order, false for insertion-order

public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

如文档注释所说,第三个参数为true时,记录顺序为访问顺序(也就是这个特点才让它成为了LruCache的存储容器),为false时记录顺序为插入顺序。看源码时觉得第二个参数 “0.75” 很有意思,一般来说不是不提倡使用这种 “magic number” 吗?看了v4包里的LruCache的构造函数里也使用了这个0.75,跟进LinkedHashMap的源码看了一番,发现其默认值也是0.75,即使自定义值进去也没有在任何地方被赋值。其父类HashMap的文档里有这么一段说明:

The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.
As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.

这里涉及到一个HashMap的自动扩容问题,当哈希表中的条目数超出了load factor 与 capacity 的乘积时,通过调用 rehash 方法将容量翻倍。这个 load factor 就是用来在空间&时间上权衡时的一个最佳值。需知更多可参考:

  • http://stackoverflow.com/questions/10901752/what-is-the-significance-of-load-factor-in-hashmap
  • http://blog.csdn.net/ghsau/article/details/16890151

构造函数之后,去看常用的方法 set(String key,Bitmap bitmap)

  @Override public void set(@NonNull String key, @NonNull Bitmap bitmap) {
    if (key == null || bitmap == null) {
      throw new NullPointerException("key == null || bitmap == null");
    }
    //计算要存入的bitmap的大小,如果直接超出了可分配的最大内存缓存空间就return
    int addedSize = Utils.getBitmapBytes(bitmap);
    if (addedSize > maxSize) {
      return;
    }
    //由于HashMap是线程不安全的,这里加个锁
    synchronized (this) {
      putCount++;
      size += addedSize;
      Bitmap previous = map.put(key, bitmap);
      if (previous != null) {
        //当key已经存过时,将map的容量减回刚才加前的值
        size -= Utils.getBitmapBytes(previous);
      }
    }

    trimToSize(maxSize);
  }

trimToSize(int maxSize)很关键:

 private void trimToSize(int maxSize) {
    while (true) {
      String key;
      Bitmap value;
      synchronized (this) {
        //1
        if (size < 0 || (map.isEmpty() && size != 0)) {
          throw new IllegalStateException(
              getClass().getName() + ".sizeOf() is reporting inconsistent results!");
        }
        //2
        if (size <= maxSize || map.isEmpty()) {
          break;
        }
        //3
        Map.Entry toEvict = map.entrySet().iterator().next();
        key = toEvict.getKey();
        value = toEvict.getValue();
        map.remove(key);
        size -= Utils.getBitmapBytes(value);
        evictionCount++;
      }
    }
  }

走到第3块代码时,就是说明当前的缓存容量已经超出可分配的最大内存了,迭代整个map,由于当前LinkedHashMap的存储顺序是按访问顺序存储,那么经由map.entrySet().iterator().next()迭代出的就是最先添加进且最不经常访问的那个Entry了,从map中将它remove(),然后更新此时map的容量,并且将移除次数的记录+1,直到符合第2块代码的判断,即缓存的内存容量小于最大可分配的空间。
get(String key)就更加简单:

 @Override public Bitmap get(@NonNull String key) {
    if (key == null) {
      throw new NullPointerException("key == null");
    }

    Bitmap mapValue;
    synchronized (this) {
      mapValue = map.get(key);
      if (mapValue != null) {
        hitCount++;
        return mapValue;
      }
      missCount++;
    }

    return null;
  }

命中了就 hitCount++ 记录并且返回该value,否则 missCount++ 。
在此类里发现一个clearKeyUri(String uri)方法,这是Picasso从2.5.0开始增加的一个方法,用于清除指定存储key的缓存的图片。看另外一个网友的文章,发现可以复写它实现一些其他自定义的清除功能,比如清除指定Activity或者Fragment的图片缓存,当然这样也需要在set时做一些处理。

  • 小结:分析完LruCache,深觉自己需要对已有的知识做一下系统性梳理。打算接下来看看其他几种缓存算法,并分析下使用场景。

你可能感兴趣的:(LruCache - Picasso的实现)