In-depth source code analysis LruCache and source code analysis lrucache
Introduction: many people recently mentioned in their blogs that they were asked during an interview, "what is the principle of LruCache ?", I found that I have never touched this knowledge point before. In the attitude of knowing it, I first searched some blog posts for relevant knowledge and went to the source code. Now I know what LruCache is. write a blog post to take notes.
What is the significance of LruCache in the past and present?
I generally do not like the definition of the wild path, so I chose the definition of LruCache officially on Android:
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.
The definition means that LruCache holds a strongly referenced cache for a limited number of cache objects. Each time a cache object is accessed, it is moved to the queue header. When an object is added to the LruCache that has reached the upper limit, the object at the end of the queue will be removed and may be recycled by the garbage collection mechanism.
From the definition, we can know that the LruCache has a queue to store the access sequence of objects. When the LruCache reaches the storage online, the elements to be added will be removed from the queue to free up space.
What is the significance of queue in LruCache?
To solve this problem, we need to know that Lru in LruCache refers to "Least Recently Used-Least Recently Used algorithm ". This means that LruCache should be a cache class that can determine which cache object is the least recently used. I believe everyone can understand why the queue needs to be introduced.
The significance of queue introduction is that each time a cache object in LruCache is accessed, the position of this object in the queue will change-that is, the queue header is mentioned. Assume that the queue has n elements, element A has never been accessed, so Element A must be at the end of the team and become the "least recently used element.
LruCache implementation principle LruCache initial source code analysis
There are too many codes, and I cannot paste them all, so I will post them according to my ideas ~
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;
From the code, we can know that LruCache is not a subclass of other classes, and only one javashashmap object and a bunch of int values are involved. That is to say,The key to implementing LruCache's core logic is LinkedHashMap..
Let's take a look at the source code of LinkedHashMap.
LinkedHashMap analyzes what a LinkedHashMap is.
Similarly, the official explanation is quoted as follows:
LinkedHashMap is an implementation of Map that guarantees iteration order. All optional operations are supported.
Entries are kept in a doubly-linked list. the iteration order is, by default, the order in which keys were inserted. reinserting an already-present key doesn't change the order. if the three argument constructor is used, and accessOrder is specified as true, the iteration will be in the order that entries were accessed. the access order is affected by put, get, and putAll operations, but not by operations on the collection views.
LinkedHashMap is an implementation of Map (a subclass of HashMap. The parent class of HashMap is an abstract Map class, which implements the Map interface), which can ensure the order of the iterator. Data in LinkedHashMap is stored through a two-way linked list. By default, the order of the iterator is the order of key mixing, and re-inserting an existing key does not change the sequence of the iterator.
If the third parameter in the constructorboolean accessOrderIf the value of accessOrder is true, the iterator order is the data access order, and this order will be affected by put, get, and putall methods, but it is not affected by the operation of the Set view.
The explanation of LinkedHashmap proves our speculation.The key to implementing LruCache's core logic is LinkedHashMap.Yes, so we only need to understand the specific principle of LinkedHashpMap, and the principle of LruCache is not a problem.
LinkedHashMap example
Before analyzing LinkedHashMap, let's take a look at what LinkedHashMap can do. After all, it is easier to analyze the internal logic from its external performance. Below is a simple Java code:
Public class Demo {public static void main (String [] args) {javashashmap <String, String> accessOrderMap = new javashashmap <> (10, 0.75f, true); System. out. println ("before processing"); // The javashashmap test accessOrderMap that is affected by the access sequence. put ("one", "one"); accessOrderMap. put ("two", "two"); accessOrderMap. put ("three", "three"); // output without any processing (Entry <String, String> entry: accessOrderMap. entrySet () {System. out. println (entry. getValue ();} System. out. println ("processed"); // access the evil elements in the Map and change the order of accessOrderMap. get ("one"); // processed output for (Entry <String, String> entry: accessOrderMap. entrySet () {System. out. println (entry. getValue ());}}}
Here is the output:
Before processing
One
Two
Three
After processing
Two
Three
One
We can see that after processing, the location of one has obviously changed and ran to the front of two and three.
LinkedHashMap implementation principle
Let's look at the third constructor in LinkedHashMap:
public LinkedHashMap( int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); init(); this.accessOrder = accessOrder; }
@Override void init() { header = new LinkedEntry<K, V>(); }
We can see that the constructor calls the constructor of the parent class, creates an entry object, and sets the value of accessOrder to the value passed in by the constructor. What is this external entry?
/** * 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; } }
We can see that the external entry is a two-way linked list used to store data. Therefore, we only need to know how the LinkedHashMap class decides how the program operates the LinkedEntry using get, put, and other methods based on the value of accessOrder to understand the implementation principle of the entire LinkedHashMap.
We track the value of accessOrder and it is easy to find that there is a corresponding processing in the get method:
/** * 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. */ if (key == null) { HashMapEntry<K, V> e = entryForNullKey; if (e == null) return null; if (accessOrder) makeTail((LinkedEntry<K, V>) 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; }
As you can see from the code, as long as the accessOrder is true, the makeTail method will be executed to operate on a new volume entry object. We may as well go to the makeTail method to find out:
/** * 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++; }
I believe that the logic here can be understood by anyone who has learned a little about the data structure, that is, to change the location of a node and put it in the header.
We know the queue changes brought about by get. What about put? When we searched for put, we found that this method was not available in the class. What is this ...... Don't panic, we will find it through the Code patientlyaddNewEntry(K key, V value, int hash, int index)AndaddNewEntryForNullKey(V value)Methods, and they are two methods for implementing put to change the queue logic. Let's take a look:
@Override void addNewEntry(K key, V value, int hash, int index) { LinkedEntry<K, V> header = this.header; // Remove eldest entry if instructed to do so. LinkedEntry<K, V> eldest = header.nxt; if (eldest != header && removeEldestEntry(eldest)) { remove(eldest.key); } // Create new entry, link it on to list, and put it into table LinkedEntry<K, V> oldTail = header.prv; LinkedEntry<K, V> newTail = new LinkedEntry<K,V>( key, value, hash, table[index], header, oldTail); table[index] = oldTail.nxt = header.prv = newTail; } @Override void addNewEntryForNullKey(V value) { LinkedEntry<K, V> header = this.header; // Remove eldest entry if instructed to do so. LinkedEntry<K, V> eldest = header.nxt; if (eldest != header && removeEldestEntry(eldest)) { remove(eldest.key); } // Create new entry, link it on to list, and put it into table LinkedEntry<K, V> oldTail = header.prv; LinkedEntry<K, V> newTail = new LinkedEntry<K,V>( null, value, 0, null, header, oldTail); entryForNullKey = oldTail.nxt = header.prv = newTail; }
In the addNewEntry method, when a new element is added, the system determines whether to remove the expired element. When we enter the removeEldestEntry method, the removeEldestEntry method returns a value of false, that is to say, the expired element will not be removed in any way. What does that mean?
In fact, this is quite understandable. As a storage structure, LinkedHashMap does not limit its storage capacity (because its implementation is based on a two-way linked list ), therefore, he has no reason to remove expired elements in the default implementation, while LruCache has designed its own logic to process expired elements.
LruCache
Through the analysis just now, we know how the core of LruCache's LinkedHashMap extracts "least recently used objects". What else does LruCache do with LinkedHashMap? Let's continue exploring with me ~
Through the get/put method, we can know that LruCache uses trimToSize to control the number of elements in it. When the maximum number is reached, adding an element will remove the expired element:
private 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; } // BEGIN LAYOUTLIB CHANGE // get the last item in the linked list. // This is not efficient, the goal here is to minimize the changes // compared to the platform version. Map.Entry<K, V> toEvict = null; for (Map.Entry<K, V> entry : map.entrySet()) { toEvict = entry; } // END LAYOUTLIB CHANGE if (toEvict == null) { break; } key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= safeSizeOf(key, value); evictionCount++; } entryRemoved(true, key, value, null); } }
The code first makes some simple judgments. Only the size> maxSize can execute the relevant logic: traverse the elements of the LinkedHashMap bidirectional linked list and obtain the elements at the end of the queue (that is, the expired elements ), if it is not empty, remove it.
Notes
1. If your cache object holds the resources that need to be accurately recycled, rewrite the entryRemoved method to meet your needs.
2. By default, the size of the cache space is determined by the number of elements, but you can rewrite the sizeOf method in different units to change the size. For example, the bitmap object size is 4 MB.
int cacheSize = 4 * 1024 * 1024; // 4MiB LruCache bitmapCache = new LruCache(cacheSize) { protected int sizeOf(String key, Bitmap value) { return value.getByteCount(); } }}
3. The key-Value Pair passed into LruCache cannot be null, because the meaning of the get/put/remove method will be blurred.