SoFunction
Updated on 2025-03-11

About the use of Android memory cache LruCache and its source code analysis

Overall introduction

As a memory cache, LruCache uses strong reference to cache limited data. When a cached data is accessed, it will be moved to the head of the queue. When a new data is to be added to LruCache and the cache size is to be full, the data at the end of the queue may be collected by the garbage collector (GC). LruCache uses LRU (Least Recently Used) algorithm, that is,:Removes the least recently used data from the queue and allocates memory to the latest incoming data.

  • If a piece of data cached in LruCache explicitly needs to be released, it can be overriddenentryRemoved(boolean evicted, K key, V oldValue, V newValue)
  • If a piece of data cached by LruCache is not found through the key, you can override itcreate(K key), This simplifies the calling code, and even if a cached data is missed, it will not return null, but will return data created through create(K key).
  • If you want to limit the cache size of each piece of data, you can override itsizeOf(K key, V value), as shown below, the maximum cache size of bitmap is 4MB:
 int cacheSize = 4 * 1024 * 1024; // 4MiB
 LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
     protected int sizeOf(String key, Bitmap value) {
         return ();
     }
 }

LruCacheThis class is thread-safe. Automatically perform multiple cache operations throughsynchronizedSynchronous cache:

 synchronized (cache) {
   if ((key) == null) {
       (key, value);
   }
 }

LruCache does not allow null as a key or valueget(K)、put(K,V),remove(K)If the key or value is null, an exception will be thrown:throw new NullPointerException("key == null || value == null")

Common APIs

method Remark
void resize(int maxSize) Update storage size
V put(K key, V value) Save data, return the value corresponding to the previous key, if not, return null
V get(K key) Take out the cached data corresponding to the key
V remove(K key) Remove the value corresponding to the key
void evictAll() Clear cached data
Map<K, V> snapshot() Copy a cache and return in order from least recent access to most access sort

Example of usage

  • initialization:
 private Bitmap bitmap;
 private String STRING_KEY = "data_string";
 private LruCache&lt;String, Bitmap&gt; lruCache;
 private static final int CACHE_SIZE = 10 * 1024 * 1024;//10M
 lruCache = new LruCache&lt;String, Bitmap&gt;(CACHE_SIZE) {
    @Override
    protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
       (evicted, key, oldValue, newValue);
    }
    @Override
    protected int sizeOf(String key, Bitmap value) {
        //The size returned here is expressed in unit kb        return () / 1024;
    }
};

The maximum cache is set to 10M, the key is String type, and the value is set to Bitmap

  • Save data:
  if (lruCache!=null){
     (STRING_KEY, bitmap);
   }
  • Get data:
 if (lruCache != null) {
     bitmap = (STRING_KEY);
  }
  • Clear cache:

Source code analysis

Define variables, constructor initialization

    //
    private final LinkedHashMap&lt;K, V&gt; map;//Use LinkedHashMap to store operation data    /** Size of this cache in units. Not necessarily the number of elements. */
    private int size;//The cache data size is not necessarily equal to the number of elements    private int maxSize;//Maximum cache size    private int putCount;// Number of times data was added successfully    private int createCount;//The number of times cached data is created manually    private int evictionCount;//The number of times data was successfully recovered    private int hitCount;//The number of hits when searching for data    private int missCount;//Number of misses when searching for data    /**
     * @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 &lt;= 0) {
            throw new IllegalArgumentException("maxSize &lt;= 0");
        }
         = maxSize;
         = new LinkedHashMap&lt;K, V&gt;(0, 0.75f, true);
    }

Initialized in the constructorLinkedHashMap,parametermaxSizeSpecifies the maximum size of the cache.

Modify cache size

