【Android 开发】深入理解内存缓存类LruCache:源码分析

来源:互联网 发布:cocostudio 1.6 mac 编辑:程序博客网 时间:2024/05/21 04:02

1.   原理

    LruCache是Android API实现的用于内存缓存的类,LRU即“Leatestrecent used”的缩写,典型应用是集中展示多个BitMap。我在《现代操作系统》一书中第3章第4节的“页面置换算法”中看到过该算法,比较简单。

    LruCache内部依赖于队列(LinkedHashMap<K,V>方式实现了队列)。最近使用的对象位于队尾,最不常使用的位于队头。每一个对象入队,都计算整个队列是否超过最大内存空间,如果超过,就从队头将对象出队,直到队列空间小于最大存储空间。

    LruCache是线程安全的。

2.   源码分析

2.1初始化

    首先看LruCache的8个成员变量,其中map就是队列的一种实现,LruCache的所有操作都是围绕map的存取进行的。

private finalLinkedHashMap<K, V> map;  //链表存储的Key-Value   /** Size of this cache in units. Not necessarily the number of elements.*/   private int size;   //按用户指定的单位(Byte、MByte……)计算的累计缓存。   private int maxSize; //最大缓存    private int putCount;  //入队的对象,计数   private int createCount;  //通过key没有查找到value,然后根据key生成了value,计数   private int evictionCount;  //由于加入新的对象超过缓存区大小而被踢出的对象个数   private int hitCount;   //通过key查找到了value,计数   private int miscount; //通过key没有查找到value,计数

    LruCache有唯一的构造函数,将map进行初始化。初始化时map的容量为0,负载因子为0.75。负载因子是哈希表的一个术语,表明要存储的对象个数与map容量的最大比值。负载因子0.75,表明每个map的位置可以存储0.75个对象。负载因子小于1,可以减少哈希表的冲突现象。

/**     * @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);    }

    初始化时,必须同时指定以何种单位来计算LruCache的内存大小,默认是以key-value的个数为单位,显然不是内存大小。

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

    例如,下面就是一种初始化方式,以字节为单位计算缓存。

int cacheSize = 4 * 1024 * 1024; // 4MiB    LruCache bitmapCache = new LruCache(cacheSize) {        protected int sizeOf(String key, Bitmap value) {            return value.getByteCount(); //除以1024可以换位以MB为单位计算缓存           }}

    除了在初始化时指定缓存大小,LruCache也可以在初始化之后指定缓存大小。

/**     * Sets the size of the cache.     * @param maxSize The new maximum size.     *     * @hide     */    public void resize(int maxSize) {        if (maxSize <= 0) {            throw new IllegalArgumentException("maxSize <= 0");        }        synchronized (this) {            this.maxSize = maxSize;        }        trimToSize(maxSize);  //根据当前的缓存容量,调整map小于容量    }

2.2 Key-value入队

  Key-value入队的过程主要是查看该key是否已经在map中存储有值(冲突检测),没有的话就直接添加。否则,就让新的value替换key对应的旧的value。

/**     * 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) {        if (key == null || value == null) {            throw new NullPointerException("key == null || value == null");        }        V previous;        synchronized (this) {            putCount++;  //入队对象计数            size += safeSizeOf(key, value);  //累计缓存大小            previous = map.put(key, value); //加入的key是否已经有匹配            if (previous != null) {  //有匹配                size -= safeSizeOf(key, previous); //容量减去之前能匹配key的对象            }        }        if (previous != null) {            entryRemoved(false, key, previous, value);  //去除之前的匹配的value        }        trimToSize(maxSize);  //调整缓存        return previous;}

    用来调整缓存大小的函数是trimToSize(maxSize),这里是判断当前缓存是否越界,如果越界就持续去除队头对象,直到满足条件。

    /**     * 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();//队头的key-value                if (toEvict == null) {                    break;                }                key = toEvict.getKey();                value = toEvict.getValue();                map.remove(key);                size -= safeSizeOf(key, value);                evictionCount++;            }            entryRemoved(true, key, value, null);        }

   另外在入队和调整队列大小的时候都用到了一个函数,即entryRemoved(true, key, value, null),这是一个空的用于继承的函数,这些踢出缓存的key-value如果要再进行处理,可以继承该方法。

    /**     * Called for entries that have been evicted or removed. This method is     * invoked when a value is evicted to make space, removed by a call to     * {@link #remove}, or replaced by a call to {@link #put}. The default     * implementation does nothing.     *     * <p>The method is called without synchronization: other threads may     * access the cache while this method is executing.     *     * @param evicted true if the entry is being removed to make space, false     *     if the removal was caused by a {@link #put} or {@link #remove}.     * @param newValue the new value for {@code key}, if it exists. If non-null,     *     this removal was caused by a {@link #put}. Otherwise it was caused by     *     an eviction or a {@link #remove}.     */    protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}

    例如,对于BitMap来讲,暂时不必将这些从缓存中踢出的对象从内存中擦除,可以将这些缓冲区踢出的对象放到软引用集合中,生成新的BitMap时可以查找一下这个软引用队列是否有可用的旧的BitMap内存空间,有的话就将新的BitMap覆盖到旧的BitMap内存空间。这样,就节省了一次旧的BitMap的GC过程。例如,下面在初始化LruCache时就复写了该函数,将踢出缓存的对象放到了软引用集合中。

