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類相對比較易讀; 願大家都有美好的一天….

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章