1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tv.util; 18 19 import android.support.annotation.VisibleForTesting; 20 import android.util.Log; 21 import android.util.LruCache; 22 23 import com.android.tv.common.MemoryManageable; 24 import com.android.tv.util.BitmapUtils.ScaledBitmapInfo; 25 26 /** 27 * A convenience class for caching bitmap. 28 */ 29 public class ImageCache implements MemoryManageable { 30 private static final float MAX_CACHE_SIZE_PERCENT = 0.8f; 31 private static final float MIN_CACHE_SIZE_PERCENT = 0.05f; 32 private static final float DEFAULT_CACHE_SIZE_PERCENT = 0.1f; 33 private static final boolean DEBUG = false; 34 private static final String TAG = "ImageCache"; 35 private static final int MIN_CACHE_SIZE_KBYTES = 1024; 36 37 private final LruCache<String, ScaledBitmapInfo> mMemoryCache; 38 39 /** 40 * Creates a new ImageCache object with a given cache size percent. 41 * 42 * @param memCacheSizePercent The cache size as a percent of available app memory. 43 */ 44 private ImageCache(float memCacheSizePercent) { 45 int memCacheSize = calculateMemCacheSize(memCacheSizePercent); 46 47 // Set up memory cache 48 if (DEBUG) { 49 Log.d(TAG, "Memory cache created (size = " + memCacheSize + " Kbytes)"); 50 } 51 mMemoryCache = new LruCache<String, ScaledBitmapInfo>(memCacheSize) { 52 /** 53 * Measure item size in kilobytes rather than units which is more practical for a bitmap 54 * cache 55 */ 56 @Override 57 protected int sizeOf(String key, ScaledBitmapInfo bitmapInfo) { 58 return (bitmapInfo.bitmap.getByteCount() + 1023) / 1024; 59 } 60 }; 61 } 62 63 private static ImageCache sImageCache; 64 65 /** 66 * Returns an existing ImageCache, if it doesn't exist, a new one is created using the supplied 67 * param. 68 * 69 * @param memCacheSizePercent The cache size as a percent of available app memory. Should be in 70 * range of MIN_CACHE_SIZE_PERCENT(0.05) ~ MAX_CACHE_SIZE_PERCENT(0.8). 71 * @return An existing retained ImageCache object or a new one if one did not exist 72 */ 73 public static synchronized ImageCache getInstance(float memCacheSizePercent) { 74 if (sImageCache == null) { 75 sImageCache = newInstance(memCacheSizePercent); 76 } 77 return sImageCache; 78 } 79 80 @VisibleForTesting 81 static ImageCache newInstance(float memCacheSizePercent) { 82 return new ImageCache(memCacheSizePercent); 83 } 84 85 86 /** 87 * Returns an existing ImageCache, if it doesn't exist, a new one is created using 88 * DEFAULT_CACHE_SIZE_PERCENT (0.1). 89 * 90 * @return An existing retained ImageCache object or a new one if one did not exist 91 */ 92 public static ImageCache getInstance() { 93 return getInstance(DEFAULT_CACHE_SIZE_PERCENT); 94 } 95 96 /** 97 * Adds a bitmap to memory cache. 98 * 99 * <p>If there is an existing bitmap only replace it if 100 * {@link ScaledBitmapInfo#needToReload(ScaledBitmapInfo)} is true. 101 * 102 * @param bitmapInfo The {@link ScaledBitmapInfo} object to store 103 */ 104 public void putIfNeeded(ScaledBitmapInfo bitmapInfo) { 105 if (bitmapInfo == null || bitmapInfo.id == null) { 106 throw new IllegalArgumentException("Neither bitmap nor bitmap.id should be null."); 107 } 108 String key = bitmapInfo.id; 109 // Add to memory cache 110 synchronized (mMemoryCache) { 111 ScaledBitmapInfo old = mMemoryCache.put(key, bitmapInfo); 112 if (old != null && !old.needToReload(bitmapInfo)) { 113 mMemoryCache.put(key, old); 114 if (DEBUG) { 115 Log.d(TAG, 116 "Kept original " + old + " in memory cache because it was larger than " 117 + bitmapInfo + "."); 118 } 119 } else { 120 if (DEBUG) { 121 Log.d(TAG, "Add " + bitmapInfo + " to memory cache. Current size is " + 122 mMemoryCache.size() + " / " + mMemoryCache.maxSize() + " Kbytes"); 123 } 124 } 125 } 126 } 127 128 /** 129 * Get from memory cache. 130 * 131 * @param key Unique identifier for which item to get 132 * @return The bitmap if found in cache, null otherwise 133 */ 134 public ScaledBitmapInfo get(String key) { 135 ScaledBitmapInfo memBitmapInfo = mMemoryCache.get(key); 136 if (DEBUG) { 137 int hit = mMemoryCache.hitCount(); 138 int miss = mMemoryCache.missCount(); 139 String result = memBitmapInfo == null ? "miss" : "hit"; 140 double ratio = ((double) hit) / (hit + miss) * 100; 141 Log.d(TAG, "Memory cache " + result + " for " + key); 142 Log.d(TAG, "Memory cache " + hit + "h:" + miss + "m " + ratio + "%"); 143 } 144 return memBitmapInfo; 145 } 146 147 /** 148 * Remove from memory cache. 149 * 150 * @param key Unique identifier for which item to remove 151 * @return The previous bitmap mapped by key 152 */ 153 public ScaledBitmapInfo remove(String key) { 154 return mMemoryCache.remove(key); 155 } 156 157 /** 158 * Calculates the memory cache size based on a percentage of the max available VM memory. Eg. 159 * setting percent to 0.2 would set the memory cache to one fifth of the available memory. 160 * Throws {@link IllegalArgumentException} if percent is < 0.05 or > .8. memCacheSize is stored 161 * in kilobytes instead of bytes as this will eventually be passed to construct a LruCache 162 * which takes an int in its constructor. This value should be chosen carefully based on a 163 * number of factors Refer to the corresponding Android Training class for more discussion: 164 * http://developer.android.com/training/displaying-bitmaps/ 165 * 166 * @param percent Percent of available app memory to use to size memory cache. 167 */ 168 public static int calculateMemCacheSize(float percent) { 169 if (percent < MIN_CACHE_SIZE_PERCENT || percent > MAX_CACHE_SIZE_PERCENT) { 170 throw new IllegalArgumentException("setMemCacheSizePercent - percent must be " 171 + "between 0.05 and 0.8 (inclusive)"); 172 } 173 return Math.max(MIN_CACHE_SIZE_KBYTES, 174 Math.round(percent * Runtime.getRuntime().maxMemory() / 1024)); 175 } 176 177 @Override 178 public void performTrimMemory(int level) { 179 mMemoryCache.evictAll(); 180 } 181 } 182