• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.util;
18 
19 import android.compat.annotation.UnsupportedAppUsage;
20 
21 import java.util.LinkedHashMap;
22 import java.util.Map;
23 
24 /**
25  * A cache that holds strong references to a limited number of values. Each time
26  * a value is accessed, it is moved to the head of a queue. When a value is
27  * added to a full cache, the value at the end of that queue is evicted and may
28  * become eligible for garbage collection.
29  *
30  * <p>If your cached values hold resources that need to be explicitly released,
31  * override {@link #entryRemoved}.
32  *
33  * <p>If a cache miss should be computed on demand for the corresponding keys,
34  * override {@link #create}. This simplifies the calling code, allowing it to
35  * assume a value will always be returned, even when there's a cache miss.
36  *
37  * <p>By default, the cache size is measured in the number of entries. Override
38  * {@link #sizeOf} to size the cache in different units. For example, this cache
39  * is limited to 4MiB of bitmaps:
40  * <pre>   {@code
41  *   int cacheSize = 4 * 1024 * 1024; // 4MiB
42  *   LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
43  *       protected int sizeOf(String key, Bitmap value) {
44  *           return value.getByteCount();
45  *       }
46  *   }}</pre>
47  *
48  * <p>This class is thread-safe. Perform multiple cache operations atomically by
49  * synchronizing on the cache: <pre>   {@code
50  *   synchronized (cache) {
51  *     if (cache.get(key) == null) {
52  *         cache.put(key, value);
53  *     }
54  *   }}</pre>
55  *
56  * <p>This class does not allow null to be used as a key or value. A return
57  * value of null from {@link #get}, {@link #put} or {@link #remove} is
58  * unambiguous: the key was not in the cache.
59  *
60  * <p>This class appeared in Android 3.1 (Honeycomb MR1); it's available as part
61  * of <a href="http://developer.android.com/sdk/compatibility-library.html">Android's
62  * Support Package</a> for earlier releases.
63  */
64 @android.ravenwood.annotation.RavenwoodKeepWholeClass
65 public class LruCache<K, V> {
66     @UnsupportedAppUsage
67     private final LinkedHashMap<K, V> map;
68 
69     /** Size of this cache in units. Not necessarily the number of elements. */
70     private int size;
71     private int maxSize;
72 
73     private int putCount;
74     private int createCount;
75     private int evictionCount;
76     private int hitCount;
77     private int missCount;
78 
79     /**
80      * @param maxSize for caches that do not override {@link #sizeOf}, this is
81      *     the maximum number of entries in the cache. For all other caches,
82      *     this is the maximum sum of the sizes of the entries in this cache.
83      */
LruCache(int maxSize)84     public LruCache(int maxSize) {
85         if (maxSize <= 0) {
86             throw new IllegalArgumentException("maxSize <= 0");
87         }
88         this.maxSize = maxSize;
89         this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
90     }
91 
92     /**
93      * Sets the size of the cache.
94      *
95      * @param maxSize The new maximum size.
96      */
resize(int maxSize)97     public void resize(int maxSize) {
98         if (maxSize <= 0) {
99             throw new IllegalArgumentException("maxSize <= 0");
100         }
101 
102         synchronized (this) {
103             this.maxSize = maxSize;
104         }
105         trimToSize(maxSize);
106     }
107 
108     /**
109      * Returns the value for {@code key} if it exists in the cache or can be
110      * created by {@code #create}. If a value was returned, it is moved to the
111      * head of the queue. This returns null if a value is not cached and cannot
112      * be created.
113      */
get(K key)114     public final V get(K key) {
115         if (key == null) {
116             throw new NullPointerException("key == null");
117         }
118 
119         V mapValue;
120         synchronized (this) {
121             mapValue = map.get(key);
122             if (mapValue != null) {
123                 hitCount++;
124                 return mapValue;
125             }
126             missCount++;
127         }
128 
129         /*
130          * Attempt to create a value. This may take a long time, and the map
131          * may be different when create() returns. If a conflicting value was
132          * added to the map while create() was working, we leave that value in
133          * the map and release the created value.
134          */
135 
136         V createdValue = create(key);
137         if (createdValue == null) {
138             return null;
139         }
140 
141         synchronized (this) {
142             createCount++;
143             mapValue = map.put(key, createdValue);
144 
145             if (mapValue != null) {
146                 // There was a conflict so undo that last put
147                 map.put(key, mapValue);
148             } else {
149                 size += safeSizeOf(key, createdValue);
150             }
151         }
152 
153         if (mapValue != null) {
154             entryRemoved(false, key, createdValue, mapValue);
155             return mapValue;
156         } else {
157             trimToSize(maxSize);
158             return createdValue;
159         }
160     }
161 
162     /**
163      * Caches {@code value} for {@code key}. The value is moved to the head of
164      * the queue.
165      *
166      * @return the previous value mapped by {@code key}.
167      */
put(K key, V value)168     public final V put(K key, V value) {
169         if (key == null || value == null) {
170             throw new NullPointerException("key == null || value == null");
171         }
172 
173         V previous;
174         synchronized (this) {
175             putCount++;
176             size += safeSizeOf(key, value);
177             previous = map.put(key, value);
178             if (previous != null) {
179                 size -= safeSizeOf(key, previous);
180             }
181         }
182 
183         if (previous != null) {
184             entryRemoved(false, key, previous, value);
185         }
186 
187         trimToSize(maxSize);
188         return previous;
189     }
190 
191     /**
192      * Remove the eldest entries until the total of remaining entries is at or
193      * below the requested size.
194      *
195      * @param maxSize the maximum size of the cache before returning. May be -1
196      *            to evict even 0-sized elements.
197      */
trimToSize(int maxSize)198     public void trimToSize(int maxSize) {
199         while (true) {
200             K key;
201             V value;
202             synchronized (this) {
203                 if (size < 0 || (map.isEmpty() && size != 0)) {
204                     throw new IllegalStateException(getClass().getName()
205                             + ".sizeOf() is reporting inconsistent results!");
206                 }
207 
208                 if (size <= maxSize) {
209                     break;
210                 }
211 
212                 Map.Entry<K, V> toEvict = eldest();
213                 if (toEvict == null) {
214                     break;
215                 }
216 
217                 key = toEvict.getKey();
218                 value = toEvict.getValue();
219                 map.remove(key);
220                 size -= safeSizeOf(key, value);
221                 evictionCount++;
222             }
223 
224             entryRemoved(true, key, value, null);
225         }
226     }
227 
eldest()228     private Map.Entry<K, V> eldest() {
229         return map.eldest();
230     }
231 
232     /**
233      * Removes the entry for {@code key} if it exists.
234      *
235      * @return the previous value mapped by {@code key}.
236      */
remove(K key)237     public final V remove(K key) {
238         if (key == null) {
239             throw new NullPointerException("key == null");
240         }
241 
242         V previous;
243         synchronized (this) {
244             previous = map.remove(key);
245             if (previous != null) {
246                 size -= safeSizeOf(key, previous);
247             }
248         }
249 
250         if (previous != null) {
251             entryRemoved(false, key, previous, null);
252         }
253 
254         return previous;
255     }
256 
257     /**
258      * Called for entries that have been evicted or removed. This method is
259      * invoked when a value is evicted to make space, removed by a call to
260      * {@link #remove}, or replaced by a call to {@link #put}. The default
261      * implementation does nothing.
262      *
263      * <p>The method is called without synchronization: other threads may
264      * access the cache while this method is executing.
265      *
266      * @param evicted true if the entry is being removed to make space, false
267      *     if the removal was caused by a {@link #put} or {@link #remove}.
268      * @param newValue the new value for {@code key}, if it exists. If non-null,
269      *     this removal was caused by a {@link #put} or a {@link #get}. Otherwise it was caused by
270      *     an eviction or a {@link #remove}.
271      */
entryRemoved(boolean evicted, K key, V oldValue, V newValue)272     protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
273 
274     /**
275      * Called after a cache miss to compute a value for the corresponding key.
276      * Returns the computed value or null if no value can be computed. The
277      * default implementation returns null.
278      *
279      * <p>The method is called without synchronization: other threads may
280      * access the cache while this method is executing.
281      *
282      * <p>If a value for {@code key} exists in the cache when this method
283      * returns, the created value will be released with {@link #entryRemoved}
284      * and discarded. This can occur when multiple threads request the same key
285      * at the same time (causing multiple values to be created), or when one
286      * thread calls {@link #put} while another is creating a value for the same
287      * key.
288      */
create(K key)289     protected V create(K key) {
290         return null;
291     }
292 
safeSizeOf(K key, V value)293     private int safeSizeOf(K key, V value) {
294         int result = sizeOf(key, value);
295         if (result < 0) {
296             throw new IllegalStateException("Negative size: " + key + "=" + value);
297         }
298         return result;
299     }
300 
301     /**
302      * Returns the size of the entry for {@code key} and {@code value} in
303      * user-defined units.  The default implementation returns 1 so that size
304      * is the number of entries and max size is the maximum number of entries.
305      *
306      * <p>An entry's size must not change while it is in the cache.
307      */
sizeOf(K key, V value)308     protected int sizeOf(K key, V value) {
309         return 1;
310     }
311 
312     /**
313      * Clear the cache, calling {@link #entryRemoved} on each removed entry.
314      */
evictAll()315     public final void evictAll() {
316         trimToSize(-1); // -1 will evict 0-sized elements
317     }
318 
319     /**
320      * For caches that do not override {@link #sizeOf}, this returns the number
321      * of entries in the cache. For all other caches, this returns the sum of
322      * the sizes of the entries in this cache.
323      */
size()324     public synchronized final int size() {
325         return size;
326     }
327 
328     /**
329      * For caches that do not override {@link #sizeOf}, this returns the maximum
330      * number of entries in the cache. For all other caches, this returns the
331      * maximum sum of the sizes of the entries in this cache.
332      */
maxSize()333     public synchronized final int maxSize() {
334         return maxSize;
335     }
336 
337     /**
338      * Returns the number of times {@link #get} returned a value that was
339      * already present in the cache.
340      */
hitCount()341     public synchronized final int hitCount() {
342         return hitCount;
343     }
344 
345     /**
346      * Returns the number of times {@link #get} returned null or required a new
347      * value to be created.
348      */
missCount()349     public synchronized final int missCount() {
350         return missCount;
351     }
352 
353     /**
354      * Returns the number of times {@link #create(Object)} returned a value.
355      */
createCount()356     public synchronized final int createCount() {
357         return createCount;
358     }
359 
360     /**
361      * Returns the number of times {@link #put} was called.
362      */
putCount()363     public synchronized final int putCount() {
364         return putCount;
365     }
366 
367     /**
368      * Returns the number of values that have been evicted.
369      */
evictionCount()370     public synchronized final int evictionCount() {
371         return evictionCount;
372     }
373 
374     /**
375      * Returns a copy of the current contents of the cache, ordered from least
376      * recently accessed to most recently accessed.
377      */
snapshot()378     public synchronized final Map<K, V> snapshot() {
379         return new LinkedHashMap<K, V>(map);
380     }
381 
toString()382     @Override public synchronized final String toString() {
383         int accesses = hitCount + missCount;
384         int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;
385         return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
386                 maxSize, hitCount, missCount, hitPercent);
387     }
388 }
389