Picasso源码解析之Lrucache算法源码解析

Posted SmallCheric

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Picasso源码解析之Lrucache算法源码解析相关的知识,希望对你有一定的参考价值。

前面的Picasso源码分析中我们看到了Picasso的底层是用到了Lrucache进行缓存,但是并没有深入的解析其原理,今天我们就从源码的角度解析一下Lrucache的缓存原理及工作模式,Let’s Go!

注意:本篇所分析源码基于Picasso下的Lrucache类进行分析

LinkedHashMap特点

我们先来说一下LinkedhashMap的特点:

  • 继承自HashMap,与HashMap的存储结构相同,并且增加了一个双向链表的头结点,将所有put到LinkedHashmap的节点组成了一个双向循环链表,因为它保留了节点插入的顺序,所以在非Lru排序下可以使节点的输出顺序与输入顺序相同。
  • 线程是不安全的,只能在单线程中使用,这点可以在get()set()方法中查检
  • 支持LRU最近最少使用算法

我们先来看一下LinkedHashMap内部的链式结构表,通常画图更能直观的表达代码意思:

  1. 这个图是默认的LinkedHashMap的初始图,有一个头节点不存放数据,真正的entryheader.nxt
  2. 内部结构是一个双链表循环结构,对于数据得增加和删除性能较高,只需改变所指值即可

  1. 这个图是展示了当链表中删除一条数据时候内部链表结构的变化,只是更改了指向值

Lrucache源码分析

我们再回顾一下Picasso初始化时候的源码:

 public Picasso build() 

      //在这里进行Lrucache的初始化
      if (cache == null) 
        cache = new LruCache(context);
      

我们再来看一下Lrucache初始化时的操作


  /** Create a cache using an appropriate portion of the available RAM as the maximum size. */
  public LruCache(Context context) 
   //调用本地LruCache(int maxSize)方法
    this(Utils.calculateMemoryCacheSize(context));
  

  /** Create a cache with a given maximum size in bytes. */
  public LruCache(int maxSize) 
    if (maxSize <= 0) 
      throw new IllegalArgumentException("Max size must be positive.");
    
    this.maxSize = maxSize;
    this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
  

根据代码我们可以得到以下几点:

  1. 缓存的大小是可以自定义的,我们在Picasso的源码也分析到的缓存值为可用内存的15%
  2. 内部使用了LinkedHashMap进行实现,而且有一个比较重要的方法LinkedHashMap(
    int initialCapacity, float loadFactor, boolean accessOrder)
    ,我们就来着重看一下这个方法内部所做的操作
/**
* Constructs a new @code LinkedHashMap instance with the *specified capacity, *load factor and a flag specifying the *ordering behavior.
*
* @param initialCapacity
*            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;
    

我们现在来看一下该方法中三个参数的作用:

  1. initialCapacity - hash map的初始容量大小,这个比较容易理解
  2. accessOrder - 初始加载因子,我们下面会详细分析
  3. accessOrder - 排序方式,如果为true就按最近使用排序,如果为false就按插入顺序排序,我们下面也会进行详细分析

accessOrder加载因子解析

我们从该构造方法中,发现传进来的initialCapacityloadFactor又被传入了super(initialCapacity, loadFactor);中,我们现在跟进去看父类HashMap看都做了什么操作

 /**
     * Constructs a new @code HashMap instance with the specified capacity and
     * load factor.
     *
     * @param capacity
     *            the initial capacity of this hash map.
     * @param loadFactor
     *            the initial load factor.
     * @throws IllegalArgumentException
     *                when the capacity is less than zero or the load factor is
     *                less or equal to zero or NaN.
     */
    public HashMap(int capacity, float loadFactor) 
        this(capacity);
        //只是在这里进行了是否合法判断
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) 
            throw new IllegalArgumentException("Load factor: " + loadFactor);
        

        /*
         * Note that this implementation ignores loadFactor; it always uses
         * a load factor of 3/4. This simplifies the code and generally
         * improves performance.
         */
    

结果令人大跌眼镜,这个loadFactor只是判断了一下是否合法,其他并没有什么卵用,我们注意到下方有一个提示:当前的接口实现已经忽略了loadFactor,这个值默认为 3/4, 这样的话能简化代码并能提高性能; 这个值也跟Picasso传过来的0.75是相对应的,但是我们还是对这个值的作用一知半解,进行全局搜索后,只有一个地方用到了loadFactor,是作为一个key值存入一个map中,而键值为默认的0.75,我们可以看下源码:

    /**
     * The default load factor. Note that this implementation ignores the
     * load factor, but cannot do away with it entirely because it's
     * mentioned in the API.
     *
     * <p>Note that this constant has no impact on the behavior of the program,
     * but it is emitted as part of the serialized form. The load factor of
     * .75 is hardwired into the program, which uses cheap shifts in place of
     * expensive division.
     */
    static final float DEFAULT_LOAD_FACTOR = .75F;

   private void writeObject(ObjectOutputStream stream) throws IOException 
        // Emulate loadFactor field for other implementations to read
        ObjectOutputStream.PutField fields = stream.putFields();
        fields.put("loadFactor", DEFAULT_LOAD_FACTOR);
        stream.writeFields();

        stream.writeInt(table.length); // Capacity
        stream.writeInt(size);
        for (Entry<K, V> e : entrySet()) 
            stream.writeObject(e.getKey());
            stream.writeObject(e.getValue());
        
    

