完全解析Andorid的缓存机制LruCache

前言

在Android 2.2以上的sdk中提供了缓存类LruCache。LruCache用于内存缓存,常用的应用场景有很多,比如我们的图片加载库的内存缓存就可以利用LruCache来实现,今天我们一起来学习一下LruCache的源码。

LruCache介绍

LruCache 顾名思义就是使用LRU缓存策略的缓存,那么LRU是什么呢? 最近最少使用到的(least recently used),就是当超出缓存容量的时候,就优先淘汰链表中最近最少使用的那个数据。

讲到LruCache,其实最关键的还是LinkedHashMap。LruCache的源码本身很少,而实现了LRU的功能都是靠LinkedHashMap。所以我们先来一起学习一下LinkedHashMap。

LinkedHashMap

LinkedHashMap是hashmap和链表的结合体,通过链表来记录元素的顺序和链接关系,通过HashMap来存储数据,它可以控制元素的被遍历时候输出的顺序(按照最近访问顺序来排序,还是按插入顺序)。元素被保存在一个双向链表中,默认的遍历顺序是按插入顺序来被遍历的。通过构造函数的accessOrder来控制是否按照访问顺序来遍历。待会我们一起来看看它的构造函数。当以访问顺序来排序的时候,LinkedHashMap总是将最近访问的元素放在队列的尾部,所以第一个元素就是最不经常访问的元素,从而当容量满了的时候就会将第一个元素移除,凭借,get,put,以及putAll都会影响表结构,get的时候需要将,当前被访问的这个对象移动到表的最后面。
首先 LinkHashMap是继承自HashMap的一个类,对于HashMap来说它只是多定义了两个变量如下,当然也是通过这两个变量以及重写HashMap的部分方法来实现,LRU的功能。

/**
   循环链表中一个虚拟的入口,第一个元素是header的next,   
   最后一个元素是header的尾部  
   
   如果map是空的话,那么header.nxt == header && header.prv == header. 
   nxt,prv,表示一个元素的前后元素,相信学过的链表的道友们都会知道这个的吧
   */
  transient LinkedEntry<K, V> header;

  /**
   * True if access ordered, false if insertion ordered.  
   这个是用于控制,外部访问链表的时候,是按什么顺序访问的  ,
   ,true就是按照最近访问来排列元素,从而被遍历,否则就是按照插入顺序来被遍历,当然存储的时候也会根据这个变量来做特殊的处理,   
   true的时候那么就会在访问和删除的时候修改链表的元素的before和next的指针。
   */
  private final boolean accessOrder;

首先一起来看看LinkedHashMap的关键构造方法之一:

  /**
    
    * @param initialCapacity  定义LinkedHashMap的初始容量
    *            the initial capacity of this hash map.
    * @param loadFactor 定义加载因子,用于当容量不够用的时候,扩展容量时,是用初始容量*加载因子等于之后扩展的容量。
    *            the initial load factor.
    * @param accessOrder 
    *            {@code true} if the ordering should be done based on the last
    *            access (from least-recently accessed to most-recently
    *            accessed), and {@code false} if the ordering should be the
    *            order in which the entries were inserted.
    * @throws IllegalArgumentException
    *             when the capacity is less than zero or the load factor is
    *             less or equal to zero.
    */
public LinkedHashMap(
       int initialCapacity, float loadFactor, boolean accessOrder) {
   super(initialCapacity, loadFactor);
   init();
   this.accessOrder = accessOrder;
}

我们可以看到LinkedHashMap的构造方法就是调用了父类构造方法,然后给accessOrder赋值,其中调用了init
(),那接下来我们一起来看看init方法,init方法很简单就是给header赋值了,指向了一个LinkedEntry对象。

       @Override void init() {
        header = new LinkedEntry<K, V>();
    }

