diff options
Diffstat (limited to 'library')
13 files changed, 881 insertions, 359 deletions
diff --git a/library/.gitignore b/library/.gitignore index ed6b3e00..1cf16874 100644 --- a/library/.gitignore +++ b/library/.gitignore @@ -1,4 +1,6 @@ tests/ant.properties tests/local.properties tests/gen/**/* +tests/bin +libs/volley.jar diff --git a/library/lint.xml b/library/lint.xml index 2d559c9d..558db4f5 100644 --- a/library/lint.xml +++ b/library/lint.xml @@ -4,5 +4,6 @@ <issue id="AllowBackup" severity="ignore" /> <issue id="InlinedApi"> <ignore path="src/com/bumptech/glide/resize/cache/LruMemoryCache.java" /> + <ignore path="src/com/bumptech/glide/resize/bitmap_recycle/LruBitmapPool.java" /> </issue> </lint> diff --git a/library/src/com/bumptech/glide/resize/bitmap_recycle/AttributeStrategy.java b/library/src/com/bumptech/glide/resize/bitmap_recycle/AttributeStrategy.java new file mode 100644 index 00000000..bb3d3e1a --- /dev/null +++ b/library/src/com/bumptech/glide/resize/bitmap_recycle/AttributeStrategy.java @@ -0,0 +1,117 @@ +package com.bumptech.glide.resize.bitmap_recycle; + +import android.graphics.Bitmap; + +class AttributeStrategy implements LruPoolStrategy { + private final KeyPool keyPool = new KeyPool(); + private final GroupedLinkedMap<Key, Bitmap> groupedMap = new GroupedLinkedMap<Key, Bitmap>(); + + public void put(Bitmap bitmap) { + final Key key = keyPool.get(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig()); + + groupedMap.put(key, bitmap); + } + + @Override + public Bitmap get(int width, int height, Bitmap.Config config) { + final Key key = keyPool.get(width, height, config); + + return groupedMap.get(key); + } + + @Override + public Bitmap removeLast() { + return groupedMap.removeLast(); + } + + @Override + public String logBitmap(Bitmap bitmap) { + return getBitmapString(bitmap); + } + + @Override + public String logBitmap(int width, int height, Bitmap.Config config) { + return getBitmapString(width, height, config); + } + + @Override + public int getSize(Bitmap bitmap) { + return bitmap.getHeight() * bitmap.getRowBytes(); + } + + @Override + public String toString() { + return "AttributeStrategy:\n " + groupedMap; + } + + private static String getBitmapString(Bitmap bitmap) { + return getBitmapString(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig()); + } + + private static String getBitmapString(int width, int height, Bitmap.Config config) { + return "[" + width + "x" + height + "], " + config; + } + + private static class KeyPool extends BaseKeyPool<Key> { + public Key get(int width, int height, Bitmap.Config config) { + Key result = get(); + result.init(width, height, config); + return result; + } + + @Override + protected Key create() { + return new Key(this); + } + } + + private static class Key implements Poolable { + private final KeyPool pool; + private int width; + private int height; + // Config can be null :( + private Bitmap.Config config; + + public Key(KeyPool pool) { + this.pool = pool; + } + + public void init(int width, int height, Bitmap.Config config) { + this.width = width; + this.height = height; + this.config = config; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Key key = (Key) o; + + if (height != key.height) return false; + if (width != key.width) return false; + if (config != key.config) return false; + + return true; + } + + @Override + public int hashCode() { + int result = width; + result = 31 * result + height; + result = 31 * result + (config != null ? config.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return getBitmapString(width, height, config); + } + + @Override + public void offer() { + pool.offer(this); + } + } +} diff --git a/library/src/com/bumptech/glide/resize/bitmap_recycle/BaseKeyPool.java b/library/src/com/bumptech/glide/resize/bitmap_recycle/BaseKeyPool.java new file mode 100644 index 00000000..186850b8 --- /dev/null +++ b/library/src/com/bumptech/glide/resize/bitmap_recycle/BaseKeyPool.java @@ -0,0 +1,36 @@ +package com.bumptech.glide.resize.bitmap_recycle; + +import android.os.Build; + +import java.util.ArrayDeque; +import java.util.LinkedList; +import java.util.Queue; + +abstract class BaseKeyPool<T extends Poolable> { + private static final int MAX_SIZE = 20; + private final Queue<T> keyPool; + + public BaseKeyPool() { + if (Build.VERSION.SDK_INT >= 9) { + keyPool = new ArrayDeque<T>(MAX_SIZE); + } else { + keyPool = new LinkedList<T>(); + } + } + + protected T get() { + T result = keyPool.poll(); + if (result == null) { + result = create(); + } + return result; + } + + public void offer(T key) { + if (keyPool.size() < MAX_SIZE) { + keyPool.offer(key); + } + } + + protected abstract T create(); +} diff --git a/library/src/com/bumptech/glide/resize/bitmap_recycle/GroupedLinkedMap.java b/library/src/com/bumptech/glide/resize/bitmap_recycle/GroupedLinkedMap.java new file mode 100644 index 00000000..5fef781b --- /dev/null +++ b/library/src/com/bumptech/glide/resize/bitmap_recycle/GroupedLinkedMap.java @@ -0,0 +1,146 @@ +package com.bumptech.glide.resize.bitmap_recycle; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Similar to {@link java.util.LinkedHashMap} when access ordered except that it is access ordered on groups + * of bitmaps rather than individual objects. The idea is to be able to find the LRU bitmap size, rather than the + * LRU bitmap object. We can then remove bitmaps from the least recently used size of bitmap when we need to + * reduce our cache size. + * + * For the purposes of the LRU, we count gets for a particular size of bitmap as an access, even if no bitmaps + * of that size are present. We do not count addition or removal of bitmaps as an access. + */ +class GroupedLinkedMap<K extends Poolable, V> { + private final LinkedEntry<K, V> head = new LinkedEntry<K, V>(); + private final Map<K, LinkedEntry<K, V>> keyToEntry = new HashMap<K, LinkedEntry<K, V>>(); + + public void put(K key, V value) { + LinkedEntry<K, V> entry = keyToEntry.get(key); + + if (entry == null) { + entry = new LinkedEntry<K, V>(key); + makeTail(entry); + keyToEntry.put(key, entry); + } else { + key.offer(); + } + + entry.add(value); + } + + public V get(K key) { + LinkedEntry<K, V> entry = keyToEntry.get(key); + if (entry == null) { + entry = new LinkedEntry<K, V>(key); + keyToEntry.put(key, entry); + } else { + key.offer(); + } + + makeHead(entry); + + return entry.removeLast(); + } + + public V removeLast() { + LinkedEntry<K, V> last = head.prev; + + while (last != head) { + V removed = last.removeLast(); + if (removed != null) { + return removed; + } else { + // We will clean up empty lru entries since they are likely to have been one off or unusual sizes and + // are not likely to be requested again so the gc thrash should be minimal. Doing so will speed up our + // removeLast operation in the future and prevent our linked list from growing to arbitrarily large + // sizes. + removeEntry(last); + keyToEntry.remove(last.key); + last.key.offer(); + } + + last = last.prev; + } + + return null; + } + + @Override + public String toString() { + String result = "GroupedLinkedMap( "; + LinkedEntry<K, V> current = head.next; + boolean hadAtLeastOneItem = false; + while (current != head) { + hadAtLeastOneItem = true; + result += "{" + current.key + ":" + current.size() + "}, "; + current = current.next; + } + if (hadAtLeastOneItem) { + result = result.substring(0, result.length() - 2); + } + return result + " )"; + } + + // Make the entry the most recently used item. + private void makeHead(LinkedEntry<K, V> entry) { + removeEntry(entry); + entry.prev = head; + entry.next = head.next; + updateEntry(entry); + } + + // Make the entry the least recently used item. + private void makeTail(LinkedEntry<K, V> entry) { + removeEntry(entry); + entry.prev = head.prev; + entry.next = head; + updateEntry(entry); + } + + private static void updateEntry(LinkedEntry entry) { + entry.next.prev = entry; + entry.prev.next = entry; + } + + private static void removeEntry(LinkedEntry entry) { + entry.prev.next = entry.next; + entry.next.prev = entry.prev; + } + + private static class LinkedEntry<K, V> { + private final K key; + private List<V> values; + LinkedEntry<K, V> next; + LinkedEntry<K, V> prev; + + // Used only for the first item in the list which we will treat specially and which will not contain a value. + public LinkedEntry() { + this(null); + } + + public LinkedEntry(K key) { + next = prev = this; + this.key = key; + } + + public V removeLast() { + final int valueSize = size(); + return valueSize > 0 ? values.remove(valueSize - 1) : null; + } + + public int size() { + return values != null ? values.size() : 0; + } + + public void add(V value) { + if (values == null) { + values = new ArrayList<V>(); + } + values.add(value); + } + } +} diff --git a/library/src/com/bumptech/glide/resize/bitmap_recycle/LruBitmapPool.java b/library/src/com/bumptech/glide/resize/bitmap_recycle/LruBitmapPool.java index 4682271f..f4fc9be5 100644 --- a/library/src/com/bumptech/glide/resize/bitmap_recycle/LruBitmapPool.java +++ b/library/src/com/bumptech/glide/resize/bitmap_recycle/LruBitmapPool.java @@ -5,34 +5,52 @@ import static android.content.ComponentCallbacks2.TRIM_MEMORY_MODERATE; import android.graphics.Bitmap; import android.util.Log; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Queue; +import android.os.Build; public class LruBitmapPool implements BitmapPool { private static final String TAG = "LruBitmapPool"; - private final GroupedBitmapLinkedMap pool = new GroupedBitmapLinkedMap(); + private final LruPoolStrategy strategy; private final int maxSize; private int currentSize = 0; + private int hits; + private int misses; + private int puts; + private int evictions; + + // Exposed for testing only. + LruBitmapPool(int maxSize, LruPoolStrategy strategy) { + this.maxSize = maxSize; + this.strategy = strategy; + } public LruBitmapPool(int maxSize) { this.maxSize = maxSize; + if (Build.VERSION.SDK_INT >= 19) { + strategy = new SizeStrategy(); + } else { + strategy = new AttributeStrategy(); + } } @Override public synchronized boolean put(Bitmap bitmap) { - final int size = getSize(bitmap); + if (!bitmap.isMutable() || strategy.getSize(bitmap) > maxSize) { + return false; + } - pool.put(bitmap); + final int size = strategy.getSize(bitmap); + strategy.put(bitmap); + puts++; currentSize += size; - evict(); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Put bitmap in pool=" + strategy.logBitmap(bitmap)); + } + dump(); + + evict(); return true; } @@ -42,14 +60,20 @@ public class LruBitmapPool implements BitmapPool { @Override public synchronized Bitmap get(int width, int height, Bitmap.Config config) { - final Bitmap result = pool.get(width, height, config); + final Bitmap result = strategy.get(width, height, config); if (result == null) { if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Missing bitmap with dimens=[" + width + "x" + height + "] and config config=" + config); + Log.d(TAG, "Missing bitmap=" + strategy.logBitmap(width, height, config)); } + misses++; } else { - currentSize -= getSize(result); + hits++; + currentSize -= strategy.getSize(result); } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Get bitmap=" + strategy.logBitmap(width, height, config)); + } + dump(); return result; } @@ -64,198 +88,27 @@ public class LruBitmapPool implements BitmapPool { if (level >= TRIM_MEMORY_MODERATE) { clearMemory(); } else if (level >= TRIM_MEMORY_BACKGROUND) { - trimToSize(currentSize / 2); + trimToSize(maxSize / 2); } } private void trimToSize(int size) { while (currentSize > size) { - final Bitmap removed = pool.removeLast(); - currentSize -= getSize(removed); + final Bitmap removed = strategy.removeLast(); + currentSize -= strategy.getSize(removed); removed.recycle(); - } - } - - private static int getSize(Bitmap bitmap) { - return bitmap.getHeight() * bitmap.getRowBytes(); - } - - /** - * Similar to {@link java.util.LinkedHashMap} when access ordered except that it is access ordered on groups - * of bitmaps rather than individual objects. The idea is to be able to find the LRU bitmap size, rather than the - * LRU bitmap object. We can then remove bitmaps from the least recently used size of bitmap when we need to - * reduce our cache size. - * - * For the purposes of the LRU, we count gets for a particular size of bitmap as an access, even if no bitmaps - * of that size are present. We do not count addition or removal of bitmaps as an access. - */ - private static class GroupedBitmapLinkedMap { - private final Map<Key, LinkedEntry> keyToEntry = new HashMap<Key, LinkedEntry>(); - private final LinkedEntry head = new LinkedEntry(); - private final KeyPool keyPool = new KeyPool(); - - private static class KeyPool { - private static final int MAX_SIZE = 20; - - private final Queue<Key> keyPool = new LinkedList<Key>(); - - public Key get(int width, int height, Bitmap.Config config) { - Key result = keyPool.poll(); - if (result == null) { - result = new Key(); - } - result.init(width, height, config); - return result; - } - - public void offer(Key key) { - if (keyPool.size() <= MAX_SIZE) { - keyPool.offer(key); - } - } - } - - private static class Key { - private int width; - private int height; - private Bitmap.Config config; //this can be null :( - - public void init(int width, int height, Bitmap.Config config) { - this.width = width; - this.height = height; - this.config = config; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Key key = (Key) o; - - if (height != key.height) return false; - if (width != key.width) return false; - if (config != key.config) return false; - - return true; - } - - @Override - public int hashCode() { - int result = width; - result = 31 * result + height; - result = 31 * result + (config != null ? config.hashCode() : 0); - return result; - } - } - - public void put(Bitmap bitmap) { - final Key key = keyPool.get(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig()); - - LinkedEntry entry = keyToEntry.get(key); - if (entry == null) { - entry = new LinkedEntry(key); - makeTail(entry); - keyToEntry.put(key, entry); - } else { - keyPool.offer(key); - } - - entry.add(bitmap); - } - - public Bitmap get(int width, int height, Bitmap.Config config) { - final Key key = keyPool.get(width, height, config); - - LinkedEntry entry = keyToEntry.get(key); - if (entry == null) { - entry = new LinkedEntry(key); - keyToEntry.put(key, entry); - } else { - keyPool.offer(key); - } - - makeHead(entry); - - return entry.removeLast(); - } - - public Bitmap removeLast() { - LinkedEntry last = head.prev; - - while (last != head) { - Bitmap removed = last.removeLast(); - if (removed != null) { - return removed; - } else { - //we will clean up empty lru entries since they are likely to have been one off or unusual sizes - //and are not likely to be requested again so the gc thrash should be minimal. Doing so will speed - //up our removeLast operation in the future and prevent our linked list from growing to arbitrarily - //large sizes - removeEntry(last); - keyToEntry.remove(last.key); - keyPool.offer(last.key); - } - - last = last.prev; + evictions++; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Evicting bitmap=" + strategy.logBitmap(removed)); } - - return null; + dump(); } + } - private void makeHead(LinkedEntry entry) { - removeEntry(entry); - entry.prev = head; - entry.next = head.next; - updateEntry(entry); - } - - private void makeTail(LinkedEntry entry) { - removeEntry(entry); - entry.prev = head.prev; - entry.next = head; - updateEntry(entry); - } - - //after updating entry's next and prev, set - //those entry's prev and next (respectively) to rentry - private static void updateEntry(LinkedEntry entry) { - entry.next.prev = entry; - entry.prev.next = entry; - } - - private static void removeEntry(LinkedEntry entry) { - entry.prev.next = entry.next; - entry.next.prev = entry.prev; - } - - private static class LinkedEntry { - private List<Bitmap> value; - private final Key key; - LinkedEntry next; - LinkedEntry prev; - - //head only - public LinkedEntry() { - this(null); - } - - public LinkedEntry(Key key) { - next = prev = this; - this.key = key; - } - - public Bitmap removeLast() { - final int valueSize = value != null ? value.size() : 0; - return valueSize > 0 ? value.remove(valueSize-1) : null; - } - - public void add(Bitmap bitmap) { - if (value == null) { - value = new ArrayList<Bitmap>(); - } - value.add(bitmap); - } + private void dump() { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Hits=" + hits + " misses=" + misses + " puts=" + puts + " evictions=" + evictions + " currentSize=" + + currentSize + " maxSize=" + maxSize + "\nStrategy=" + strategy); } } } diff --git a/library/src/com/bumptech/glide/resize/bitmap_recycle/LruPoolStrategy.java b/library/src/com/bumptech/glide/resize/bitmap_recycle/LruPoolStrategy.java new file mode 100644 index 00000000..359af6e4 --- /dev/null +++ b/library/src/com/bumptech/glide/resize/bitmap_recycle/LruPoolStrategy.java @@ -0,0 +1,12 @@ +package com.bumptech.glide.resize.bitmap_recycle; + +import android.graphics.Bitmap; + +interface LruPoolStrategy { + public void put(Bitmap bitmap); + public Bitmap get(int width, int height, Bitmap.Config config); + public Bitmap removeLast(); + public String logBitmap(Bitmap bitmap); + public String logBitmap(int width, int height, Bitmap.Config config); + public int getSize(Bitmap bitmap); +} diff --git a/library/src/com/bumptech/glide/resize/bitmap_recycle/Poolable.java b/library/src/com/bumptech/glide/resize/bitmap_recycle/Poolable.java new file mode 100644 index 00000000..33a5abb7 --- /dev/null +++ b/library/src/com/bumptech/glide/resize/bitmap_recycle/Poolable.java @@ -0,0 +1,5 @@ +package com.bumptech.glide.resize.bitmap_recycle; + +interface Poolable { + public void offer(); +} diff --git a/library/src/com/bumptech/glide/resize/bitmap_recycle/SizeStrategy.java b/library/src/com/bumptech/glide/resize/bitmap_recycle/SizeStrategy.java new file mode 100644 index 00000000..f8778772 --- /dev/null +++ b/library/src/com/bumptech/glide/resize/bitmap_recycle/SizeStrategy.java @@ -0,0 +1,175 @@ +package com.bumptech.glide.resize.bitmap_recycle; + +import android.annotation.TargetApi; +import android.graphics.Bitmap; + +import java.util.TreeMap; + +@TargetApi(19) +class SizeStrategy implements LruPoolStrategy { + private static final int MAX_SIZE_MULTIPLE = 4; + private final KeyPool keyPool = new KeyPool(); + private final GroupedLinkedMap<Key, Bitmap> groupedMap = new GroupedLinkedMap<Key, Bitmap>(); + private final TreeMap<Integer, Integer> sortedSizes = new TreeMap<Integer, Integer>(); + + @Override + public void put(Bitmap bitmap) { + final Key key = keyPool.get(bitmap.getAllocationByteCount()); + + groupedMap.put(key, bitmap); + + Integer current = sortedSizes.get(key.size); + sortedSizes.put(key.size, current == null ? 1 : current + 1); + } + + @Override + public Bitmap get(int width, int height, Bitmap.Config config) { + final int size = getSize(width, height, config); + Key key = keyPool.get(size); + + Integer possibleSize = sortedSizes.ceilingKey(size); + if (possibleSize != null && possibleSize != size && possibleSize <= size * MAX_SIZE_MULTIPLE) { + keyPool.offer(key); + key = keyPool.get(possibleSize); + } + + // Do a get even if we know we don't have a bitmap so that the key moves to the front in the lru pool + final Bitmap result = groupedMap.get(key); + if (result != null) { + result.reconfigure(width, height, config); + decrementBitmapOfSize(possibleSize); + } + + return result; + } + + @Override + public Bitmap removeLast() { + Bitmap removed = groupedMap.removeLast(); + if (removed != null) { + final int removedSize = removed.getAllocationByteCount(); + decrementBitmapOfSize(removedSize); + } + return removed; + } + + private void decrementBitmapOfSize(Integer size) { + Integer current = sortedSizes.get(size); + if (current == 1) { + sortedSizes.remove(size); + } else { + sortedSizes.put(size, current - 1); + } + } + + @Override + public String logBitmap(Bitmap bitmap) { + return getBitmapString(bitmap); + } + + @Override + public String logBitmap(int width, int height, Bitmap.Config config) { + return getBitmapString(getSize(width, height, config)); + } + + @Override + public int getSize(Bitmap bitmap) { + return bitmap.getAllocationByteCount(); + } + + @Override + public String toString() { + String result = "SizeStrategy:\n " + groupedMap + "\n SortedSizes( "; + boolean hadAtLeastOneKey = false; + for (Integer size : sortedSizes.keySet()) { + hadAtLeastOneKey = true; + result += "{" + getBitmapString(size) + ":" + sortedSizes.get(size) + "}, "; + } + if (hadAtLeastOneKey) { + result = result.substring(0, result.length() - 2); + } + return result + " )"; + } + + private static String getBitmapString(Bitmap bitmap) { + return getBitmapString(bitmap.getAllocationByteCount()); + } + + private static String getBitmapString(int size) { + return "[" + size + "]"; + } + + private static int getSize(int width, int height, Bitmap.Config config) { + return width * height * getBytesPerPixel(config); + } + + private static int getBytesPerPixel(Bitmap.Config config) { + switch (config) { + case ARGB_8888: + return 4; + case RGB_565: + return 2; + case ARGB_4444: + return 2; + case ALPHA_8: + return 1; + default: + // We only use this to calculate sizes to get, so choosing 4 bytes per pixel is conservative and + // probably forces us to get a larger bitmap than we really need. Since we can't tell for sure, probably + // better safe than sorry. + return 4; + } + } + + private static class KeyPool extends BaseKeyPool<Key> { + + public Key get(int size) { + Key result = get(); + result.init(size); + return result; + } + + @Override + protected Key create() { + return new Key(this); + } + } + + private static class Key implements Poolable { + private final KeyPool pool; + private int size; + + private Key(KeyPool pool) { + this.pool = pool; + } + + public void init(int size) { + this.size = size; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Key key = (Key) o; + + return size == key.size; + } + + @Override + public int hashCode() { + return size; + } + + @Override + public String toString() { + return getBitmapString(size); + } + + @Override + public void offer() { + pool.offer(this); + } + } +} diff --git a/library/tests/src/com/bumptech/glide/LruBitmapPoolTest.java b/library/tests/src/com/bumptech/glide/LruBitmapPoolTest.java deleted file mode 100644 index 767403ef..00000000 --- a/library/tests/src/com/bumptech/glide/LruBitmapPoolTest.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.bumptech.glide; - -import android.content.ComponentCallbacks2; -import android.graphics.Bitmap; -import android.test.AndroidTestCase; -import com.bumptech.glide.resize.bitmap_recycle.LruBitmapPool; - -import java.util.ArrayList; -import java.util.List; - -public class LruBitmapPoolTest extends AndroidTestCase { - private static final int SIZE = 1024 * 1024; - private LruBitmapPool pool; - - @Override - protected void setUp() throws Exception { - super.setUp(); - pool = new LruBitmapPool(SIZE); - } - - public void testCanAddAndRemoveBitmap() { - Bitmap bitmap = getBitmap(); - pool.put(bitmap); - assertEquals(bitmap, getEquivalentFromPool(bitmap)); - } - - public void testCanAddAndRemoveBitmapsOfDifferentSizes() { - Bitmap first = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); - Bitmap second = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888); - pool.put(first); - pool.put(second); - assertEquals(first, getEquivalentFromPool(first)); - assertEquals(second, getEquivalentFromPool(second)); - } - - public void testCanAddAndRemoveBitmapsOfDifferentConfigs() { - Bitmap first = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); - Bitmap second = Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565); - pool.put(first); - pool.put(second); - assertEquals(first, getEquivalentFromPool(first)); - assertEquals(second, getEquivalentFromPool(second)); - } - - public void testPoolIsSizeLimited() { - List<Bitmap> bitmaps = fillPool(); - Bitmap first = bitmaps.get(0); - pool.put(Bitmap.createBitmap(first)); - - int totalInPool = 0; - for (int i = 0; i < bitmaps.size(); i++) { - if (getEquivalentFromPool(first) == null) { - break; - } - totalInPool++; - } - - assertEquals(bitmaps.size(), totalInPool); - } - - public void testLeastRecentlyAcquiredBitmapRemovedFirst() { - Bitmap special = Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565); - pool.put(Bitmap.createBitmap(special)); - pool.put(Bitmap.createBitmap(special)); - getEquivalentFromPool(special); - List<Bitmap> bitmaps = fillPool(); - - assertNotNull(getEquivalentFromPool(special)); - - Bitmap first = bitmaps.get(0); - int totalAcquired = 0; - for (int i = 0; i < bitmaps.size(); i++) { - if (getEquivalentFromPool(first) == null) { - break; - } - totalAcquired++; - } - - assertEquals(totalAcquired, bitmaps.size() - 1); - } - - public void testTrimMemoryCompleteClearsPool() { - doTestTrimMemory(ComponentCallbacks2.TRIM_MEMORY_COMPLETE, false); - } - - public void testTrimMemoryModerateClearsPool() { - doTestTrimMemory(ComponentCallbacks2.TRIM_MEMORY_MODERATE, false); - } - - public void testTrimMemoryBackgroundRemovesHalf() { - doTestTrimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND, true); - } - - private void doTestTrimMemory(int level, boolean half) { - List<Bitmap> bitmaps = fillPool(); - Bitmap first = bitmaps.get(0); - assertTrue(bitmaps.size() >= 2); - - Bitmap fromPool = getEquivalentFromPool(first); - assertNotNull(fromPool); - pool.put(fromPool); - pool.trimMemory(level); - if (half) { - for (int i = 0; i < bitmaps.size() / 2; i++) { - assertNotNull(getEquivalentFromPool(first)); - } - } - assertNull(getEquivalentFromPool(first)); - } - - public void testClearMemoryRemovesAllBitmaps() { - List<Bitmap> bitmaps = fillPool(); - assertTrue(bitmaps.size() >= 2); - - Bitmap first = bitmaps.get(0); - assertNotNull(getEquivalentFromPool(first)); - pool.clearMemory(); - assertNull(getEquivalentFromPool(first)); - } - - public void testTrimMemoryCallsRecycleOnRemovedBitmaps() { - List<Bitmap> bitmaps = fillPool(); - pool.trimMemory(ComponentCallbacks2.TRIM_MEMORY_COMPLETE); - for (Bitmap bitmap : bitmaps) { - assertTrue(bitmap.isRecycled()); - } - } - - public void testClearMemoryCallsRecycleOnRemovedBitmaps() { - List<Bitmap> bitmaps = fillPool(); - pool.clearMemory(); - for (Bitmap bitmap : bitmaps) { - assertTrue(bitmap.isRecycled()); - } - } - - public List<Bitmap> fillPool() { - List<Bitmap> bitmaps = new ArrayList<Bitmap>(); - Bitmap toPut = getBitmap(); - int bitmapSize = getSize(toPut); - for (int i = 0; i < (SIZE / bitmapSize); i++) { - bitmaps.add(Bitmap.createBitmap(toPut)); - } - for (Bitmap bitmap : bitmaps) { - pool.put(bitmap); - } - assertTrue(bitmaps.size() > 0); - return bitmaps; - } - - private Bitmap getEquivalentFromPool(Bitmap bitmap) { - return pool.get(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig()); - } - - private static int getSize(Bitmap bitmap) { - return bitmap.getRowBytes() * bitmap.getHeight(); - } - - private static Bitmap getBitmap() { - return Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); - } -} diff --git a/library/tests/src/com/bumptech/glide/resize/bitmap_recycle/AttributeStrategyTest.java b/library/tests/src/com/bumptech/glide/resize/bitmap_recycle/AttributeStrategyTest.java new file mode 100644 index 00000000..bfba0fd4 --- /dev/null +++ b/library/tests/src/com/bumptech/glide/resize/bitmap_recycle/AttributeStrategyTest.java @@ -0,0 +1,87 @@ +package com.bumptech.glide.resize.bitmap_recycle; + +import android.graphics.Bitmap; +import android.test.AndroidTestCase; + +public class AttributeStrategyTest extends AndroidTestCase { + + private AttributeStrategy strategy; + + @Override + protected void setUp() throws Exception { + super.setUp(); + strategy = new AttributeStrategy(); + } + + public void testIGetNullIfNoMatchingBitmapExists() { + assertNull(strategy.get(100, 100, Bitmap.Config.ARGB_8888)); + } + + public void testICanAddAndGetABitmapOfTheSameSizeAndDimensions() { + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + strategy.put(bitmap); + assertEquals(bitmap, strategy.get(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888)); + } + + public void testICantGetABitmapOfTheSameDimensionsButDifferentConfigs() { + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + strategy.put(bitmap); + assertNull(strategy.get(100, 100, Bitmap.Config.RGB_565)); + } + + public void testICantGetABitmapOfTheSameDimensionsAndSizeButDifferentConfigs() { + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_4444); + strategy.put(bitmap); + assertNull(strategy.get(100, 100, Bitmap.Config.RGB_565)); + } + + public void testICantGetABitmapOfDifferentWidths() { + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + strategy.put(bitmap); + assertNull(strategy.get(99, 100, Bitmap.Config.ARGB_8888)); + } + + public void testICantGetABitmapOfDifferentHeights() { + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + strategy.put(bitmap); + assertNull(strategy.get(100, 99, Bitmap.Config.ARGB_8888)); + } + + public void testICantGetABitmapOfDifferentDimensionsButTheSameSize() { + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + strategy.put(bitmap); + assertNull(strategy.get(50, 200, Bitmap.Config.ARGB_8888)); + } + + public void testMultipleBitmapsOfDifferentAttributesCanBeAddedAtOnce() { + Bitmap first = Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565); + Bitmap second = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + Bitmap third = Bitmap.createBitmap(120, 120, Bitmap.Config.RGB_565); + + strategy.put(first); + strategy.put(second); + strategy.put(third); + + assertEquals(first, strategy.get(100, 100, Bitmap.Config.RGB_565)); + assertEquals(second, strategy.get(100, 100, Bitmap.Config.ARGB_8888)); + assertEquals(third, strategy.get(120, 120, Bitmap.Config.RGB_565)); + } + + public void testLeastRecentlyUsedAttributeSetIsRemovedFirst() { + final Bitmap leastRecentlyUsed = Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8); + final Bitmap other = Bitmap.createBitmap(1000, 1000, Bitmap.Config.RGB_565); + final Bitmap mostRecentlyUsed = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + + strategy.get(100, 100, Bitmap.Config.ALPHA_8); + strategy.get(1000, 1000, Bitmap.Config.RGB_565); + strategy.get(100, 100, Bitmap.Config.ARGB_8888); + + strategy.put(other); + strategy.put(leastRecentlyUsed); + strategy.put(mostRecentlyUsed); + + Bitmap removed = strategy.removeLast(); + assertEquals("Expected=" + strategy.logBitmap(leastRecentlyUsed) + " got=" + strategy.logBitmap(removed), + leastRecentlyUsed, removed); + } +} diff --git a/library/tests/src/com/bumptech/glide/resize/bitmap_recycle/LruBitmapPoolTest.java b/library/tests/src/com/bumptech/glide/resize/bitmap_recycle/LruBitmapPoolTest.java new file mode 100644 index 00000000..95aa7968 --- /dev/null +++ b/library/tests/src/com/bumptech/glide/resize/bitmap_recycle/LruBitmapPoolTest.java @@ -0,0 +1,145 @@ +package com.bumptech.glide.resize.bitmap_recycle; + +import android.graphics.Bitmap; +import android.test.AndroidTestCase; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import static android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND; +import static android.content.ComponentCallbacks2.TRIM_MEMORY_COMPLETE; +import static android.content.ComponentCallbacks2.TRIM_MEMORY_MODERATE; + +public class LruBitmapPoolTest extends AndroidTestCase { + private static final int MAX_SIZE = 10; + private MockStrategy strategy; + private LruBitmapPool pool; + + @Override + protected void setUp() throws Exception { + strategy = new MockStrategy(); + pool = new LruBitmapPool(MAX_SIZE, strategy); + } + + public void testICanAddAndGetABitmap() { + fillPool(pool, 1); + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + pool.put(bitmap); + assertNotNull(pool.get(100, 100, Bitmap.Config.ARGB_8888)); + } + + public void testImmutableBitmapsAreNotAdded() { + pool.put(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888).copy(Bitmap.Config.ARGB_8888, false)); + assertEquals(0, strategy.bitmaps.size()); + } + + public void testItIsSizeLimited() { + fillPool(pool, MAX_SIZE + 2); + assertEquals(2, strategy.numRemoves); + } + + public void testBitmapLargerThanPoolIsNotAdded() { + strategy = new MockStrategy() { + @Override + public int getSize(Bitmap bitmap) { + return 4; + } + }; + pool = new LruBitmapPool(3, strategy); + pool.put(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); + assertEquals(0, strategy.numRemoves); + assertEquals(0, strategy.numPuts); + } + + public void testClearMemoryRemovesAllBitmaps() { + fillPool(pool, MAX_SIZE); + pool.clearMemory(); + + assertEquals(MAX_SIZE, strategy.numRemoves); + } + + public void testEvictedBitmapsAreRecycled() { + fillPool(pool, MAX_SIZE); + List<Bitmap> bitmaps = new ArrayList<Bitmap>(MAX_SIZE); + for (Bitmap b : strategy.bitmaps) { + bitmaps.add(b); + } + + pool.clearMemory(); + + for (Bitmap b : bitmaps) { + assertTrue(b.isRecycled()); + } + } + + public void testTrimMemoryBackgroundOrLessRemovesHalfOfBitmaps() { + testTrimMemory(MAX_SIZE, TRIM_MEMORY_BACKGROUND, MAX_SIZE / 2); + } + + public void testTrimMemoryBackgroundOrLessRemovesNoBitmapsIfPoolLessThanHalfFull() { + testTrimMemory(MAX_SIZE / 2, TRIM_MEMORY_BACKGROUND, 0); + } + + public void testTrimMemoryModerateOrGreaterRemovesAllBitmaps() { + for (int trimLevel : new int[] { TRIM_MEMORY_MODERATE, TRIM_MEMORY_COMPLETE }) { + testTrimMemory(MAX_SIZE, trimLevel, MAX_SIZE); + } + } + + private void testTrimMemory(int fillSize, int trimLevel, int expectedSize) { + MockStrategy strategy = new MockStrategy(); + LruBitmapPool pool = new LruBitmapPool(MAX_SIZE, strategy); + fillPool(pool, fillSize); + pool.trimMemory(trimLevel); + assertEquals("Failed level=" + trimLevel, expectedSize, strategy.numRemoves); + } + + private void fillPool(LruBitmapPool pool, int fillCount) { + for (int i = 0; i < fillCount; i++) { + pool.put(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); + } + } + + private static final int getSize(Bitmap bitmap) { + return bitmap.getRowBytes() * bitmap.getHeight(); + } + + private static class MockStrategy implements LruPoolStrategy { + private LinkedList<Bitmap> bitmaps = new LinkedList<Bitmap>(); + private int numRemoves; + private int numPuts; + + @Override + public void put(Bitmap bitmap) { + numPuts++; + bitmaps.add(bitmap); + } + + @Override + public Bitmap get(int width, int height, Bitmap.Config config) { + return bitmaps.removeLast(); + } + + @Override + public Bitmap removeLast() { + numRemoves++; + return bitmaps.removeLast(); + } + + @Override + public String logBitmap(Bitmap bitmap) { + return null; + } + + @Override + public String logBitmap(int width, int height, Bitmap.Config config) { + return null; + } + + @Override + public int getSize(Bitmap bitmap) { + return 1; + } + } +} diff --git a/library/tests/src/com/bumptech/glide/resize/bitmap_recycle/SizeStrategyTest.java b/library/tests/src/com/bumptech/glide/resize/bitmap_recycle/SizeStrategyTest.java new file mode 100644 index 00000000..bc24710f --- /dev/null +++ b/library/tests/src/com/bumptech/glide/resize/bitmap_recycle/SizeStrategyTest.java @@ -0,0 +1,105 @@ +package com.bumptech.glide.resize.bitmap_recycle; + +import android.graphics.Bitmap; +import android.test.AndroidTestCase; + +public class SizeStrategyTest extends AndroidTestCase { + private SizeStrategy strategy; + + @Override + protected void setUp() throws Exception { + super.setUp(); + strategy = new SizeStrategy(); + } + + public void testIGetNullIfNoMatchingBitmapExists() { + assertNull(strategy.get(100, 100, Bitmap.Config.ARGB_8888)); + } + + public void testICanAddAndGetABitmapOfTheSameSizeAndDimensions() { + Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + strategy.put(bitmap); + assertEquals(bitmap, strategy.get(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888)); + } + + public void testICanAddAndGetABitmapOfDifferentConfigsButSameSize() { + Bitmap original = Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888); + strategy.put(original); + assertEquals(original, strategy.get(800, 400, Bitmap.Config.RGB_565)); + } + + public void testICanAddAndGetABitmapOfDifferentDimensionsButSameSize() { + Bitmap original = Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888); + strategy.put(original); + assertEquals(original, strategy.get(200, 800, Bitmap.Config.ARGB_8888)); + } + + public void testICanGetABitmapUpToFourTimesLarger() { + Bitmap original = Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888); + strategy.put(original); + assertEquals(original, strategy.get(200, 200, Bitmap.Config.ARGB_8888)); + } + + public void testICantGetABitmapMoreThanFourTimesLarger() { + Bitmap original = Bitmap.createBitmap(401, 401, Bitmap.Config.ARGB_8888); + strategy.put(original); + assertNull(strategy.get(200, 200, Bitmap.Config.ARGB_8888)); + } + + public void testICantGetASmallerBitmap() { + Bitmap original = Bitmap.createBitmap(99, 99, Bitmap.Config.ARGB_8888); + strategy.put(original); + assertNull(strategy.get(100, 100, Bitmap.Config.ARGB_8888)); + } + + public void testReturnedDimensionsMatchIfSizeDoesNotMatch() { + Bitmap original = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + strategy.put(original); + Bitmap result = strategy.get(99, 99, Bitmap.Config.ARGB_8888); + assertEquals(99, result.getWidth()); + assertEquals(99, result.getHeight()); + } + + public void testReturnedConfigMatchesIfSizeDoesNotMatch() { + Bitmap original = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + strategy.put(original); + Bitmap result = strategy.get(100, 100, Bitmap.Config.RGB_565); + assertEquals(Bitmap.Config.RGB_565, result.getConfig()); + } + + public void testSmallestMatchingSizeIsReturned() { + Bitmap smallest = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + Bitmap medium = Bitmap.createBitmap(120, 120, Bitmap.Config.ARGB_8888); + Bitmap large = Bitmap.createBitmap(150, 150, Bitmap.Config.ARGB_8888); + + strategy.put(large); + strategy.put(smallest); + strategy.put(medium); + + assertEquals(smallest, strategy.get(99, 99, Bitmap.Config.ARGB_8888)); + } + + // This ensures that our sizes are incremented and decremented appropriately so we don't think we have more bitmaps + // of a size than we actually do. + public void testAMatchingBitmapIsReturnedIfAvailable() { + strategy.put(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)); + strategy.get(99, 99, Bitmap.Config.ARGB_8888); + strategy.put(Bitmap.createBitmap(101, 101, Bitmap.Config.ARGB_8888)); + assertNotNull(strategy.get(99, 99, Bitmap.Config.ARGB_8888)); + } + + public void testLeastRecentlyObtainedSizeIsRemovedFirst() { + Bitmap mostRecentlyUsed = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + Bitmap other = Bitmap.createBitmap(1000, 1000, Bitmap.Config.RGB_565); + Bitmap leastRecentlyUsed = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888); + strategy.get(500, 500, Bitmap.Config.ARGB_8888); + strategy.get(1000, 1000, Bitmap.Config.RGB_565); + strategy.get(100, 100, Bitmap.Config.ARGB_8888); + + strategy.put(other); + strategy.put(leastRecentlyUsed); + strategy.put(mostRecentlyUsed); + + assertEquals(leastRecentlyUsed, strategy.removeLast()); + } +} |