Android記憶體快取LruCache原始碼解析

_小馬快跑_發表於2017-12-15

LruCache 作為記憶體快取,使用強引用方式快取有限個資料,當快取的某個資料被訪問時,它就會被移動到佇列的頭部,當一個新資料要新增到LruCache而此時快取大小要滿時,隊尾的資料就有可能會被垃圾回收器(GC)回收掉,LruCache使用的LRU(Least Recently Used)演算法,即:把最近最少使用的資料從佇列中移除,把記憶體分配給最新進入的資料。

  • 如果LruCache快取的某條資料明確地需要被釋放,可以覆寫entryRemoved(boolean evicted, K key, V oldValue, V newValue)

  • 如果LruCache快取的某條資料通過key沒有找到,可以覆寫 create(K key),這簡化了呼叫程式碼,即使錯過一個快取資料,也不會返回null,而會返回通過create(K key)創造的資料。

  • 如果想限制每條資料的快取大小,可以覆寫sizeOf(K key, V value),如下面設定bitmap最大快取大小是4MB:

 int cacheSize = 4 * 1024 * 1024; // 4MiB
 LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
     protected int sizeOf(String key, Bitmap value) {
         return value.getByteCount();
     }
 }
複製程式碼

LruCache這個類是執行緒安全的。自動地執行多個快取操作通過synchronized 同步快取:

 synchronized (cache) {
   if (cache.get(key) == null) {
       cache.put(key, value);
   }
 }
複製程式碼

LruCache不允許null作為一個key或value,get(K)、put(K,V),remove(K)中如果key或value為null,都會丟擲異常:throw new NullPointerException("key == null || value == null"),LruCache是在Android 3.1 (Honeycomb MR1)有的,如果想在3.1之前使用,可以使用support-v4相容庫。

####LruCache常用方法

方法 備註
void resize(int maxSize) 更新儲存大小
V put(K key, V value) 存資料,返回之前key對應的value,如果沒有,返回null
V get(K key) 取出key對應的快取資料
V remove(K key) 移除key對應的value
void evictAll() 清空快取資料
Map<K, V> snapshot() 複製一份快取並返回,順序從最近最少訪問到最多訪問排序

####LruCache的使用

  • ######初始化:
 private Bitmap bitmap;
 private String STRING_KEY = "data_string";
 private LruCache<String, Bitmap> lruCache;
 private static final int CACHE_SIZE = 10 * 1024 * 1024;//10M

 lruCache = new LruCache<String, Bitmap>(CACHE_SIZE) {
    @Override
    protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
       super.entryRemoved(evicted, key, oldValue, newValue);
    }

    @Override
    protected int sizeOf(String key, Bitmap value) {
        //這裡返回的大小用單位kb來表示的
        return value.getByteCount() / 1024;
    }
};
複製程式碼

最大快取設定為10M,key是String型別,value設定的是Bitmap

  • ######存資料:
  if(lruCache!=null){
     lruCache.put(STRING_KEY, bitmap);
   }
複製程式碼
  • ######取資料:
 if (lruCache != null) {
     bitmap = lruCache.get(STRING_KEY);
  }
複製程式碼
  • ######清除快取:
 if (lruCache != null) {
     lruCache.evictAll();
   }
複製程式碼

####LruCache原始碼分析

  • ######定義變數、構造器初始化:
  private final LinkedHashMap<K, V> map;//使用LinkedHashMap來儲存運算元據
    /** Size of this cache in units. Not necessarily the number of elements. */
    private int size;//快取資料大小,不一定等於元素的個數
    private int maxSize;//最大快取大小

    private int putCount;// 成功新增資料的次數
    private int createCount;//手動建立快取資料的次數
    private int evictionCount;//成功回收資料的次數
    private int hitCount;//查詢資料時命中次數
    private int missCount;//查詢資料時未命中次數

    /**
     * @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);
    }
複製程式碼

建構函式中初始化了LinkedHashMap,引數maxSize指定了快取的最大大小。

  • ######修改快取大小:
/**
 * Sets the size of the cache.
 *
 * @param maxSize The new maximum size.
 */
public void resize(int maxSize) {
    if (maxSize <= 0) {
        throw new IllegalArgumentException("maxSize <= 0");
    }
    synchronized (this) {
        this.maxSize = maxSize;
    }
    trimToSize(maxSize);
}
複製程式碼

resize(int maxSize)用來更新大小,先是加同步鎖更新maxSize的值,接著呼叫了trimToSize()方法:

/**
 * Remove the eldest entries until the total of remaining entries is at or
 * below the requested size.
 *
 * @param maxSize the maximum size of the cache before returning. May be -1
 *            to evict even 0-sized elements.
 */
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!");
            }
            //如果已經小於最大快取,則無需接著往下執行了
            if (size <= maxSize) {
                break;
            }
            //拿到最近最少使用的那條資料
            Map.Entry<K, V> toEvict = map.eldest();
            if (toEvict == null) {
                break;
            }

            key = toEvict.getKey();
            value = toEvict.getValue();
            //從LinkedHashMap移除這條最少使用的資料
            map.remove(key);
            //快取大小size減去移除資料的大小,如果沒有覆寫sizeOf,則減去的值是1
            size -= safeSizeOf(key, value);
            evictionCount++;
        }

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

private int safeSizeOf(K key, V value) {
   int result = sizeOf(key, value);
    if (result < 0) {
        throw new IllegalStateException("Negative size: " + key + "=" + value);
    }
    return result;
}