然后我们来看一下官方是怎么解释这个DEFAULT_LOAD_FACTOR值的:这个接口忽略了当前这个loadFactor,但是并不能把它删除,因为在API中提到了它; 这个值对程序的运行没有任何的影响,但是作为序列化的一部分,如果将值强制设置为0.75,就可以提高程序的整体性能;解释了这么多我们依然不能理解这个值的作用,但是作为一个老司机,Let me read the Fucking Source Code ,我们从官方文档中去找,谷歌开发者网站解释和源码中一致没有什么可参考,这岂能甘心呢. 我又去了StacOverFloworacle的官网去找,二者都对super(initialCapacity, loadFactor);给出了同一个答案:

An instance of HashMap has two parameters that affect its performance: initial capacity and load factor. The capacity is the number of buckets in the hash table, and the initial capacity is simply the capacity at the time the hash table is created. 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.

我们来解释一下上面两段话的意思

  • 第一段:这是有两个参数的创建实例的构造方法,初始容量值和加载因子. initialCapacity是哈希表的容量值大小,这个初始值也仅仅是在哈希表在创建时的容量值;loadFactor是衡量允许哈希表在自增长时能获取最大增长的因素. 当在哈希表中的实体数量超过了加载因子与哈希表的乘积的值后,这个哈希表将会被重新处理(也就是说,内部数据结构将被重建),所以哈希表的大小将被扩大到原来的2倍大小;
  • 第二段:作为一个自增长的约束,0.75这个值很好的权衡了时间和空间之间的关系,当加载因子的值越大,空间耗费就会变小,但同时会增加会增加查找成本(比如get set操作),为了尽量减少哈希表的重构,在设置初始容量值时就应该考虑到预期的实体数量及加载因子值.如果初始值大于实体的数量除以加载因子的值,就从来不会发生哈希表重构.

总结: 虽然不知道0.75这个值是怎么来的,是大量测试还是某种算法实现,无从考究,但是可以确定的是,这个传入的0.75并没有对其进行过多的操作与处理,可能该处的代码是一个人写但可能有其他人来调用了,所以后期如果将这个构造方法的loadFactor删除或者更改的话可能会影响到别人或者考虑到代码的稳定性,就一直这样吧.

accessOrder排序参数

我们知道,如果该值为true,则LinkedHashMap是按照最常访问模式进行排序,如果为false,则按插入顺序排序,那我们就看看这里面到底做了哪些操作,而实现的不同的排序方式,通过全局查找,发现在get(Object key)方法及preModify(HashMapEntry<K, V> e)方法中用到了accessOrder参数的地方,而且都调用了同一个makeTail((LinkedEntry<K, V>) e);方法:

 if (accessOrder)
      makeTail((LinkedEntry<K, V>) e);

看来核心的操作都在这个makeTail()中,但是为了让我们更好的理解这个方法,我们先对LinkedEntry做一个简单的了解,让我们继续….

/**
 * A dummy entry in the circular linked list of entries in the map.
 * The first real entry is header.nxt, and the last is header.prv.
 * If the map is empty, header.nxt == header && header.prv == header.
 */
transient LinkedEntry<K, V> header;

/**
 * LinkedEntry adds nxt/prv double-links to plain HashMapEntry.
 */
static class LinkedEntry<K, V> extends HashMapEntry<K, V> 
    LinkedEntry<K, V> nxt;
    LinkedEntry<K, V> prv;

    /** Create the header entry */
    LinkedEntry() 
        super(null, null, 0, null);
        nxt = prv = this;
    

    /** Create a normal entry */
    LinkedEntry(K key, V value, int hash, HashMapEntry<K, V> next,
                LinkedEntry<K, V> nxt, LinkedEntry<K, V> prv) 
        super(key, value, hash, next);
        this.nxt = nxt;
        this.prv = prv;
    

这个实例对象主要有以下几点作用:

  1. 定义的header对象在map的链表循环中是一个虚拟的实体类,头节点本身不保存数据,头节点的下一个节点才开始保存数据;第一位真正的实体类是header.nxt,上一位是header.prv,如果map为空,则header.nxt == header && header.prv == header,也就是说链表中的首位实体为header,这个类在调用init()方法时被初始化;
  2. 该类继承自HashMapEntry,增加了 nex/prv 双链表到一个简单的HashMapEntry实体类;
    1. 无参构造 - 调用init()初始化时创建一个header实体,是对 nxtprv 赋值,都为当前LinkedHashMap对象自身
    2. 有参构造 - 创建一个标准的实体,同时将传入的nxtprv 分别进行赋值

OK,对LinkedEntry有了一定得了解后,我们现在看一下期待已久的makeTail()方法的真面目:

