/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tv.util; import android.support.annotation.VisibleForTesting; import android.util.Log; import android.util.LruCache; import com.android.tv.common.MemoryManageable; import com.android.tv.util.BitmapUtils.ScaledBitmapInfo; /** * A convenience class for caching bitmap. */ public class ImageCache implements MemoryManageable { private static final float MAX_CACHE_SIZE_PERCENT = 0.8f; private static final float MIN_CACHE_SIZE_PERCENT = 0.05f; private static final float DEFAULT_CACHE_SIZE_PERCENT = 0.1f; private static final boolean DEBUG = false; private static final String TAG = "ImageCache"; private static final int MIN_CACHE_SIZE_KBYTES = 1024; private final LruCache mMemoryCache; /** * Creates a new ImageCache object with a given cache size percent. * * @param memCacheSizePercent The cache size as a percent of available app memory. */ private ImageCache(float memCacheSizePercent) { int memCacheSize = calculateMemCacheSize(memCacheSizePercent); // Set up memory cache if (DEBUG) { Log.d(TAG, "Memory cache created (size = " + memCacheSize + " Kbytes)"); } mMemoryCache = new LruCache(memCacheSize) { /** * Measure item size in kilobytes rather than units which is more practical for a bitmap * cache */ @Override protected int sizeOf(String key, ScaledBitmapInfo bitmapInfo) { return (bitmapInfo.bitmap.getByteCount() + 1023) / 1024; } }; } private static ImageCache sImageCache; /** * Returns an existing ImageCache, if it doesn't exist, a new one is created using the supplied * param. * * @param memCacheSizePercent The cache size as a percent of available app memory. Should be in * range of MIN_CACHE_SIZE_PERCENT(0.05) ~ MAX_CACHE_SIZE_PERCENT(0.8). * @return An existing retained ImageCache object or a new one if one did not exist */ public static synchronized ImageCache getInstance(float memCacheSizePercent) { if (sImageCache == null) { sImageCache = newInstance(memCacheSizePercent); } return sImageCache; } @VisibleForTesting static ImageCache newInstance(float memCacheSizePercent) { return new ImageCache(memCacheSizePercent); } /** * Returns an existing ImageCache, if it doesn't exist, a new one is created using * DEFAULT_CACHE_SIZE_PERCENT (0.1). * * @return An existing retained ImageCache object or a new one if one did not exist */ public static ImageCache getInstance() { return getInstance(DEFAULT_CACHE_SIZE_PERCENT); } /** * Adds a bitmap to memory cache. * *

If there is an existing bitmap only replace it if * {@link ScaledBitmapInfo#needToReload(ScaledBitmapInfo)} is true. * * @param bitmapInfo The {@link ScaledBitmapInfo} object to store */ public void putIfNeeded(ScaledBitmapInfo bitmapInfo) { if (bitmapInfo == null || bitmapInfo.id == null) { throw new IllegalArgumentException("Neither bitmap nor bitmap.id should be null."); } String key = bitmapInfo.id; // Add to memory cache synchronized (mMemoryCache) { ScaledBitmapInfo old = mMemoryCache.put(key, bitmapInfo); if (old != null && !old.needToReload(bitmapInfo)) { mMemoryCache.put(key, old); if (DEBUG) { Log.d(TAG, "Kept original " + old + " in memory cache because it was larger than " + bitmapInfo + "."); } } else { if (DEBUG) { Log.d(TAG, "Add " + bitmapInfo + " to memory cache. Current size is " + mMemoryCache.size() + " / " + mMemoryCache.maxSize() + " Kbytes"); } } } } /** * Get from memory cache. * * @param key Unique identifier for which item to get * @return The bitmap if found in cache, null otherwise */ public ScaledBitmapInfo get(String key) { ScaledBitmapInfo memBitmapInfo = mMemoryCache.get(key); if (DEBUG) { int hit = mMemoryCache.hitCount(); int miss = mMemoryCache.missCount(); String result = memBitmapInfo == null ? "miss" : "hit"; double ratio = ((double) hit) / (hit + miss) * 100; Log.d(TAG, "Memory cache " + result + " for " + key); Log.d(TAG, "Memory cache " + hit + "h:" + miss + "m " + ratio + "%"); } return memBitmapInfo; } /** * Remove from memory cache. * * @param key Unique identifier for which item to remove * @return The previous bitmap mapped by key */ public ScaledBitmapInfo remove(String key) { return mMemoryCache.remove(key); } /** * Calculates the memory cache size based on a percentage of the max available VM memory. Eg. * setting percent to 0.2 would set the memory cache to one fifth of the available memory. * Throws {@link IllegalArgumentException} if percent is < 0.05 or > .8. memCacheSize is stored * in kilobytes instead of bytes as this will eventually be passed to construct a LruCache * which takes an int in its constructor. This value should be chosen carefully based on a * number of factors Refer to the corresponding Android Training class for more discussion: * http://developer.android.com/training/displaying-bitmaps/ * * @param percent Percent of available app memory to use to size memory cache. */ public static int calculateMemCacheSize(float percent) { if (percent < MIN_CACHE_SIZE_PERCENT || percent > MAX_CACHE_SIZE_PERCENT) { throw new IllegalArgumentException("setMemCacheSizePercent - percent must be " + "between 0.05 and 0.8 (inclusive)"); } return Math.max(MIN_CACHE_SIZE_KBYTES, Math.round(percent * Runtime.getRuntime().maxMemory() / 1024)); } @Override public void performTrimMemory(int level) { mMemoryCache.evictAll(); } }