LinkedEntry是LinkedHashMap和HashMap的重大区别之一。HashMapEntry的结构如下:

 static class HashMapEntry<K, V> implements Entry<K, V> {
        final K key; //键
        V value;  //值
        final int hash; //该元素对应的hash值
        HashMapEntry<K, V> next; //table数组中下一个元素

而LinkedEntry则多了两个对象,如下:

    LinkedEntry<K, V> nxt;
        LinkedEntry<K, V> prv;

不知道道友们有没有学过c,c里面常用的概念指针和我们Java所指的引用,我觉得概念差不多,都是指向某个对象,这里的nxt,和prv就是指向了相对当前元素来说的前一个元素和后一个元素,从而存储了数据之前的关联关系。
从而实现一个链表结构,通过一个虚拟的头部来链接一个表的头部和尾部从而实现循环链表。

看完了初始化linkedHaspMap,接下来我们就通过他的操作来看看它的其他关键的方法,数据结构主要的操作就是增删查改咯,接下来我就带着各位道友一起来看看,这四个方法首先看增加:

 @Override void addNewEntry(K key, V value, int hash, int index) {
        LinkedEntry<K, V> header = this.header;
    
        // Remove eldest entry if instructed to do so.
        LinkedEntry<K, V> eldest = header.nxt;
        if (eldest != header && removeEldestEntry(eldest)) {
            remove(eldest.key);
        }

        // Create new entry, link it on to list, and put it into table
        LinkedEntry<K, V> oldTail = header.prv;
        LinkedEntry<K, V> newTail = new LinkedEntry<K,V>(
                key, value, hash, table[index], header, oldTail);
        table[index] = oldTail.nxt = header.prv = newTail;
    }

从addNewEntry代码可以看到 ,首先保存了当前链表的头部,然后判断是否需要移除最不常访问的entry,然后将它移除,因为LinkedHashMap是始终将最不常访问的放在头部,所以header.nxt便是就老的entry。 然后创建一个新的entry,记录下当前最近被访问的元素即尾部元素,由于是循环链表所以头部的prv,便是当前最近被访问的元素,然后新插入的元素应该位于尾部,我们假设插入的元素为a, 那么a的prv,便是刚刚记录下来的最近被访问的元素,而nxt这是header元素,从而将entry的nxt,pre调整好之后,插入功能这也就完成了。而这个方法呢是在HashMap的put方法中调用的,LinkedHashMap重写了HashMap的addNewEntry方法。从而完成链表操作。
接下来我们来看看,查询操作get,LinkedHashMap重写了HashMap的get方法,具体代码如下:

    @Override public V get(Object key) {
      /*
       * This method is overridden to eliminate the need for a polymorphic
       * invocation in superclass at the expense of code duplication.
       */
      if (key == null) {
          HashMapEntry<K, V> e = entryForNullKey;
          if (e == null)
              return null;
          if (accessOrder)
              makeTail((LinkedEntry<K, V>) e);
          return e.value;
      }

      int hash = Collections.secondaryHash(key);
      HashMapEntry<K, V>[] tab = table;
      for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
              e != null; e = e.next) {
          K eKey = e.key;
          if (eKey == key || (e.hash == hash && key.equals(eKey))) {
              if (accessOrder)
                  makeTail((LinkedEntry<K, V>) e);
              return e.value;
          }
      }
      return null;
  }

从上面可以看出来,key是null的时候,则使用entryForNullKey,来表示当前被查询到的entry,当然如果存在key,则通过匹配key的引用,或者key的hash相等并且key的值相等来找到对应的entry,找到entry之后他们都做了共同的操作的那就是makeTail,那么makeTail是什么作用呢,接下来我们一起看一下。

 /**
   当accessOrder是true的时候,重新链接传入的entry到链表的尾部。当put 修改entry和,get的时候都会调用这个方法,
   * Relinks the given entry to the tail of the list. Under access ordering,
   * this method is invoked whenever the value of a  pre-existing entry is
   * read by Map.get or modified by Map.put.
   */
  private void makeTail(LinkedEntry<K, V> e) {
      // Unlink e
      e.prv.nxt = e.nxt;
      e.nxt.prv = e.prv;

      // Relink e as tail
      LinkedEntry<K, V> header = this.header;
      LinkedEntry<K, V> oldTail = header.prv;
      e.nxt = header;
      e.prv = oldTail;
      oldTail.nxt = header.prv = e;
      modCount++;
  }

