LruCache源码解析

分析的LurCache来自于开源库picasso
一、LruCache初始化

  public LruCache(int maxSize) {
    if (maxSize <= 0) {
      throw new IllegalArgumentException("Max size must be positive.");
    }
    this.maxSize = maxSize;
    this.map = new LinkedHashMap<>(0, 0.75f, true);
  }

LruCache初始化很简单,只用传一个maxSize就行了。内部用LinkedHashMap存储值。前两个值是和LinkedHashMap原理有关,一个是LinkedHashMap的Capacity,0.75是负载因子。

//当出现下面情况时,HashMap就会扩容
HashMap.Size >=  Capacity * LoadFactor

注意最后一个值传的true。看看LinkedHashMap构造函数

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

accessOrder的作用是排序,因为LRU算法是按查找顺序排序,这个参数正好排上用场。比如原始Map中值的顺序是ABCD,这时查找AB,那么LinkedHashMap重新排序顺序为CDAB,最后查找的排在list尾部,这样等容量超过LruCache初始化的最大值maxSize时,就可以从list头开始删除。
二、LurCache查找
查找调用get()方法

@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+1,否则missCount+1,有些版本的LruCache还会在没命中时建个新的value插入,这个版本没有,更加简洁。
三、LurCache插入
插入调用set()方法

@Override public void set(@NonNull String key, @NonNull Bitmap bitmap) {
    if (key == null || bitmap == null) {
      throw new NullPointerException("key == null || bitmap == null");
    }
    int addedSize = Utils.getBitmapBytes(bitmap);
    if (addedSize > maxSize) {
      return;
    }
    synchronized (this) {
      putCount++;
      size += addedSize;
      Bitmap previous = map.put(key, bitmap);
      if (previous != null) {
        size -= Utils.getBitmapBytes(previous);
      }
    }
    trimToSize(maxSize);
  }

流程是先取得将要插入图片的大小,如果图片比maxSize都大,就直接返回,没有插入到LinkedHashMap保存(这样搞大图片岂不是删不了?)。接着就算出当前size,并插入到LinkedHashMap,这里注意,如果LinkedHashMap之前有一样的key,那之前一样key的value会被这次的value覆盖掉,所以出现这种情况,就要用当前size 减掉被覆盖的图片大小,得到当前最新的size。这里用同步块,是因为LinkedHashMap不是线程安全的,如果几个线程同时操作,就乱套了。
好了,现在得到当前最新的size了,如果size比maxSize大,就要将最不常用的删除,如果没有就结束。最后一步调用trimToSize就是干这事的,遍历LinkedHashMap,从头开始,一个个判断,每删一个,就和maxSize比较下,直到当前的size小于maxSize,就跳出循环,结束。

private void trimToSize(int maxSize) {
    while (true) {
      String key;
      Bitmap value;
      synchronized (this) {
        if (size < 0 || (map.isEmpty() && size != 0)) {
          throw new IllegalStateException(
              getClass().getName() + ".sizeOf() is reporting inconsistent results!");
        }

        if (size <= maxSize || map.isEmpty()) {
          break;
        }

        Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
        key = toEvict.getKey();
        value = toEvict.getValue();
        map.remove(key);
        size -= Utils.getBitmapBytes(value);
        evictionCount++;
      }
    }
  }

上面提到如果插入的图片比maxSize都大,就直接返回,这样显然不好,于是找了下Glide的LruCache源码看,看看Glide怎么处理这种情况,Glide通过一个回调函数onItemEvicted处理,想怎么处理,用户自己决定。

public Y put(T key, Y item) {
        final int itemSize = getSize(item);
        if (itemSize >= maxSize) {
            onItemEvicted(key, item);//用户自己可以覆盖,原函数是空的
            return null;
        }
        //以下流程都差不多......
    }
   /**
     * A callback called whenever an item is evicted from the cache. Subclasses can override.
     *
     * @param key The key of the evicted item.
     * @param item The evicted item.
     */
    protected void onItemEvicted(T key, Y item) {
        // optional override
    }

最后提下怎么计算图片的大小,算法是这样:

static int getBitmapBytes(Bitmap bitmap) {
    int result = SDK_INT >= KITKAT ? bitmap.getAllocationByteCount() : bitmap.getByteCount();
    if (result < 0) {
      throw new IllegalStateException("Negative size: " + bitmap);
    }
    return result;
  }

在sdk19之后的调用getAllocationByteCount,之前的调用getByteCount。

推荐阅读更多精彩内容