Skip to content

Commit

Permalink
Use a fixed size for items in LruCache.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 303010044
  • Loading branch information
sjudd authored and glide-copybara-robot committed Mar 26, 2020
1 parent 5e2ccc6 commit 5090b6d
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 17 deletions.
59 changes: 43 additions & 16 deletions library/src/main/java/com/bumptech/glide/util/LruCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* @param <Y> The type of the values.
*/
public class LruCache<T, Y> {
private final Map<T, Y> cache = new LinkedHashMap<>(100, 0.75f, true);
private final Map<T, Entry<Y>> cache = new LinkedHashMap<>(100, 0.75f, true);
private final long initialMaxSize;
private long maxSize;
private long currentSize;
Expand Down Expand Up @@ -98,7 +98,8 @@ public synchronized boolean contains(@NonNull T key) {
*/
@Nullable
public synchronized Y get(@NonNull T key) {
return cache.get(key);
Entry<Y> entry = cache.get(key);
return entry != null ? entry.value : null;
}

/**
Expand All @@ -109,6 +110,19 @@ public synchronized Y get(@NonNull T key) {
* the cache and instead {@link #onItemEvicted(Object, Object)} will be called synchronously with
* the given key and item.
*
* <p>The size of the item is determined by the {@link #getSize(Object)} method. To avoid errors
* where {@link #getSize(Object)} returns different values for the same object when called at
* different times, the size value is acquired in {@code put} and retained until the item is
* evicted, replaced or removed.
*
* <p>If {@code item} is null the behavior here is a little odd. For the most part it's similar to
* simply calling {@link #remove(Object)} with the given key. The difference is that calling this
* method with a null {@code item} will result in an entry remaining in the cache with a null
* value and 0 size. The only real consequence is that at some point {@link #onItemEvicted(Object,
* Object)} may be called with the given {@code key} and a null value. Ideally we'd make calling
* this method with a null {@code item} identical to {@link #remove(Object)} but we're preserving
* this odd behavior to match older versions :(.
*
* @param key The key to add the item at.
* @param item The item to add.
*/
Expand All @@ -123,17 +137,17 @@ public synchronized Y put(@NonNull T key, @Nullable Y item) {
if (item != null) {
currentSize += itemSize;
}
@Nullable final Y old = cache.put(key, item);
@Nullable Entry<Y> old = cache.put(key, item == null ? null : new Entry<>(item, itemSize));
if (old != null) {
currentSize -= getSize(old);
currentSize -= old.size;

if (!old.equals(item)) {
onItemEvicted(key, old);
if (!old.value.equals(item)) {
onItemEvicted(key, old.value);
}
}
evict();

return old;
return old != null ? old.value : null;
}

/**
Expand All @@ -143,11 +157,12 @@ public synchronized Y put(@NonNull T key, @Nullable Y item) {
*/
@Nullable
public synchronized Y remove(@NonNull T key) {
final Y value = cache.remove(key);
if (value != null) {
currentSize -= getSize(value);
Entry<Y> entry = cache.remove(key);
if (entry == null) {
return null;
}
return value;
currentSize -= entry.size;
return entry.value;
}

/** Clears all items in the cache. */
Expand All @@ -162,20 +177,32 @@ public void clearMemory() {
* @param size The size the cache should be less than.
*/
protected synchronized void trimToSize(long size) {
Map.Entry<T, Y> last;
Iterator<Map.Entry<T, Y>> cacheIterator;
Map.Entry<T, Entry<Y>> last;
Iterator<Map.Entry<T, Entry<Y>>> cacheIterator;
while (currentSize > size) {
cacheIterator = cache.entrySet().iterator();
last = cacheIterator.next();
final Y toRemove = last.getValue();
currentSize -= getSize(toRemove);
final Entry<Y> toRemove = last.getValue();
currentSize -= toRemove.size;
final T key = last.getKey();
cacheIterator.remove();
onItemEvicted(key, toRemove);
onItemEvicted(key, toRemove.value);
}
}

private void evict() {
trimToSize(maxSize);
}

@Synthetic
static final class Entry<Y> {
final Y value;
final int size;

@Synthetic
Entry(Y value, int size) {
this.value = value;
this.size = size;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.bumptech.glide.load.engine.cache;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
Expand Down Expand Up @@ -199,7 +200,7 @@ public void testCanPutSameItemMultipleTimes() {
}

@Test
public void put_withSameValueTwice_doesNotEvictItems() {
public void put_withSameKeyAndValueTwice_doesNotEvictItems() {
String key = getKey();
Object value = new Object();
cache.put(key, value);
Expand Down Expand Up @@ -318,6 +319,58 @@ public void testGetMaxSizeReturnsCurrentMaxSizeOfCache() {
assertEquals(SIZE, cache.getMaxSize());
}

@Test
public void setSizeMultiplier_withItemWhoseSizeDecreasesAfterAdd_doesNotCrash() {
Object itemWhoseSizeWillChange = new Object();
when(listener.getSize(itemWhoseSizeWillChange)).thenReturn(SIZE - 1).thenReturn(SIZE / 2);
cache.put(getKey(), itemWhoseSizeWillChange);
cache.setSizeMultiplier(0);
}

@Test
public void getCurrentSize_afterRemovingItemWhoseSizeChanged_returnsZero() {
Object itemWhoseSizeWillChange = new Object();
when(listener.getSize(itemWhoseSizeWillChange)).thenReturn(SIZE - 1).thenReturn(SIZE / 2);
String key = getKey();
cache.put(key, itemWhoseSizeWillChange);
cache.remove(key);

assertThat(cache.getCurrentSize()).isEqualTo(0);
}

@Test
public void clearMemory_afterRemovingItemWhoseSizeChanged_doesNotCrash() {
Object itemWhoseSizeWillChange = new Object();
when(listener.getSize(itemWhoseSizeWillChange)).thenReturn(SIZE - 1).thenReturn((SIZE / 2) - 1);
String key = getKey();
cache.put(key, itemWhoseSizeWillChange);
cache.remove(key);

cache.clearMemory();
}

@Test
public void getCurrentSize_afterUpdatingItemWhoseSizeChanged_returnsTheNewSize() {
Object itemWhoseSizeWillChange = new Object();
when(listener.getSize(itemWhoseSizeWillChange)).thenReturn(SIZE - 1).thenReturn((SIZE / 2) - 1);
String key = getKey();
cache.put(key, itemWhoseSizeWillChange);
cache.put(key, new Object());

assertThat(cache.getCurrentSize()).isEqualTo(1);
}

@Test
public void clearMemory_afterUpdatingItemWhoseSizeChanged_doesNotCrash() {
Object itemWhoseSizeWillChange = new Object();
when(listener.getSize(itemWhoseSizeWillChange)).thenReturn(SIZE - 1).thenReturn((SIZE / 2) - 1);
String key = getKey();
cache.put(key, itemWhoseSizeWillChange);
cache.put(key, new Object());

cache.clearMemory();
}

@Test
public void testGetMaxSizeChangesIfMaxSizeChanges() {
int multiplier = 2;
Expand Down

1 comment on commit 5090b6d

@hisweek
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am currently using v4.9.0 version, but there is a problem with LRUCache.java().
In the log below, the A part of (LruCache.java:178) looks like a (trimToSize) function.
And then I found this commit that seems relevant.
Could this commit be a solution to this exception??

java.util.NoSuchElementException
at java.util.LinkedHashMap$LinkedHashIterator.nextNode(LinkedHashMap.java:759)
at java.util.LinkedHashMap$LinkedEntryIterator.next(LinkedHashMap.java:790)
at java.util.LinkedHashMap$LinkedEntryIterator.next(LinkedHashMap.java:788)
at com.bumptech.glide.g.g.a(LruCache.java:178)
at com.bumptech.glide.g.g.a(LruCache.java:164)
at com.bumptech.glide.load.engine.a.g.a(LruResourceCache.java:52)
at com.bumptech.glide.c.a(Glide.java:628)
at com.bumptech.glide.c.onTrimMemory(Glide.java:841)
at android.app.Application.onTrimMemory(Application.java:306)
at android.app.ActivityThread.handleTrimMemory(ActivityThread.java:6410)
at android.app.ActivityThread.access$1100(ActivityThread.java:269)
at android.app.ActivityThread$ApplicationThread.lambda$scheduleTrimMemory$0(ActivityThread.java:1690)
at android.app.-$$Lambda$ActivityThread$ApplicationThread$tUGFX7CUhzB4Pg5wFd5yeqOnu38.accept(Unknown Source:8)
at com.android.internal.util.function.pooled.PooledLambdaImpl.doInvoke(PooledLambdaImpl.java:271)
at com.android.internal.util.function.pooled.PooledLambdaImpl.invoke(PooledLambdaImpl.java:195)
at com.android.internal.util.function.pooled.OmniFunction.run(OmniFunction.java:86)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:996)
at android.view.Choreographer.doCallbacks(Choreographer.java:794)
at android.view.Choreographer.doFrame(Choreographer.java:731)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:981)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:237)
at android.app.ActivityThread.main(ActivityThread.java:7860)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1075)

Please sign in to comment.