从代码和注释也可以知道,这个方法就是首先将当前被访问的entry从链表中移除,然后将当前entry添加到队列的尾部。这里需要注意的是,频繁使用查改,并不会消耗大量的内存和资源,因为其实引用的常量改变而已,没有造成new新的资源等。

这就是整个get的调用流程。

接下来一起看一下改的逻辑,其实改的逻辑也很简单,首先找到和key相对应的entry,然后修改值,并且调用makeTail方法。

首先一起来看看HashMap的put方法,因为LinkedHashMap的put方法用的就是父类的put方法,只不过是重写了preModify和addNewEntry从而实现了链表的功能呢。

 @Override public V put(K key, V value) {
   
      if (key == null) {
          return putValueForNullKey(value);
      }

      int hash = Collections.secondaryHash(key);
      HashMapEntry<K, V>[] tab = table;
      int index = hash & (tab.length - 1);
       //查找是否已经存在key的entry,如果存在着更新,而不是添加。
      for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
          if (e.hash == hash && key.equals(e.key)) {
              preModify(e);
              V oldValue = e.value;
              e.value = value;
              return oldValue;
          }
      }

      //如果没找到则添加一个新的entry
      // No entry for (non-null) key is present; create one
      modCount++;
      if (size++ > threshold) {
          tab = doubleCapacity();
          index = hash & (tab.length - 1);
      }
      addNewEntry(key, value, hash, index);
      return null;
  }

从上面的代码可以看到,当更改一个元素的时候,调用了preModify方法,这个方法HashMap没有实现,而是由子类实现,linkedHashMap的实现如下:

   @Override void preModify(HashMapEntry<K, V> e) {
       if (accessOrder) {
           makeTail((LinkedEntry<K, V>) e);
       }
   }

可以看到同样是调用了makeTail去使entry位于尾部。
这样看下来,其实最关键的就是将最近访问的元素添加到尾部,如果已经存在,则现将存在的那个entry从列表中删除然后再添加到尾部。

删除也是一样的道理,linkedHashMap通过重写父类的postRemove方法来实现将链表中的entry移除,具体看父类的HashMap和linkedHashMap的代码如下:

    /**
    * Removes the mapping with the specified key from this map.
    *
    * @param key
    *            the key of the mapping to remove.
    * @return the value of the removed mapping or {@code null} if no mapping
    *         for the specified key was found.
    */
   @Override public V remove(Object key) {
       if (key == null) {
           return removeNullKey();
       }
       int hash = Collections.secondaryHash(key);
       HashMapEntry<K, V>[] tab = table;
       int index = hash & (tab.length - 1);
       for (HashMapEntry<K, V> e = tab[index], prev = null;
               e != null; prev = e, e = e.next) {
           if (e.hash == hash && key.equals(e.key)) {
               if (prev == null) {
                   tab[index] = e.next;
               } else {
                   prev.next = e.next;
               }
               modCount++;
               size--;
               postRemove(e);
               return e.value;
           }
       }
       return null;
   }
LinkedHashMap:
   @Override void postRemove(HashMapEntry<K, V> e) {
       LinkedEntry<K, V> le = (LinkedEntry<K, V>) e;
       le.prv.nxt = le.nxt;
       le.nxt.prv = le.prv;
       le.nxt = le.prv = null; // Help the GC (for performance)
   }

从上面可以看到,同样的是先找到对应的entry,然后将调整当前entry的前一个元素的next。然后调用linkedHashMap重写的postRemove方法,然后将当前entry从链表中移除,并且将当前元素的nxt和prv置为null,从而加速GC的回收。
自己绘制了个LinkedHashMap的图,道友们凑合看看。

image

到这里LinkedHashMap就讲解完了,接下来就学习一下LruCache的源码。

首先看到构造方法:

 /**
    * @param maxSize for caches that do not override {@link #sizeOf}, this is
    *     the maximum number of entries in the cache. For all other caches,
    *     this is the maximum sum of the sizes of the entries in this cache.
    */
   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);
   }