/**
 * 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++;

LinkedHashMap作为一个双向循环链表结构体,这个方法就是将传入的实体重新链接到链表的最后位置,这样的结果就是不管用户是调用get()还是put()都会将操作的那个实体放在链表的最后位置,那么最不常用的就会放在最首位的下一个节点,对应的实体为header.nex,也就是放在header的下一节点. 我们现在来逐一解读mailTail()的两段逻辑:

// Unlink e
e.prv.nxt = e.nxt;
e.nxt.prv = e.prv;

这两行代码的意思就是将e从链表中先剔除出来,改变e前一个(pre)及e下一个(pre)节点的指向值;我们将上面的代码也可以这样写:

e2 = e.nxt;//e所指向的下一个为e2
e1 = e.prv;//e所指向的上一个为e1
e1.nxt = e2;//此时e1的下一个原来为e,现在跳过e,指向了e2
e2.prv = e1;//此时e2的上一个原来的上一个为e,现在跳过e,指向了e1

我们在来看将e链接到链表的尾部,以实现最近使用的都在末端,最少使用的在最首位:

// Relink e as tail
//拿到当前的header进行赋值,该header为链表中的头字节,不存放数据,只作为引用使用
LinkedEntry<K, V> header = this.header;
//获取当前链表中的最后一个字节,因为链表是循环的,所以头字节的上一个就是链表中的最后一个字节
LinkedEntry<K, V> oldTail = header.prv;
//将e的下一个指向header,说明此时已经在最末尾了
e.nxt = header;
//将e的上一个指向之前的链表中的最末尾的字节oldTail,说明现在e已经排在了链表的最末尾
e.prv = oldTail;
//所以此时如果e作为了最末尾字节,那oldTail的下一个和header的上一个都会同时指向e
oldTail.nxt = header.prv = e;

我们分析到这里的时候,大家应该已经对Lrucache的构造方法有了一个简单的认识了, 现在让我们继续分析一下我们最常用的get()set()方法的源码实现:

/**
 * Returns the value of the mapping with the specified key.
 *
 * @param key
 *            the key.
 * @return the value of the mapping with the specified key, or @code null
 *         if no mapping for the specified key is found.
 */
@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.
     */
    //当key为null时的情况
    if (key == null) 
        //此处e的内容为调用addNewEntryForNullKey(value)所设置进去
        HashMapEntry<K, V> e = entryForNullKey;
        //如果value也为null,就直接返回null
        if (e == null)
            return null;
        //是否需要Lrucache排序,true的话,这时就会把e放在了链表的最尾端
        if (accessOrder)
            makeTail((LinkedEntry<K, V>) e);
        //然后返回值,如果需要最近最少排序,就会将使用过的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;

注意:get()方法为LinkedhashMap中的实现,在Lrucache类中,其get()核心代码也是调取的该方法;

我们现在来看一下Lrucache类中的set()方法:

//因为Picasso是用于加载图片,所以传入的value值为bitmap
@Override 
public void set(String key, Bitmap bitmap) 
//先进行非空判断
if (key == null || bitmap == null) 
  throw new NullPointerException("key == null || bitmap == null");


Bitmap previous;
//因是非安全的,所以需要加同步锁
synchronized (this) 
  putCount++;
  //计算目前内存中的size值
  size += Utils.getBitmapBytes(bitmap);
  //调用父类的put方法将数据放到链表的末尾
  previous = map.put(key, bitmap);
  if (previous != null) 
    size -= Utils.getBitmapBytes(previous);
  

//这里我们要注意,这个方法就是在计算是否需要清除最不常用的键值对,以保证缓存小于最大缓存值
trimToSize(maxSize);

我们现在来看一下trimToSize(maxSize);的源码

 private void trimToSize(int maxSize) 
    //循环判断
    while (true) 
      String key;
      Bitmap value;
      //添加同步锁
      synchronized (this) 
        //如果size小于0,即map为null或者map为空(非null)但是size不等于0,就抛出异常
        if (size < 0 || (map.isEmpty() && size != 0)) 
          throw new IllegalStateException(
              getClass().getName() + ".sizeOf() is reporting inconsistent results!");
        
        //如果size缓存值小于了最大缓存值或者map为空(非null),当然满足就跳出循环
        if (size <= maxSize || map.isEmpty()) 
          break;
        
        //找出map集合中最不常使用的,因为排在了首位
        Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
        key = toEvict.getKey();
        value = toEvict.getValue();
        //将最不常用的移除
        map.remove(key);
        size -= Utils.getBitmapBytes(value);
        evictionCount++;
      
    
  

ok,到现在为止,我们已经对Lrucache的缓存算法有了一定得了解了,这里解析的是Picasso的Lrucache类,而非官方的Lrucache类,可能会有所不同,但大致逻辑都是一样的,而且这个Picasso的Lrucache类相对比较易读; 愿大家都有美好的一天….

以上是关于Picasso源码解析之Lrucache算法源码解析的主要内容,如果未能解决你的问题,请参考以下文章

LruCache源码解析

Android源码解析——LruCache

Picasso源码解析

Android LruCache 源码分析

Android LruCache 源码分析

Picasso源码解析