public class ImageCache {    private LruCache<String, BitmapDrawable> mMemoryCache;    private Set<SoftReference<Bitmap>> mReusableBitmaps;……省略无关内容……  mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {                /**                 * Notify the removed entry that is no longer being cached                 */                @Override                protected void entryRemoved(boolean evicted, String key,                        BitmapDrawable oldValue, BitmapDrawable newValue) {                    if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {                        // The removed entry is a recycling drawable, so notify it                        // that it has been removed from the memory cache                        ((RecyclingBitmapDrawable) oldValue).setIsCached(false);                    } else {                        // The removed entry is a standard BitmapDrawable                        if (Utils.hasHoneycomb()) {                            // We're running on Honeycomb or later, so add the bitmap                            // to a SoftReference set for possible use with inBitmap later                            mReusableBitmaps.add(new SoftReference<Bitmap>(oldValue.getBitmap()));                        }                    }                }                /**                 * Measure item size in kilobytes rather than units which is more practical                 * for a bitmap cache                 */                @Override                protected int sizeOf(String key, BitmapDrawable value) {                    final int bitmapSize = getBitmapSize(value) / 1024;                    return bitmapSize == 0 ? 1 : bitmapSize;                }            };        }……省略无关内容……}

2.3Key-value查找

    查找的过程主要考察没有找到的情况。LruCache提供了可继承的函数,看用户是否要根据key生成一个value。如果既没找到,用户也没有提供重写key生成value的函数,那就返回null。

    /**     * 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) {            mapValue = map.get(key);            if (mapValue != null) {  //查找到value                hitCount++;  //计数                return mapValue;            }            missCount++;  //没有查找到value,计数        }        /*         * 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);  //没有找到的情况,看用户是否根据key生成value        if (createdValue == null) {  //用户不生成value            return null;        }        synchronized (this) {            createCount++;  //没有找到,用户根据key生成了value,计数            mapValue = map.put(key, createdValue); //将生成的key-value入队            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生成value的过程如下,默认是null,可以继承覆写。
/**     * Called after a cache miss to compute a value for the corresponding key.     * Returns the computed value or null if no value can be computed. The     * default implementation returns null.     *     * <p>The method is called without synchronization: other threads may     * access the cache while this method is executing.     *     * <p>If a value for {@code key} exists in the cache when this method     * returns, the created value will be released with {@link #entryRemoved}     * and discarded. This can occur when multiple threads request the same key     * at the same time (causing multiple values to be created), or when one     * thread calls {@link #put} while another is creating a value for the same     * key.     */    protected V create(K key) {        return null;    }

2.4缓存快照

  可以获得缓存的拷贝。