通过传入一个maxSize来设置LruCache的最大值,并且穿件了一个LinkedHashMap的成员变量。接下来呢我们一起来看一下,关键的增删查改功能。

  • put 插入更新数据
 public final V put(K key, V value) {
       if (key == null || value == null) {
           throw new NullPointerException("key == null || value == null");
       }

       V previous; //查找是否已经存在key对应的元素
       synchronized (this) {
           putCount++;
           //计算entry的大小
           size += safeSizeOf(key, value); 
           previous = map.put(key, value);
           if (previous != null) {
             //如果之前存在,这先减去之前那个entry所占用的内存大小
               size -= safeSizeOf(key, previous);
           }
       }

       if (previous != null) {
       //如果之前存在则调用entryRemoved回调子类重写的此方法,做一些处理
           entryRemoved(false, key, previous, value);
       }
       //根据最大的容量,计算是否需要淘汰掉最不常使用的entry
       trimToSize(maxSize);
       return previous;
   }

上面的代码都有标注,可以看到对于淘汰策略关键的是trimeToSize这个方法我们一起来看看:

    public 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!");
               }
               //如果当前已经使用的size小于最大容量,那就不做处理直接退出。

               if (size <= maxSize || map.isEmpty()) {
                   break;
               }
               //当超出容量的时候,通过map的iterator,获取第一个数据将其删除,一直循环,知道size小于maxsize为止。
               Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
               key = toEvict.getKey();
               value = toEvict.getValue();
               map.remove(key);
               size -= safeSizeOf(key, value);
               evictionCount++;
           }

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

从上面可以看到,trimSize就是LruCache控制内存大小,并且淘汰掉LRU策略中最不常使用的元素的地方。

  • 查询
    public final V get(K key) {
      if (key == null) {
          throw new NullPointerException("key == null");
      }

      V mapValue;
      //根据key来查询符合条件的etnry
      synchronized (this) {
          mapValue = map.get(key);
          if (mapValue != null) {
              hitCount++;
              return mapValue;
          }
          missCount++;
      }

      /*
       * Attempt to create a value. This may take a long time, and the map
       * may be different when create() returns. If a conflicting value was
       * added to the map while create() was working, we leave that value in
       * the map and release the created value.
       */

      V createdValue = create(key);
      if (createdValue == null) {
          return null;
      }

      synchronized (this) {
          createCount++;
          //mapValue返回的是已经存在相同key的entry
          mapValue = map.put(key, createdValue);

          if (mapValue != null) {
              // There was a conflict so undo that last put
              map.put(key, mapValue);
          } else {
              size += safeSizeOf(key, createdValue);
          }
      }

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

从上面可以看到,当根据key查询不到对应的值的时候,LruCache就会自己创建一个值,因为创建值的同时可能会有其他线程对这个key进行插入作用,所以在创建完之后如果之前有这个key被插入,那么就撤销新创建的值,然后保留之前插入的那个值。如果没有就返回创建的值。LruCache默认是null的,所以如果子类没有复写这个方法的话,那么是不会在查询不到的时候创建默认的值的。一般都不会重写这个方法的。

  • 删除
   public final V remove(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V previous;
        synchronized (this) {
            previous = map.remove(key);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

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

        return previous;
    }

其实删除的方法很简单就调用LinkedHashMap的删除,然后减少内存的占用大小。

除了这些关键方法之外,LruCache也提供了public的方法可以访问,查询命中率以及未命中率等。

至此呢,LruCache和LinkHashMap就讲完了,总结一下,LruCache是限定大小的内部缓存类,利用LinkHashMap实现LRU的缓存策略,而LruCache主要负责缓存大小的控制以及淘汰元素控制,而LinkedHashMap通过链表和HashMap结合负责LRU的实现。

为什么要学习LruCache方便之后讲解图片的缓存策略理解原理,当然其他的缓存需求也可以用这个。今天就学习到这里。 有错误欢迎指出 _

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容