/**
 * 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) {
         = maxSize;
    }
    trimToSize(maxSize);
}

resize(int maxSize)Used to update the size, first add a synchronous lock to updatemaxSizeThe value oftrimToSize()method:

/**
 * 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 &lt; 0 || (() &amp;&amp; size != 0)) {
                throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
            }
            //If it is already less than the maximum cache, there is no need to continue to execute it            if (size &lt;= maxSize) {
                break;
            }
            //Get the least used data recently            &lt;K, V&gt; toEvict = ();
            if (toEvict == null) {
                break;
            }
            key = ();
            value = ();
            //Remove this minimally used data from LinkedHashMap            (key);
            //Cached size minus the size of the removed data. If sizeOf is not overridden, the subtracted value is 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 &lt; 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.
 *
 * &lt;p&gt;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()Looping away the least recently used data until the size of the remaining cached data is equal to less than the maximum cache size. Note: We seesizeOf()andentryRemoved()Allprotectedto modify, that is, it can be overwritten, ifsizeOf()Not overwritten, then variablesizeIt represents the amount of cached data.maxSizeRepresents the maximum number, if overriddensizeOf(),like:

 @Override
  protected int sizeOf(String key, BitmapDrawable value) {
      return ().getByteCount() / 1024;
  }

at this timesizeIt represents cached datasizemaxSizeRepresents the maximum cachesize

Save data

/**
 * 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) {
    //The key or value is empty and throws the exception directly    if (key == null || value == null) {
        throw new NullPointerException("key == null || value == null");
    }
    V previous;
    synchronized (this) {
        //The number of data added +1        putCount++;
         //The size of cached data increases        size += safeSizeOf(key, value);
         //Add cache data. If the value corresponding to the key value is not empty before adding, newValue will overwrite oldValue and return oldValue;         //If the value corresponding to the key value is empty, return Null        previous = (key, value);
        if (previous != null) {
           //The previous oldValue is not empty, then the oldValue is subtracted in the cache size            size -= safeSizeOf(key, previous);
        }
    }
    if (previous != null) {
        entryRemoved(false, key, previous, value);
    }
    //Recheck cache size    trimToSize(maxSize);
    return previous;
}

CallLinkedHashMapofput(key, value)After adding cached data, before adding ifkeyThe corresponding valuevalueNot empty,newValueWill coveroldValueand returnoldValue;ifkeyThe corresponding valuevalueis empty, then returnnull, then reset the cache according to the return valuesizeand maximum cachemaxSizesize.

Get data

 /**
  * 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) {
         //Fetch data through key         mapValue = (key);
         if (mapValue != null) {
             //If the data is retrieved, the number of hits will be +1             hitCount++;
             return mapValue;
         }
         //No data was retrieved, +1 was missed         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.
      */
     //If create() is not overridden, the null returned by the default create() method     V createdValue = create(key);    
     if (createdValue == null) {
         return null;
     }
     //If create() is overridden, that is, the value is manually created based on the key value, continue to execute     synchronized (this) {
          //The number of data creation times +1         createCount++;
         //Try to add data to cache         mapValue = (key, createdValue);
         if (mapValue != null) {
             // There was a conflict so undo that last put
             //mapValue is not empty, which means that the previous key corresponds to data, so it conflicts with the data we created manually.             //So perform the undo operation, add mapValue to the cache again, and use mapValue to overwrite createdValue             (key, mapValue);
         } else {
             //If mapValue is empty, it means that the value corresponding to the previous key value is indeed empty. After we manually add createdValue,             //The cache size needs to be recalculated             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;
  }

The process of fetching data is roughly like this:

  • First pass(key)Come and try to remove itvalue,ifvalueIf it exists, it will return directlyvalue
  • ifvalueIf it does not exist, then the followingcreate(key),ifcreate()Not overwritten, return directlynull
  • ifcreate()It is overwritten, that is, it is passedkeyValue created acreatedValue, then try to passmapValue = (key, createdValue)BundlecreatedValueAdd to cache,mapValueyesput()The return value of the method,mapValueIt representskeyThe corresponding value before, ifmapValueNot empty, it means the previous onekeyThe corresponding one has data, so it conflicts with the data we created manually, so perform the undo operation and re-set it.mapValueAdd to cache, usemapValueGo to covercreatedValue, and finally recalculate the cache size.

Remove data

 /**
  * 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 = (key);
         if (previous != null) {
             size -= safeSizeOf(key, previous);
         }
     }
     if (previous != null) {
         entryRemoved(false, key, previous, null);
     }
     return previous;
 }

Call(key)passkeyValue deletion is in cachekeyCorrespondingvalue, then recalculate the cache size and return the deletedvalue

Some other methods:

      //Clear the cache     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.
      */
      //Copy a cache, sorting from the least recent access to the most access in order     public synchronized final Map&lt;K, V&gt; snapshot() {
         return new LinkedHashMap&lt;K, V&gt;(map);
     }

The above is the detailed content about the use of Android memory cache LruCache and its source code analysis. For more information about the use of Android LruCache, please follow my other related articles!