package me.xiaopan.sketch.cache.recycle; import android.annotation.TargetApi; import android.graphics.Bitmap; import android.os.Build; import java.util.HashMap; import java.util.Map; import java.util.NavigableMap; import java.util.TreeMap; import me.xiaopan.sketch.util.SketchUtils; /** * Keys {@link android.graphics.Bitmap Bitmaps} using both {@link android.graphics.Bitmap#getAllocationByteCount()} and * the {@link android.graphics.Bitmap.Config} returned from {@link android.graphics.Bitmap#getConfig()}. * * <p> * Using both the config and the byte size allows us to safely re-use a greater variety of * {@link android.graphics.Bitmap Bitmaps}, which increases the hit rate of the pool and therefore the performance * of applications. This class works around #301 by only allowing re-use of {@link android.graphics.Bitmap Bitmaps} * with a matching number of bytes per pixel. * </p> */ @TargetApi(Build.VERSION_CODES.KITKAT) public class SizeConfigStrategy implements LruPoolStrategy { private static final int MAX_SIZE_MULTIPLE = 8; private static final Bitmap.Config[] ARGB_8888_IN_CONFIGS = new Bitmap.Config[] { Bitmap.Config.ARGB_8888, // The value returned by Bitmaps with the hidden Bitmap config. null, }; // We probably could allow ARGB_4444 and RGB_565 to decode into each other, but ARGB_4444 is deprecated and we'd // rather be safe. private static final Bitmap.Config[] RGB_565_IN_CONFIGS = new Bitmap.Config[] { Bitmap.Config.RGB_565 }; private static final Bitmap.Config[] ARGB_4444_IN_CONFIGS = new Bitmap.Config[] { Bitmap.Config.ARGB_4444 }; private static final Bitmap.Config[] ALPHA_8_IN_CONFIGS = new Bitmap.Config[] { Bitmap.Config.ALPHA_8 }; private final KeyPool keyPool = new KeyPool(); private final GroupedLinkedMap<Key, Bitmap> groupedMap = new GroupedLinkedMap<Key, Bitmap>(); private final Map<Bitmap.Config, NavigableMap<Integer, Integer>> sortedSizes = new HashMap<Bitmap.Config, NavigableMap<Integer, Integer>>(); @Override public void put(Bitmap bitmap) { int size = SketchUtils.getByteCount(bitmap); Key key = keyPool.get(size, bitmap.getConfig()); groupedMap.put(key, bitmap); NavigableMap<Integer, Integer> sizes = getSizesForConfig(bitmap.getConfig()); Integer current = sizes.get(key.size); sizes.put(key.size, current == null ? 1 : current + 1); } @Override public Bitmap get(int width, int height, Bitmap.Config config) { int size = SketchUtils.computeByteCount(width, height, config); Key targetKey = keyPool.get(size, config); Key bestKey = findBestKey(targetKey, size, config); Bitmap result = groupedMap.get(bestKey); if (result != null) { // Decrement must be called before reconfigure. decrementBitmapOfSize(SketchUtils.getByteCount(result), result.getConfig()); try { result.reconfigure(width, height, result.getConfig() != null ? result.getConfig() : Bitmap.Config.ARGB_8888); } catch (IllegalArgumentException e) { // Bitmap.cpp Bitmap_reconfigure method may throw "IllegalArgumentException: Bitmap not large enough to support new configuration" exception e.printStackTrace(); put(result); } } return result; } private Key findBestKey(Key key, int size, Bitmap.Config config) { Key result = key; for (Bitmap.Config possibleConfig : getInConfigs(config)) { NavigableMap<Integer, Integer> sizesForPossibleConfig = getSizesForConfig(possibleConfig); Integer possibleSize = sizesForPossibleConfig.ceilingKey(size); if (possibleSize != null && possibleSize <= size * MAX_SIZE_MULTIPLE) { if (possibleSize != size || (possibleConfig == null ? config != null : !possibleConfig.equals(config))) { keyPool.offer(key); result = keyPool.get(possibleSize, possibleConfig); } break; } } return result; } @Override public Bitmap removeLast() { Bitmap removed = groupedMap.removeLast(); if (removed != null) { int removedSize = SketchUtils.getByteCount(removed); decrementBitmapOfSize(removedSize, removed.getConfig()); } return removed; } private void decrementBitmapOfSize(Integer size, Bitmap.Config config) { NavigableMap<Integer, Integer> sizes = getSizesForConfig(config); Integer current = sizes.get(size); if (current == 1) { sizes.remove(size); } else { sizes.put(size, current - 1); } } private NavigableMap<Integer, Integer> getSizesForConfig(Bitmap.Config config) { NavigableMap<Integer, Integer> sizes = sortedSizes.get(config); if (sizes == null) { sizes = new TreeMap<Integer, Integer>(); sortedSizes.put(config, sizes); } return sizes; } @Override public String logBitmap(Bitmap bitmap) { int size = SketchUtils.getByteCount(bitmap); return getBitmapString(size, bitmap.getConfig()); } @Override public String logBitmap(int width, int height, Bitmap.Config config) { int size = SketchUtils.computeByteCount(width, height, config); return getBitmapString(size, config); } @Override public int getSize(Bitmap bitmap) { return SketchUtils.getByteCount(bitmap); } @Override public String toString() { StringBuilder sb = new StringBuilder() .append("SizeConfigStrategy{groupedMap=") .append(groupedMap) .append(", sortedSizes=("); for (Map.Entry<Bitmap.Config, NavigableMap<Integer, Integer>> entry : sortedSizes.entrySet()) { sb.append(entry.getKey()).append('[').append(entry.getValue()).append("], "); } if (!sortedSizes.isEmpty()) { sb.replace(sb.length() - 2, sb.length(), ""); } return sb.append(")}").toString(); } // Visible for testing. static class KeyPool extends BaseKeyPool<Key> { public Key get(int size, Bitmap.Config config) { Key result = get(); result.init(size, config); return result; } @Override protected Key create() { return new Key(this); } } // Visible for testing. static final class Key implements Poolable { private final KeyPool pool; private int size; private Bitmap.Config config; public Key(KeyPool pool) { this.pool = pool; } // Visible for testing. Key(KeyPool pool, int size, Bitmap.Config config) { this(pool); init(size, config); } public void init(int size, Bitmap.Config config) { this.size = size; this.config = config; } @Override public void offer() { pool.offer(this); } @Override public String toString() { return getBitmapString(size, config); } @Override public boolean equals(Object o) { if (o instanceof Key) { Key other = (Key) o; return size == other.size && (config == null ? other.config == null : config.equals(other.config)); } return false; } @Override public int hashCode() { int result = size; result = 31 * result + (config != null ? config.hashCode() : 0); return result; } } private static String getBitmapString(int size, Bitmap.Config config) { return "[" + size + "](" + config + ")"; } private static Bitmap.Config[] getInConfigs(Bitmap.Config requested) { switch (requested) { case ARGB_8888: return ARGB_8888_IN_CONFIGS; case RGB_565: return RGB_565_IN_CONFIGS; case ARGB_4444: return ARGB_4444_IN_CONFIGS; case ALPHA_8: return ALPHA_8_IN_CONFIGS; default: return new Bitmap.Config[] { requested }; } } }