1package com.bumptech.glide.load.engine.bitmap_recycle;
2
3import android.annotation.SuppressLint;
4import android.annotation.TargetApi;
5import android.graphics.Bitmap;
6import android.graphics.Color;
7import android.os.Build;
8import android.util.Log;
9
10import java.util.Collections;
11import java.util.HashSet;
12import java.util.Set;
13
14/**
15 * An {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} implementation that uses an
16 * {@link com.bumptech.glide.load.engine.bitmap_recycle.LruPoolStrategy} to bucket {@link Bitmap}s and then uses an LRU
17 * eviction policy to evict {@link android.graphics.Bitmap}s from the least recently used bucket in order to keep
18 * the pool below a given maximum size limit.
19 */
20public class LruBitmapPool implements BitmapPool {
21    private static final String TAG = "LruBitmapPool";
22    private static final Bitmap.Config DEFAULT_CONFIG = Bitmap.Config.ARGB_8888;
23
24    private final LruPoolStrategy strategy;
25    private final int initialMaxSize;
26    private final BitmapTracker tracker;
27
28    private int maxSize;
29    private int currentSize;
30    private int hits;
31    private int misses;
32    private int puts;
33    private int evictions;
34
35    // Exposed for testing only.
36    LruBitmapPool(int maxSize, LruPoolStrategy strategy) {
37        this.initialMaxSize = maxSize;
38        this.maxSize = maxSize;
39        this.strategy = strategy;
40        this.tracker = new NullBitmapTracker();
41    }
42
43    /**
44     * Constructor for LruBitmapPool.
45     *
46     * @param maxSize The initial maximum size of the pool in bytes.
47     */
48    public LruBitmapPool(int maxSize) {
49        this(maxSize, getDefaultStrategy());
50    }
51
52    @Override
53    public int getMaxSize() {
54        return maxSize;
55    }
56
57    @Override
58    public synchronized void setSizeMultiplier(float sizeMultiplier) {
59        maxSize = Math.round(initialMaxSize * sizeMultiplier);
60        evict();
61    }
62
63    @Override
64    public synchronized boolean put(Bitmap bitmap) {
65        if (!bitmap.isMutable() || strategy.getSize(bitmap) > maxSize) {
66            if (Log.isLoggable(TAG, Log.VERBOSE)) {
67                Log.v(TAG, "Reject bitmap from pool=" + strategy.logBitmap(bitmap) + " is mutable="
68                        + bitmap.isMutable());
69            }
70            return false;
71        }
72
73        final int size = strategy.getSize(bitmap);
74        strategy.put(bitmap);
75        tracker.add(bitmap);
76
77        puts++;
78        currentSize += size;
79
80        if (Log.isLoggable(TAG, Log.VERBOSE)) {
81            Log.v(TAG, "Put bitmap in pool=" + strategy.logBitmap(bitmap));
82        }
83        dump();
84
85        evict();
86        return true;
87    }
88
89    private void evict() {
90        trimToSize(maxSize);
91    }
92
93    @Override
94    public synchronized Bitmap get(int width, int height, Bitmap.Config config) {
95        Bitmap result = getDirty(width, height, config);
96        if (result != null) {
97            // Bitmaps in the pool contain random data that in some cases must be cleared for an image to be rendered
98            // correctly. we shouldn't force all consumers to independently erase the contents individually, so we do so
99            // here. See issue #131.
100            result.eraseColor(Color.TRANSPARENT);
101        }
102
103        return result;
104    }
105
106    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
107    @Override
108    public synchronized Bitmap getDirty(int width, int height, Bitmap.Config config) {
109        // Config will be null for non public config types, which can lead to transformations naively passing in
110        // null as the requested config here. See issue #194.
111        final Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG);
112        if (result == null) {
113            if (Log.isLoggable(TAG, Log.DEBUG)) {
114                Log.d(TAG, "Missing bitmap=" + strategy.logBitmap(width, height, config));
115            }
116            misses++;
117        } else {
118            hits++;
119            currentSize -= strategy.getSize(result);
120            tracker.remove(result);
121            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
122                result.setHasAlpha(true);
123            }
124        }
125        if (Log.isLoggable(TAG, Log.VERBOSE)) {
126            Log.v(TAG, "Get bitmap=" + strategy.logBitmap(width, height, config));
127        }
128        dump();
129
130        return result;
131    }
132
133    @Override
134    public void clearMemory() {
135        trimToSize(0);
136    }
137
138    @SuppressLint("InlinedApi")
139    @Override
140    public void trimMemory(int level) {
141        if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
142            clearMemory();
143        } else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
144            trimToSize(maxSize / 2);
145        }
146    }
147
148    private synchronized void trimToSize(int size) {
149        while (currentSize > size) {
150            final Bitmap removed = strategy.removeLast();
151            tracker.remove(removed);
152            currentSize -= strategy.getSize(removed);
153            removed.recycle();
154            evictions++;
155            if (Log.isLoggable(TAG, Log.DEBUG)) {
156                Log.d(TAG, "Evicting bitmap=" + strategy.logBitmap(removed));
157            }
158            dump();
159        }
160    }
161
162    private void dump() {
163        if (Log.isLoggable(TAG, Log.VERBOSE)) {
164            Log.v(TAG, "Hits=" + hits + " misses=" + misses + " puts=" + puts + " evictions=" + evictions
165                    + " currentSize=" + currentSize + " maxSize=" + maxSize + "\nStrategy=" + strategy);
166        }
167    }
168
169    private static LruPoolStrategy getDefaultStrategy() {
170        final LruPoolStrategy strategy;
171        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
172            strategy = new SizeStrategy();
173        } else {
174            strategy = new AttributeStrategy();
175        }
176        return strategy;
177    }
178
179    private interface BitmapTracker {
180        void add(Bitmap bitmap);
181        void remove(Bitmap bitmap);
182    }
183
184    @SuppressWarnings("unused")
185    // Only used for debugging
186    private static class ThrowingBitmapTracker implements BitmapTracker {
187        private final Set<Bitmap> bitmaps = Collections.synchronizedSet(new HashSet<Bitmap>());
188
189        @Override
190        public void add(Bitmap bitmap) {
191            if (bitmaps.contains(bitmap)) {
192                throw new IllegalStateException("Can't add already added bitmap: " + bitmap + " [" + bitmap.getWidth()
193                        + "x" + bitmap.getHeight() + "]");
194            }
195            bitmaps.add(bitmap);
196        }
197
198        @Override
199        public void remove(Bitmap bitmap) {
200            if (!bitmaps.contains(bitmap)) {
201                throw new IllegalStateException("Cannot remove bitmap not in tracker");
202            }
203            bitmaps.remove(bitmap);
204        }
205    }
206
207    private static class NullBitmapTracker implements BitmapTracker {
208        @Override
209        public void add(Bitmap bitmap) {
210            // Do nothing.
211        }
212
213        @Override
214        public void remove(Bitmap bitmap) {
215            // Do nothing.
216        }
217    }
218}
219