/* * 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.messaging.datamodel.media; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.os.SystemClock; import android.support.annotation.NonNull; import android.util.SparseArray; import com.android.messaging.Factory; import com.android.messaging.util.Assert; import com.android.messaging.util.LogUtil; import java.io.IOException; import java.io.InputStream; import java.util.LinkedList; /** * A media cache that holds image resources, which doubles as a bitmap pool that allows the * consumer to optionally decode image resources using unused bitmaps stored in the cache. */ public class PoolableImageCache extends MediaCache { private static final int MIN_TIME_IN_POOL = 5000; /** Encapsulates bitmap pool representation of the image cache */ private final ReusableImageResourcePool mReusablePoolAccessor = new ReusableImageResourcePool(); public PoolableImageCache(final int id, final String name) { this(DEFAULT_MEDIA_RESOURCE_CACHE_SIZE_IN_KILOBYTES, id, name); } public PoolableImageCache(final int maxSize, final int id, final String name) { super(maxSize, id, name); } /** * Creates a new BitmapFactory.Options for using the self-contained bitmap pool. */ public static BitmapFactory.Options getBitmapOptionsForPool(final boolean scaled, final int inputDensity, final int targetDensity) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inScaled = scaled; options.inDensity = inputDensity; options.inTargetDensity = targetDensity; options.inSampleSize = 1; options.inJustDecodeBounds = false; options.inMutable = true; return options; } @Override public synchronized ImageResource addResourceToCache(final String key, final ImageResource imageResource) { mReusablePoolAccessor.onResourceEnterCache(imageResource); return super.addResourceToCache(key, imageResource); } @Override protected synchronized void entryRemoved(final boolean evicted, final String key, final ImageResource oldValue, final ImageResource newValue) { mReusablePoolAccessor.onResourceLeaveCache(oldValue); super.entryRemoved(evicted, key, oldValue, newValue); } /** * Returns a representation of the image cache as a reusable bitmap pool. */ public ReusableImageResourcePool asReusableBitmapPool() { return mReusablePoolAccessor; } /** * A bitmap pool representation built on top of the image cache. It treats the image resources * stored in the image cache as a self-contained bitmap pool and is able to create or * reclaim bitmap resource as needed. */ public class ReusableImageResourcePool { private static final int MAX_SUPPORTED_IMAGE_DIMENSION = 0xFFFF; private static final int INVALID_POOL_KEY = 0; /** * Number of reuse failures to skip before reporting. * For debugging purposes, change to a lower number for more frequent reporting. */ private static final int FAILED_REPORTING_FREQUENCY = 100; /** * Count of reuse failures which have occurred. */ private volatile int mFailedBitmapReuseCount = 0; /** * Count of reuse successes which have occurred. */ private volatile int mSucceededBitmapReuseCount = 0; /** * A sparse array from bitmap size to a list of image cache entries that match the * given size. This map is used to quickly retrieve a usable bitmap to be reused by an * incoming ImageRequest. We need to ensure that this sparse array always contains only * elements currently in the image cache with no other consumer. */ private final SparseArray> mImageListSparseArray; public ReusableImageResourcePool() { mImageListSparseArray = new SparseArray>(); } /** * Load an input stream into a bitmap. Uses a bitmap from the pool if possible to reduce * memory turnover. * @param inputStream InputStream load. Cannot be null. * @param optionsTmp Should be the same options returned from getBitmapOptionsForPool(). * Cannot be null. * @param width The width of the bitmap. * @param height The height of the bitmap. * @return The decoded Bitmap with the resource drawn in it. * @throws IOException */ public Bitmap decodeSampledBitmapFromInputStream(@NonNull final InputStream inputStream, @NonNull final BitmapFactory.Options optionsTmp, final int width, final int height) throws IOException { if (width <= 0 || height <= 0) { // This is an invalid / corrupted image of zero size. LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "PoolableImageCache: Decoding bitmap with " + "invalid size"); throw new IOException("Invalid size / corrupted image"); } Assert.notNull(inputStream); assignPoolBitmap(optionsTmp, width, height); Bitmap b = null; try { b = BitmapFactory.decodeStream(inputStream, null, optionsTmp); mSucceededBitmapReuseCount++; } catch (final IllegalArgumentException e) { // BitmapFactory couldn't decode the file, try again without an inputBufferBitmap. if (optionsTmp.inBitmap != null) { optionsTmp.inBitmap.recycle(); optionsTmp.inBitmap = null; b = BitmapFactory.decodeStream(inputStream, null, optionsTmp); onFailedToReuse(); } } catch (final OutOfMemoryError e) { LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "Oom decoding inputStream"); Factory.get().reclaimMemory(); } return b; } /** * Turn encoded bytes into a bitmap. Uses a bitmap from the pool if possible to reduce * memory turnover. * @param bytes Encoded bytes to draw on the bitmap. Cannot be null. * @param optionsTmp The bitmap will set here and the input should be generated from * getBitmapOptionsForPool(). Cannot be null. * @param width The width of the bitmap. * @param height The height of the bitmap. * @return A Bitmap with the encoded bytes drawn in it. * @throws IOException */ public Bitmap decodeByteArray(@NonNull final byte[] bytes, @NonNull final BitmapFactory.Options optionsTmp, final int width, final int height) throws OutOfMemoryError, IOException { if (width <= 0 || height <= 0) { // This is an invalid / corrupted image of zero size. LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "PoolableImageCache: Decoding bitmap with " + "invalid size"); throw new IOException("Invalid size / corrupted image"); } Assert.notNull(bytes); Assert.notNull(optionsTmp); assignPoolBitmap(optionsTmp, width, height); Bitmap b = null; try { b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp); mSucceededBitmapReuseCount++; } catch (final IllegalArgumentException e) { // BitmapFactory couldn't decode the file, try again without an inputBufferBitmap. // (i.e. without the bitmap from the pool) if (optionsTmp.inBitmap != null) { optionsTmp.inBitmap.recycle(); optionsTmp.inBitmap = null; b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp); onFailedToReuse(); } } catch (final OutOfMemoryError e) { LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "Oom decoding inputStream"); Factory.get().reclaimMemory(); } return b; } /** * Called when a new image resource is added to the cache. We add the resource to the * pool so it's properly keyed into the pool structure. */ void onResourceEnterCache(final ImageResource imageResource) { if (getPoolKey(imageResource) != INVALID_POOL_KEY) { addResourceToPool(imageResource); } } /** * Called when an image resource is evicted from the cache. Bitmap pool's entries are * strictly tied to their presence in the image cache. Once an image is evicted from the * cache, it should be removed from the pool. */ void onResourceLeaveCache(final ImageResource imageResource) { if (getPoolKey(imageResource) != INVALID_POOL_KEY) { removeResourceFromPool(imageResource); } } private void addResourceToPool(final ImageResource imageResource) { synchronized (PoolableImageCache.this) { final int poolKey = getPoolKey(imageResource); Assert.isTrue(poolKey != INVALID_POOL_KEY); LinkedList imageList = mImageListSparseArray.get(poolKey); if (imageList == null) { imageList = new LinkedList(); mImageListSparseArray.put(poolKey, imageList); } imageList.addLast(imageResource); } } private void removeResourceFromPool(final ImageResource imageResource) { synchronized (PoolableImageCache.this) { final int poolKey = getPoolKey(imageResource); Assert.isTrue(poolKey != INVALID_POOL_KEY); final LinkedList imageList = mImageListSparseArray.get(poolKey); if (imageList != null) { imageList.remove(imageResource); } } } /** * Try to get a reusable bitmap from the pool with the given width and height. As a * result of this call, the caller will assume ownership of the returned bitmap. */ private Bitmap getReusableBitmapFromPool(final int width, final int height) { synchronized (PoolableImageCache.this) { final int poolKey = getPoolKey(width, height); if (poolKey != INVALID_POOL_KEY) { final LinkedList images = mImageListSparseArray.get(poolKey); if (images != null && images.size() > 0) { // Try to reuse the first available bitmap from the pool list. We start from // the least recently added cache entry of the given size. ImageResource imageToUse = null; for (int i = 0; i < images.size(); i++) { final ImageResource image = images.get(i); if (image.getRefCount() == 1) { image.acquireLock(); if (image.getRefCount() == 1) { // The image is only used by the cache, so it's reusable. imageToUse = images.remove(i); break; } else { // Logically, this shouldn't happen, because as soon as the // cache is the only user of this resource, it will not be // used by anyone else until the next cache access, but we // currently hold on to the cache lock. But technically // future changes may violate this assumption, so warn about // this. LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "Image refCount changed " + "from 1 in getReusableBitmapFromPool()"); image.releaseLock(); } } } if (imageToUse == null) { return null; } try { imageToUse.assertLockHeldByCurrentThread(); // Only reuse the bitmap if the last time we use was greater than 5s. // This allows the cache a chance to reuse instead of always taking the // oldest. final long timeSinceLastRef = SystemClock.elapsedRealtime() - imageToUse.getLastRefAddTimestamp(); if (timeSinceLastRef < MIN_TIME_IN_POOL) { if (LogUtil.isLoggable(LogUtil.BUGLE_IMAGE_TAG, LogUtil.VERBOSE)) { LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, "Not reusing reusing " + "first available bitmap from the pool because it " + "has not been in the pool long enough. " + "timeSinceLastRef=" + timeSinceLastRef); } // Put back the image and return no reuseable bitmap. images.addLast(imageToUse); return null; } // Add a temp ref on the image resource so it won't be GC'd after // being removed from the cache. imageToUse.addRef(); // Remove the image resource from the image cache. final ImageResource removed = remove(imageToUse.getKey()); Assert.isTrue(removed == imageToUse); // Try to reuse the bitmap from the image resource. This will transfer // ownership of the bitmap object to the caller of this method. final Bitmap reusableBitmap = imageToUse.reuseBitmap(); imageToUse.release(); return reusableBitmap; } finally { // We are either done with the reuse operation, or decided not to use // the image. Either way, release the lock. imageToUse.releaseLock(); } } } } return null; } /** * Try to locate and return a reusable bitmap from the pool, or create a new bitmap. * @param width desired bitmap width * @param height desired bitmap height * @return the created or reused mutable bitmap that has its background cleared to * {@value Color#TRANSPARENT} */ public Bitmap createOrReuseBitmap(final int width, final int height) { return createOrReuseBitmap(width, height, Color.TRANSPARENT); } /** * Try to locate and return a reusable bitmap from the pool, or create a new bitmap. * @param width desired bitmap width * @param height desired bitmap height * @param backgroundColor the background color for the returned bitmap * @return the created or reused mutable bitmap with the requested background color */ public Bitmap createOrReuseBitmap(final int width, final int height, final int backgroundColor) { Bitmap retBitmap = null; try { final Bitmap poolBitmap = getReusableBitmapFromPool(width, height); retBitmap = (poolBitmap != null) ? poolBitmap : Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); retBitmap.eraseColor(backgroundColor); } catch (final OutOfMemoryError e) { LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "PoolableImageCache:try to createOrReuseBitmap"); Factory.get().reclaimMemory(); } return retBitmap; } private void assignPoolBitmap(final BitmapFactory.Options optionsTmp, final int width, final int height) { if (optionsTmp.inJustDecodeBounds) { return; } optionsTmp.inBitmap = getReusableBitmapFromPool(width, height); } /** * @return The pool key for the provided image dimensions or 0 if either width or height is * greater than the max supported image dimension. */ private int getPoolKey(final int width, final int height) { if (width > MAX_SUPPORTED_IMAGE_DIMENSION || height > MAX_SUPPORTED_IMAGE_DIMENSION) { return INVALID_POOL_KEY; } return (width << 16) | height; } /** * @return the pool key for a given image resource. */ private int getPoolKey(final ImageResource imageResource) { if (imageResource.supportsBitmapReuse()) { final Bitmap bitmap = imageResource.getBitmap(); if (bitmap != null && bitmap.isMutable()) { final int width = bitmap.getWidth(); final int height = bitmap.getHeight(); if (width > 0 && height > 0) { return getPoolKey(width, height); } } } return INVALID_POOL_KEY; } /** * Called when bitmap reuse fails. Conditionally report the failure with statistics. */ private void onFailedToReuse() { mFailedBitmapReuseCount++; if (mFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) { LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "Pooled bitmap consistently not being reused. Failure count = " + mFailedBitmapReuseCount + ", success count = " + mSucceededBitmapReuseCount); } } } }