package com.bumptech.glide.load.engine.bitmap_recycle; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.util.Log; import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Synthetic; import java.util.HashMap; import java.util.Map; import java.util.NavigableMap; import java.util.TreeMap; /** * A fixed size Array Pool that evicts arrays using an LRU strategy to keep the pool under * the maximum byte size. */ public final class LruArrayPool implements ArrayPool { // 4MB. static final int DEFAULT_SIZE = 4 * 1024 * 1024; /** * The maximum number of times larger an int array may be to be than a requested size to eligible * to be returned from the pool. */ private static final int MAX_OVER_SIZE_MULTIPLE = 8; /** Used to calculate the maximum % of the total pool size a single byte array may consume. */ private static final int SINGLE_ARRAY_MAX_SIZE_DIVISOR = 2; private final GroupedLinkedMap<Key, Object> groupedMap = new GroupedLinkedMap<>(); private final KeyPool keyPool = new KeyPool(); private final Map<Class<?>, NavigableMap<Integer, Integer>> sortedSizes = new HashMap<>(); private final Map<Class<?>, ArrayAdapterInterface<?>> adapters = new HashMap<>(); private final int maxSize; private int currentSize; @VisibleForTesting public LruArrayPool() { maxSize = DEFAULT_SIZE; } /** * Constructor for a new pool. * * @param maxSize The maximum size in integers of the pool. */ public LruArrayPool(int maxSize) { this.maxSize = maxSize; } @Override public synchronized <T> void put(T array, Class<T> arrayClass) { ArrayAdapterInterface<T> arrayAdapter = getAdapterFromType(arrayClass); int size = arrayAdapter.getArrayLength(array); int arrayBytes = size * arrayAdapter.getElementSizeInBytes(); if (!isSmallEnoughForReuse(arrayBytes)) { return; } Key key = keyPool.get(size, arrayClass); groupedMap.put(key, array); NavigableMap<Integer, Integer> sizes = getSizesForAdapter(arrayClass); Integer current = sizes.get(key.size); sizes.put(key.size, current == null ? 1 : current + 1); currentSize += arrayBytes; evict(); } @Override public <T> T get(int size, Class<T> arrayClass) { ArrayAdapterInterface<T> arrayAdapter = getAdapterFromType(arrayClass); T result; synchronized (this) { Integer possibleSize = getSizesForAdapter(arrayClass).ceilingKey(size); final Key key; if (mayFillRequest(size, possibleSize)) { key = keyPool.get(possibleSize, arrayClass); } else { key = keyPool.get(size, arrayClass); } result = getArrayForKey(key); if (result != null) { currentSize -= arrayAdapter.getArrayLength(result) * arrayAdapter.getElementSizeInBytes(); decrementArrayOfSize(arrayAdapter.getArrayLength(result), arrayClass); } } if (result == null) { if (Log.isLoggable(arrayAdapter.getTag(), Log.VERBOSE)) { Log.v(arrayAdapter.getTag(), "Allocated " + size + " bytes"); } result = arrayAdapter.newArray(size); } return result; } @SuppressWarnings("unchecked") @Nullable private <T> T getArrayForKey(Key key) { return (T) groupedMap.get(key); } private boolean isSmallEnoughForReuse(int byteSize) { return byteSize <= maxSize / SINGLE_ARRAY_MAX_SIZE_DIVISOR; } private boolean mayFillRequest(int requestedSize, Integer actualSize) { return actualSize != null && (isNoMoreThanHalfFull() || actualSize <= (MAX_OVER_SIZE_MULTIPLE * requestedSize)); } private boolean isNoMoreThanHalfFull() { return currentSize == 0 || (maxSize / currentSize >= 2); } @Override public synchronized void clearMemory() { evictToSize(0); } @Override public synchronized void trimMemory(int level) { if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { clearMemory(); } else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { evictToSize(maxSize / 2); } } private void evict() { evictToSize(maxSize); } private void evictToSize(int size) { while (currentSize > size) { Object evicted = groupedMap.removeLast(); Preconditions.checkNotNull(evicted); ArrayAdapterInterface<Object> arrayAdapter = getAdapterFromObject(evicted); currentSize -= arrayAdapter.getArrayLength(evicted) * arrayAdapter.getElementSizeInBytes(); decrementArrayOfSize(arrayAdapter.getArrayLength(evicted), evicted.getClass()); if (Log.isLoggable(arrayAdapter.getTag(), Log.VERBOSE)) { Log.v(arrayAdapter.getTag(), "evicted: " + arrayAdapter.getArrayLength(evicted)); } } } private void decrementArrayOfSize(int size, Class<?> arrayClass) { NavigableMap<Integer, Integer> sizes = getSizesForAdapter(arrayClass); Integer current = sizes.get(size); if (current == null) { throw new NullPointerException( "Tried to decrement empty size" + ", size: " + size + ", this: " + this); } if (current == 1) { sizes.remove(size); } else { sizes.put(size, current - 1); } } private NavigableMap<Integer, Integer> getSizesForAdapter(Class<?> arrayClass) { NavigableMap<Integer, Integer> sizes = sortedSizes.get(arrayClass); if (sizes == null) { sizes = new TreeMap<>(); sortedSizes.put(arrayClass, sizes); } return sizes; } @SuppressWarnings("unchecked") private <T> ArrayAdapterInterface<T> getAdapterFromObject(T object) { return (ArrayAdapterInterface<T>) getAdapterFromType(object.getClass()); } @SuppressWarnings("unchecked") private <T> ArrayAdapterInterface<T> getAdapterFromType(Class<T> arrayPoolClass) { ArrayAdapterInterface<?> adapter = adapters.get(arrayPoolClass); if (adapter == null) { if (arrayPoolClass.equals(int[].class)) { adapter = new IntegerArrayAdapter(); } else if (arrayPoolClass.equals(byte[].class)) { adapter = new ByteArrayAdapter(); } else { throw new IllegalArgumentException("No array pool found for: " + arrayPoolClass.getSimpleName()); } adapters.put(arrayPoolClass, adapter); } return (ArrayAdapterInterface<T>) adapter; } // VisibleForTesting int getCurrentSize() { int currentSize = 0; for (Class<?> type : sortedSizes.keySet()) { for (Integer size : sortedSizes.get(type).keySet()) { ArrayAdapterInterface<?> adapter = getAdapterFromType(type); currentSize += size * sortedSizes.get(type).get(size) * adapter.getElementSizeInBytes(); } } return currentSize; } private static final class KeyPool extends BaseKeyPool<Key> { @Synthetic KeyPool() { } Key get(int size, Class<?> arrayClass) { Key result = get(); result.init(size, arrayClass); return result; } @Override protected Key create() { return new Key(this); } } private static final class Key implements Poolable { private final KeyPool pool; @Synthetic int size; private Class<?> arrayClass; Key(KeyPool pool) { this.pool = pool; } void init(int length, Class<?> arrayClass) { this.size = length; this.arrayClass = arrayClass; } @Override public boolean equals(Object o) { if (o instanceof Key) { Key other = (Key) o; return size == other.size && arrayClass == other.arrayClass; } return false; } @Override public String toString() { return "Key{" + "size=" + size + "array=" + arrayClass + '}'; } @Override public void offer() { pool.offer(this); } @Override public int hashCode() { int result = size; result = 31 * result + (arrayClass != null ? arrayClass.hashCode() : 0); return result; } } }