1 package com.bumptech.glide.load.engine.prefill; 2 3 import android.graphics.Bitmap; 4 import android.os.Handler; 5 import android.os.Looper; 6 import android.os.SystemClock; 7 import android.util.Log; 8 9 import com.bumptech.glide.load.Key; 10 import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; 11 import com.bumptech.glide.load.engine.cache.MemoryCache; 12 import com.bumptech.glide.load.resource.bitmap.BitmapResource; 13 import com.bumptech.glide.util.Util; 14 15 import java.io.UnsupportedEncodingException; 16 import java.security.MessageDigest; 17 import java.util.HashSet; 18 import java.util.Set; 19 import java.util.concurrent.TimeUnit; 20 21 /** 22 * A class that allocates {@link android.graphics.Bitmap Bitmaps} to make sure that the 23 * {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} is pre-populated. 24 * 25 * <p>By posting to the main thread with backoffs, we try to avoid ANRs when the garbage collector gets into a state 26 * where a high percentage of {@link Bitmap} allocations trigger a stop the world GC. We try to detect whether or not a 27 * GC has occurred by only allowing our allocator to run for a limited number of milliseconds. Since the allocations 28 * themselves very fast, a GC is the most likely reason for a substantial delay. If we detect our allocator has run for 29 * more than our limit, we assume a GC has occurred, stop the current allocations, and try again after a delay. 30 */ 31 final class BitmapPreFillRunner implements Runnable { 32 private static final String TAG = "PreFillRunner"; 33 private static final Clock DEFAULT_CLOCK = new Clock(); 34 35 /** 36 * The maximum number of millis we can run before posting. Set to match and detect the duration of non concurrent 37 * GCs. 38 */ 39 static final long MAX_DURATION_MS = 32; 40 41 /** 42 * The amount of time in ms we wait before continuing to allocate after the first GC is detected. 43 */ 44 static final long INITIAL_BACKOFF_MS = 40; 45 46 /** 47 * The amount by which the current backoff time is multiplied each time we detect a GC. 48 */ 49 static final int BACKOFF_RATIO = 4; 50 51 /** 52 * The maximum amount of time in ms we wait before continuing to allocate. 53 */ 54 static final long MAX_BACKOFF_MS = TimeUnit.SECONDS.toMillis(1); 55 56 private final BitmapPool bitmapPool; 57 private final MemoryCache memoryCache; 58 private final PreFillQueue toPrefill; 59 private final Clock clock; 60 private final Set<PreFillType> seenTypes = new HashSet<PreFillType>(); 61 private final Handler handler; 62 63 private long currentDelay = INITIAL_BACKOFF_MS; 64 private boolean isCancelled; 65 66 public BitmapPreFillRunner(BitmapPool bitmapPool, MemoryCache memoryCache, PreFillQueue allocationOrder) { 67 this(bitmapPool, memoryCache, allocationOrder, DEFAULT_CLOCK, new Handler(Looper.getMainLooper())); 68 } 69 70 // Visible for testing. 71 BitmapPreFillRunner(BitmapPool bitmapPool, MemoryCache memoryCache, PreFillQueue allocationOrder, Clock clock, 72 Handler handler) { 73 this.bitmapPool = bitmapPool; 74 this.memoryCache = memoryCache; 75 this.toPrefill = allocationOrder; 76 this.clock = clock; 77 this.handler = handler; 78 } 79 80 public void cancel() { 81 isCancelled = true; 82 } 83 84 /** 85 * Attempts to allocate {@link android.graphics.Bitmap}s and returns {@code true} if there are more 86 * {@link android.graphics.Bitmap}s to allocate and {@code false} otherwise. 87 */ 88 private boolean allocate() { 89 long start = clock.now(); 90 while (!toPrefill.isEmpty() && !isGcDetected(start)) { 91 PreFillType toAllocate = toPrefill.remove(); 92 Bitmap bitmap = Bitmap.createBitmap(toAllocate.getWidth(), toAllocate.getHeight(), 93 toAllocate.getConfig()); 94 95 // Don't over fill the memory cache to avoid evicting useful resources, but make sure it's not empty so 96 // we use all available space. 97 if (getFreeMemoryCacheBytes() >= Util.getBitmapByteSize(bitmap)) { 98 memoryCache.put(new UniqueKey(), BitmapResource.obtain(bitmap, bitmapPool)); 99 } else { 100 addToBitmapPool(toAllocate, bitmap); 101 } 102 103 if (Log.isLoggable(TAG, Log.DEBUG)) { 104 Log.d(TAG, "allocated [" + toAllocate.getWidth() + "x" + toAllocate.getHeight() + "] " 105 + toAllocate.getConfig() + " size: " + Util.getBitmapByteSize(bitmap)); 106 } 107 } 108 109 return !isCancelled && !toPrefill.isEmpty(); 110 } 111 112 private boolean isGcDetected(long startTimeMs) { 113 return clock.now() - startTimeMs >= MAX_DURATION_MS; 114 } 115 116 private int getFreeMemoryCacheBytes() { 117 return memoryCache.getMaxSize() - memoryCache.getCurrentSize(); 118 } 119 120 private void addToBitmapPool(PreFillType toAllocate, Bitmap bitmap) { 121 // The pool may not move sizes to the front of the LRU on put. Do a get here to make sure the size we're adding 122 // is at the front of the queue so that the Bitmap we're adding won't be evicted immediately. 123 if (seenTypes.add(toAllocate)) { 124 Bitmap fromPool = bitmapPool.get(toAllocate.getWidth(), toAllocate.getHeight(), 125 toAllocate.getConfig()); 126 if (fromPool != null) { 127 bitmapPool.put(fromPool); 128 } 129 } 130 131 bitmapPool.put(bitmap); 132 } 133 134 @Override 135 public void run() { 136 if (allocate()) { 137 handler.postDelayed(this, getNextDelay()); 138 } 139 } 140 141 private long getNextDelay() { 142 long result = currentDelay; 143 currentDelay = Math.min(currentDelay * BACKOFF_RATIO, MAX_BACKOFF_MS); 144 return result; 145 } 146 147 private static class UniqueKey implements Key { 148 149 @Override 150 public void updateDiskCacheKey(MessageDigest messageDigest) throws UnsupportedEncodingException { 151 // Do nothing. 152 } 153 } 154 155 // Visible for testing. 156 static class Clock { 157 public long now() { 158 return SystemClock.currentThreadTimeMillis(); 159 } 160 } 161 } 162