/**
 * Returns the size of the entry for {@code key} and {@code value} in
 * user-defined units.  The default implementation returns 1 so that size
 * is the number of entries and max size is the maximum number of entries.
 *
 * <p>An entry's size must not change while it is in the cache.
 */
 protected int sizeOf(K key, V value) {
     return 1;
 }

protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
複製程式碼

trimToSize() 迴圈移除最近最少使用的資料直到剩餘快取資料的大小等於小於最大快取大小。 注:我們看到sizeOf()和entryRemoved()都是protected來修飾的,即可以被覆寫,如果sizeOf()沒有被覆寫,那麼變數size 代表的是快取資料的數量,maxSize代表的是最大數量,如果覆寫sizeOf(),如:

 @Override
  protected int sizeOf(String key, BitmapDrawable value) {
      return value.getBitmap().getByteCount() / 1024;
  }
複製程式碼

此時size 代表的是快取資料的大小,maxSize代表的是最大快取大小。

  • #####存資料:
/**
 * Caches {@code value} for {@code key}. The value is moved to the head of
 * the queue.
 *
 * @return the previous value mapped by {@code key}.
 */
public final V put(K key, V value) {
    //key或value為空直接拋異常
    if (key == null || value == null) {
        throw new NullPointerException("key == null || value == null");
    }

    V previous;
    synchronized (this) {
        //新增資料次數+1
        putCount++;
         //快取資料的大小增加
        size += safeSizeOf(key, value);
         //新增快取資料,新增之前如果key值對應的value不為空,則newValue會覆蓋oldValue,並返回oldValue;
         //如果key值對應的value為空,則返回Null
        previous = map.put(key, value);
        if (previous != null) {
           //之前的oldValue不為空,則在快取size中減去oldValue
            size -= safeSizeOf(key, previous);
        }
    }

    if (previous != null) {
        entryRemoved(false, key, previous, value);
    }
    //重新檢查快取大小
    trimToSize(maxSize);
    return previous;
}
複製程式碼

呼叫LinkedHashMap的put(key, value)新增快取資料時,在新增之前如果key值對應的value不為空,則newValue會覆蓋oldValue,並返回oldValue;如果key值對應的value為空,則返回null,接著根據返回值來重新設定快取size和最大快取maxSize的大小。

  • #####取資料:
 /**
  * Returns the value for {@code key} if it exists in the cache or can be
  * created by {@code #create}. If a value was returned, it is moved to the
  * head of the queue. This returns null if a value is not cached and cannot
  * be created.
  */
 public final V get(K key) {
     if (key == null) {
         throw new NullPointerException("key == null");
     }

     V mapValue;
     synchronized (this) {
         //通過key取資料
         mapValue = map.get(key);
         if (mapValue != null) {
             //如果取到了資料,命中次數+1
             hitCount++;
             return mapValue;
         }
         //沒有取到資料,未命中此時+1
         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.
      */
     //如果沒有覆寫create(),預設create()方法返回的null
     V createdValue = create(key);    
     if (createdValue == null) {
         return null;
     }
     //如果覆寫了create(),即根據key值手動創造了value,則繼續往下執行
     synchronized (this) {
          //創造資料次數+1
         createCount++;
         //嘗試將資料新增到快取中
         mapValue = map.put(key, createdValue);
         
         if (mapValue != null) {
             // There was a conflict so undo that last put
             //mapValue不為空,說明之前的key對應的是有資料的,那麼就跟我們手動建立的資料衝突了,
             //所以執行撤消操作,重新把mapValue新增到快取中,用mapValue去覆蓋createdValue
             map.put(key, mapValue);
         } else {
             //如果mapValue為空,說明之前的key值對應的value確實為空,我們手動新增createdValue後,
             //需要重新計算快取size的大小
             size += safeSizeOf(key, createdValue);
         }
     }

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

 protected V create(K key) {
      return null;
  }
複製程式碼

取資料的流程大致是這樣: 1、首先通過map.get(key)來嘗試取出value,如果value存在,則直接返回value; 2、如果value不存在,則執行下面的create(key),如果create()沒有被覆寫,則直接返回null; 3、如果create()被覆寫了,即通過key值建立了一個createdValue,那麼嘗試通過mapValue = map.put(key, createdValue)把createdValue新增到快取中去,mapValue是put()方法的返回值,mapValue代表的是key之前對應的值,如果mapValue不為空,說明之前的key對應的是有資料的,那麼就跟我們手動建立的資料衝突了,所以執行撤消操作,重新把mapValue新增到快取中,用mapValue去覆蓋createdValue,最後再重新計算快取大小。

  • #####移除資料:
 /**
  * Removes the entry for {@code key} if it exists.
  *
  * @return the previous value mapped by {@code key}.
  */
 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;
 }
複製程式碼

呼叫map.remove(key)通過key值刪除快取中key對應的value,然後重新計算快取大小,並返回刪除的value。

  • #####其他一些方法:
  //清空快取
 public final void evictAll() {
      trimToSize(-1); // -1 will evict 0-sized elements
  }

 public synchronized final int size() {
     return size;
 }

 public synchronized final int maxSize() {
      return maxSize;
 }

 public synchronized final int hitCount() {
     return hitCount;
 }

 public synchronized final int missCount() {
     return missCount;
 }

public synchronized final int createCount() {
     return createCount;
 }

 public synchronized final int putCount() {
     return putCount;
 }

 public synchronized final int evictionCount() {
     return evictionCount;
 }

 /**
  * Returns a copy of the current contents of the cache, ordered from least
  * recently accessed to most recently accessed.
  */
  //複製一份快取,順序從最近最少訪問到最多訪問排序
 public synchronized final Map<K, V> snapshot() {
     return new LinkedHashMap<K, V>(map);
 }
複製程式碼

相關文章