  /**     * 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);<pre name="code" class="java">  }

附录-完整源码

package android.util;import java.util.LinkedHashMap;import java.util.Map;/** * A cache that holds strong references to a limited number of values. Each time * a value is accessed, it is moved to the head of a queue. When a value is * added to a full cache, the value at the end of that queue is evicted and may * become eligible for garbage collection. * * <p>If your cached values hold resources that need to be explicitly released, * override {@link #entryRemoved}. * * <p>If a cache miss should be computed on demand for the corresponding keys, * override {@link #create}. This simplifies the calling code, allowing it to * assume a value will always be returned, even when there's a cache miss. * * <p>By default, the cache size is measured in the number of entries. Override * {@link #sizeOf} to size the cache in different units. For example, this cache * is limited to 4MiB of bitmaps: * <pre>   {@code *   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(); *       } *   }}</pre> * * <p>This class is thread-safe. Perform multiple cache operations atomically by * synchronizing on the cache: <pre>   {@code *   synchronized (cache) { *     if (cache.get(key) == null) { *         cache.put(key, value); *     } *   }}</pre> * * <p>This class does not allow null to be used as a key or value. A return * value of null from {@link #get}, {@link #put} or {@link #remove} is * unambiguous: the key was not in the cache. * * <p>This class appeared in Android 3.1 (Honeycomb MR1); it's available as part * of <a href="http://developer.android.com/sdk/compatibility-library.html">Android's * Support Package</a> for earlier releases. */public class LruCache<K, V> {    private final LinkedHashMap<K, V> map;    /** 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);    }    /**     * Sets the size of the cache.     * @param maxSize The new maximum size.     *     * @hide     */    public void resize(int maxSize) {        if (maxSize <= 0) {            throw new IllegalArgumentException("maxSize <= 0");        }        synchronized (this) {            this.maxSize = maxSize;        }        trimToSize(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) {            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 = 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;        }    }    /**     * 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) {        if (key == null || value == null) {            throw new NullPointerException("key == null || value == null");        }        V previous;        synchronized (this) {            putCount++;            size += safeSizeOf(key, value);            previous = map.put(key, value);            if (previous != null) {                size -= safeSizeOf(key, previous);            }        }        if (previous != null) {            entryRemoved(false, key, previous, value);        }        trimToSize(maxSize);        return previous;    }    /**     * 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();                map.remove(key);                size -= safeSizeOf(key, value);                evictionCount++;            }            entryRemoved(true, key, value, null);        }    }    /**     * 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;    }    /**     * Called for entries that have been evicted or removed. This method is     * invoked when a value is evicted to make space, removed by a call to     * {@link #remove}, or replaced by a call to {@link #put}. The default     * implementation does nothing.     *     * <p>The method is called without synchronization: other threads may     * access the cache while this method is executing.     *     * @param evicted true if the entry is being removed to make space, false     *     if the removal was caused by a {@link #put} or {@link #remove}.     * @param newValue the new value for {@code key}, if it exists. If non-null,     *     this removal was caused by a {@link #put}. Otherwise it was caused by     *     an eviction or a {@link #remove}.     */    protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}    /**     * Called after a cache miss to compute a value for the corresponding key.     * Returns the computed value or null if no value can be computed. The     * default implementation returns null.     *     * <p>The method is called without synchronization: other threads may     * access the cache while this method is executing.     *     * <p>If a value for {@code key} exists in the cache when this method     * returns, the created value will be released with {@link #entryRemoved}     * and discarded. This can occur when multiple threads request the same key     * at the same time (causing multiple values to be created), or when one     * thread calls {@link #put} while another is creating a value for the same     * key.     */    protected V create(K key) {        return 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;    }    /**     * Clear the cache, calling {@link #entryRemoved} on each removed entry.     */    public final void evictAll() {        trimToSize(-1); // -1 will evict 0-sized elements    }    /**     * For caches that do not override {@link #sizeOf}, this returns the number     * of entries in the cache. For all other caches, this returns the sum of     * the sizes of the entries in this cache.     */    public synchronized final int size() {        return size;    }    /**     * For caches that do not override {@link #sizeOf}, this returns the maximum     * number of entries in the cache. For all other caches, this returns the     * maximum sum of the sizes of the entries in this cache.     */    public synchronized final int maxSize() {        return maxSize;    }    /**     * Returns the number of times {@link #get} returned a value that was     * already present in the cache.     */    public synchronized final int hitCount() {        return hitCount;    }    /**     * Returns the number of times {@link #get} returned null or required a new     * value to be created.     */    public synchronized final int missCount() {        return missCount;    }    /**     * Returns the number of times {@link #create(Object)} returned a value.     */    public synchronized final int createCount() {        return createCount;    }    /**     * Returns the number of times {@link #put} was called.     */    public synchronized final int putCount() {        return putCount;    }    /**     * Returns the number of values that have been evicted.     */    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);    }    @Override public synchronized final String toString() {        int accesses = hitCount + missCount;        int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;        return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",                maxSize, hitCount, missCount, hitPercent);    }}


 










0 0
原创粉丝点击