Home | History | Annotate | Download | only in media
      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 package com.android.messaging.datamodel.media;
     17 
     18 import android.graphics.Bitmap;
     19 import android.graphics.BitmapFactory;
     20 import android.graphics.Color;
     21 import android.os.SystemClock;
     22 import android.support.annotation.NonNull;
     23 import android.util.SparseArray;
     24 
     25 import com.android.messaging.Factory;
     26 import com.android.messaging.util.Assert;
     27 import com.android.messaging.util.LogUtil;
     28 
     29 import java.io.IOException;
     30 import java.io.InputStream;
     31 import java.util.LinkedList;
     32 
     33 /**
     34  * A media cache that holds image resources, which doubles as a bitmap pool that allows the
     35  * consumer to optionally decode image resources using unused bitmaps stored in the cache.
     36  */
     37 public class PoolableImageCache extends MediaCache<ImageResource> {
     38     private static final int MIN_TIME_IN_POOL = 5000;
     39 
     40     /** Encapsulates bitmap pool representation of the image cache */
     41     private final ReusableImageResourcePool mReusablePoolAccessor = new ReusableImageResourcePool();
     42 
     43     public PoolableImageCache(final int id, final String name) {
     44         this(DEFAULT_MEDIA_RESOURCE_CACHE_SIZE_IN_KILOBYTES, id, name);
     45     }
     46 
     47     public PoolableImageCache(final int maxSize, final int id, final String name) {
     48         super(maxSize, id, name);
     49     }
     50 
     51     /**
     52      * Creates a new BitmapFactory.Options for using the self-contained bitmap pool.
     53      */
     54     public static BitmapFactory.Options getBitmapOptionsForPool(final boolean scaled,
     55             final int inputDensity, final int targetDensity) {
     56         final BitmapFactory.Options options = new BitmapFactory.Options();
     57         options.inScaled = scaled;
     58         options.inDensity = inputDensity;
     59         options.inTargetDensity = targetDensity;
     60         options.inSampleSize = 1;
     61         options.inJustDecodeBounds = false;
     62         options.inMutable = true;
     63         return options;
     64     }
     65 
     66     @Override
     67     public synchronized ImageResource addResourceToCache(final String key,
     68             final ImageResource imageResource) {
     69         mReusablePoolAccessor.onResourceEnterCache(imageResource);
     70         return super.addResourceToCache(key, imageResource);
     71     }
     72 
     73     @Override
     74     protected synchronized void entryRemoved(final boolean evicted, final String key,
     75             final ImageResource oldValue, final ImageResource newValue) {
     76         mReusablePoolAccessor.onResourceLeaveCache(oldValue);
     77         super.entryRemoved(evicted, key, oldValue, newValue);
     78     }
     79 
     80     /**
     81      * Returns a representation of the image cache as a reusable bitmap pool.
     82      */
     83     public ReusableImageResourcePool asReusableBitmapPool() {
     84         return mReusablePoolAccessor;
     85     }
     86 
     87     /**
     88      * A bitmap pool representation built on top of the image cache. It treats the image resources
     89      * stored in the image cache as a self-contained bitmap pool and is able to create or
     90      * reclaim bitmap resource as needed.
     91      */
     92     public class ReusableImageResourcePool {
     93         private static final int MAX_SUPPORTED_IMAGE_DIMENSION = 0xFFFF;
     94         private static final int INVALID_POOL_KEY = 0;
     95 
     96         /**
     97          * Number of reuse failures to skip before reporting.
     98          * For debugging purposes, change to a lower number for more frequent reporting.
     99          */
    100         private static final int FAILED_REPORTING_FREQUENCY = 100;
    101 
    102         /**
    103          * Count of reuse failures which have occurred.
    104          */
    105         private volatile int mFailedBitmapReuseCount = 0;
    106 
    107         /**
    108          * Count of reuse successes which have occurred.
    109          */
    110         private volatile int mSucceededBitmapReuseCount = 0;
    111 
    112         /**
    113          * A sparse array from bitmap size to a list of image cache entries that match the
    114          * given size. This map is used to quickly retrieve a usable bitmap to be reused by an
    115          * incoming ImageRequest. We need to ensure that this sparse array always contains only
    116          * elements currently in the image cache with no other consumer.
    117          */
    118         private final SparseArray<LinkedList<ImageResource>> mImageListSparseArray;
    119 
    120         public ReusableImageResourcePool() {
    121             mImageListSparseArray = new SparseArray<LinkedList<ImageResource>>();
    122         }
    123 
    124         /**
    125          * Load an input stream into a bitmap. Uses a bitmap from the pool if possible to reduce
    126          * memory turnover.
    127          * @param inputStream InputStream load. Cannot be null.
    128          * @param optionsTmp Should be the same options returned from getBitmapOptionsForPool().
    129          * Cannot be null.
    130          * @param width The width of the bitmap.
    131          * @param height The height of the bitmap.
    132          * @return The decoded Bitmap with the resource drawn in it.
    133          * @throws IOException
    134          */
    135         public Bitmap decodeSampledBitmapFromInputStream(@NonNull final InputStream inputStream,
    136                 @NonNull final BitmapFactory.Options optionsTmp,
    137                 final int width, final int height) throws IOException {
    138             if (width <= 0 || height <= 0) {
    139                 // This is an invalid / corrupted image of zero size.
    140                 LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "PoolableImageCache: Decoding bitmap with " +
    141                         "invalid size");
    142                 throw new IOException("Invalid size / corrupted image");
    143             }
    144             Assert.notNull(inputStream);
    145             assignPoolBitmap(optionsTmp, width, height);
    146             Bitmap b = null;
    147             try {
    148                 b = BitmapFactory.decodeStream(inputStream, null, optionsTmp);
    149                 mSucceededBitmapReuseCount++;
    150             } catch (final IllegalArgumentException e) {
    151                 // BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
    152                 if (optionsTmp.inBitmap != null) {
    153                     optionsTmp.inBitmap.recycle();
    154                     optionsTmp.inBitmap = null;
    155                     b = BitmapFactory.decodeStream(inputStream, null, optionsTmp);
    156                     onFailedToReuse();
    157                 }
    158             } catch (final OutOfMemoryError e) {
    159                 LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "Oom decoding inputStream");
    160                 Factory.get().reclaimMemory();
    161             }
    162             return b;
    163         }
    164 
    165         /**
    166          * Turn encoded bytes into a bitmap. Uses a bitmap from the pool if possible to reduce
    167          * memory turnover.
    168          * @param bytes Encoded bytes to draw on the bitmap. Cannot be null.
    169          * @param optionsTmp The bitmap will set here and the input should be generated from
    170          * getBitmapOptionsForPool(). Cannot be null.
    171          * @param width The width of the bitmap.
    172          * @param height The height of the bitmap.
    173          * @return A Bitmap with the encoded bytes drawn in it.
    174          * @throws IOException
    175          */
    176         public Bitmap decodeByteArray(@NonNull final byte[] bytes,
    177                 @NonNull final BitmapFactory.Options optionsTmp, final int width,
    178                 final int height) throws OutOfMemoryError, IOException {
    179             if (width <= 0 || height <= 0) {
    180                 // This is an invalid / corrupted image of zero size.
    181                 LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "PoolableImageCache: Decoding bitmap with " +
    182                         "invalid size");
    183                 throw new IOException("Invalid size / corrupted image");
    184             }
    185             Assert.notNull(bytes);
    186             Assert.notNull(optionsTmp);
    187             assignPoolBitmap(optionsTmp, width, height);
    188             Bitmap b = null;
    189             try {
    190                 b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp);
    191                 mSucceededBitmapReuseCount++;
    192             } catch (final IllegalArgumentException e) {
    193                 // BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
    194                 // (i.e. without the bitmap from the pool)
    195                 if (optionsTmp.inBitmap != null) {
    196                     optionsTmp.inBitmap.recycle();
    197                     optionsTmp.inBitmap = null;
    198                     b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp);
    199                     onFailedToReuse();
    200                 }
    201             } catch (final OutOfMemoryError e) {
    202                 LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "Oom decoding inputStream");
    203                 Factory.get().reclaimMemory();
    204             }
    205             return b;
    206         }
    207 
    208         /**
    209          * Called when a new image resource is added to the cache. We add the resource to the
    210          * pool so it's properly keyed into the pool structure.
    211          */
    212         void onResourceEnterCache(final ImageResource imageResource) {
    213             if (getPoolKey(imageResource) != INVALID_POOL_KEY) {
    214                 addResourceToPool(imageResource);
    215             }
    216         }
    217 
    218         /**
    219          * Called when an image resource is evicted from the cache. Bitmap pool's entries are
    220          * strictly tied to their presence in the image cache. Once an image is evicted from the
    221          * cache, it should be removed from the pool.
    222          */
    223         void onResourceLeaveCache(final ImageResource imageResource) {
    224             if (getPoolKey(imageResource) != INVALID_POOL_KEY) {
    225                 removeResourceFromPool(imageResource);
    226             }
    227         }
    228 
    229         private void addResourceToPool(final ImageResource imageResource) {
    230             synchronized (PoolableImageCache.this) {
    231                 final int poolKey = getPoolKey(imageResource);
    232                 Assert.isTrue(poolKey != INVALID_POOL_KEY);
    233                 LinkedList<ImageResource> imageList = mImageListSparseArray.get(poolKey);
    234                 if (imageList == null) {
    235                     imageList = new LinkedList<ImageResource>();
    236                     mImageListSparseArray.put(poolKey, imageList);
    237                 }
    238                 imageList.addLast(imageResource);
    239             }
    240         }
    241 
    242         private void removeResourceFromPool(final ImageResource imageResource) {
    243             synchronized (PoolableImageCache.this) {
    244                 final int poolKey = getPoolKey(imageResource);
    245                 Assert.isTrue(poolKey != INVALID_POOL_KEY);
    246                 final LinkedList<ImageResource> imageList = mImageListSparseArray.get(poolKey);
    247                 if (imageList != null) {
    248                     imageList.remove(imageResource);
    249                 }
    250             }
    251         }
    252 
    253         /**
    254          * Try to get a reusable bitmap from the pool with the given width and height. As a
    255          * result of this call, the caller will assume ownership of the returned bitmap.
    256          */
    257         private Bitmap getReusableBitmapFromPool(final int width, final int height) {
    258             synchronized (PoolableImageCache.this) {
    259                 final int poolKey = getPoolKey(width, height);
    260                 if (poolKey != INVALID_POOL_KEY) {
    261                     final LinkedList<ImageResource> images = mImageListSparseArray.get(poolKey);
    262                     if (images != null && images.size() > 0) {
    263                         // Try to reuse the first available bitmap from the pool list. We start from
    264                         // the least recently added cache entry of the given size.
    265                         ImageResource imageToUse = null;
    266                         for (int i = 0; i < images.size(); i++) {
    267                             final ImageResource image = images.get(i);
    268                             if (image.getRefCount() == 1) {
    269                                 image.acquireLock();
    270                                 if (image.getRefCount() == 1) {
    271                                     // The image is only used by the cache, so it's reusable.
    272                                     imageToUse = images.remove(i);
    273                                     break;
    274                                 } else {
    275                                     // Logically, this shouldn't happen, because as soon as the
    276                                     // cache is the only user of this resource, it will not be
    277                                     // used by anyone else until the next cache access, but we
    278                                     // currently hold on to the cache lock. But technically
    279                                     // future changes may violate this assumption, so warn about
    280                                     // this.
    281                                     LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "Image refCount changed " +
    282                                             "from 1 in getReusableBitmapFromPool()");
    283                                     image.releaseLock();
    284                                 }
    285                             }
    286                         }
    287 
    288                         if (imageToUse == null) {
    289                             return null;
    290                         }
    291 
    292                         try {
    293                             imageToUse.assertLockHeldByCurrentThread();
    294 
    295                             // Only reuse the bitmap if the last time we use was greater than 5s.
    296                             // This allows the cache a chance to reuse instead of always taking the
    297                             // oldest.
    298                             final long timeSinceLastRef = SystemClock.elapsedRealtime() -
    299                                     imageToUse.getLastRefAddTimestamp();
    300                             if (timeSinceLastRef < MIN_TIME_IN_POOL) {
    301                                 if (LogUtil.isLoggable(LogUtil.BUGLE_IMAGE_TAG, LogUtil.VERBOSE)) {
    302                                     LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, "Not reusing reusing " +
    303                                             "first available bitmap from the pool because it " +
    304                                             "has not been in the pool long enough. " +
    305                                             "timeSinceLastRef=" + timeSinceLastRef);
    306                                 }
    307                                 // Put back the image and return no reuseable bitmap.
    308                                 images.addLast(imageToUse);
    309                                 return null;
    310                             }
    311 
    312                             // Add a temp ref on the image resource so it won't be GC'd after
    313                             // being removed from the cache.
    314                             imageToUse.addRef();
    315 
    316                             // Remove the image resource from the image cache.
    317                             final ImageResource removed = remove(imageToUse.getKey());
    318                             Assert.isTrue(removed == imageToUse);
    319 
    320                             // Try to reuse the bitmap from the image resource. This will transfer
    321                             // ownership of the bitmap object to the caller of this method.
    322                             final Bitmap reusableBitmap = imageToUse.reuseBitmap();
    323 
    324                             imageToUse.release();
    325                             return reusableBitmap;
    326                         } finally {
    327                             // We are either done with the reuse operation, or decided not to use
    328                             // the image. Either way, release the lock.
    329                             imageToUse.releaseLock();
    330                         }
    331                     }
    332                 }
    333             }
    334             return null;
    335         }
    336 
    337         /**
    338          * Try to locate and return a reusable bitmap from the pool, or create a new bitmap.
    339          * @param width desired bitmap width
    340          * @param height desired bitmap height
    341          * @return the created or reused mutable bitmap that has its background cleared to
    342          * {@value Color#TRANSPARENT}
    343          */
    344         public Bitmap createOrReuseBitmap(final int width, final int height) {
    345             return createOrReuseBitmap(width, height, Color.TRANSPARENT);
    346         }
    347 
    348         /**
    349          * Try to locate and return a reusable bitmap from the pool, or create a new bitmap.
    350          * @param width desired bitmap width
    351          * @param height desired bitmap height
    352          * @param backgroundColor the background color for the returned bitmap
    353          * @return the created or reused mutable bitmap with the requested background color
    354          */
    355         public Bitmap createOrReuseBitmap(final int width, final int height,
    356                 final int backgroundColor) {
    357             Bitmap retBitmap = null;
    358             try {
    359                 final Bitmap poolBitmap = getReusableBitmapFromPool(width, height);
    360                 retBitmap = (poolBitmap != null) ? poolBitmap :
    361                         Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    362                 retBitmap.eraseColor(backgroundColor);
    363             } catch (final OutOfMemoryError e) {
    364                 LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "PoolableImageCache:try to createOrReuseBitmap");
    365                 Factory.get().reclaimMemory();
    366             }
    367             return retBitmap;
    368         }
    369 
    370         private void assignPoolBitmap(final BitmapFactory.Options optionsTmp, final int width,
    371                 final int height) {
    372             if (optionsTmp.inJustDecodeBounds) {
    373                 return;
    374             }
    375             optionsTmp.inBitmap = getReusableBitmapFromPool(width, height);
    376         }
    377 
    378         /**
    379          * @return The pool key for the provided image dimensions or 0 if either width or height is
    380          * greater than the max supported image dimension.
    381          */
    382         private int getPoolKey(final int width, final int height) {
    383             if (width > MAX_SUPPORTED_IMAGE_DIMENSION || height > MAX_SUPPORTED_IMAGE_DIMENSION) {
    384                 return INVALID_POOL_KEY;
    385             }
    386             return (width << 16) | height;
    387         }
    388 
    389         /**
    390          * @return the pool key for a given image resource.
    391          */
    392         private int getPoolKey(final ImageResource imageResource) {
    393             if (imageResource.supportsBitmapReuse()) {
    394                 final Bitmap bitmap = imageResource.getBitmap();
    395                 if (bitmap != null && bitmap.isMutable()) {
    396                     final int width = bitmap.getWidth();
    397                     final int height = bitmap.getHeight();
    398                     if (width > 0 && height > 0) {
    399                         return getPoolKey(width, height);
    400                     }
    401                 }
    402             }
    403             return INVALID_POOL_KEY;
    404         }
    405 
    406         /**
    407          * Called when bitmap reuse fails. Conditionally report the failure with statistics.
    408          */
    409         private void onFailedToReuse() {
    410             mFailedBitmapReuseCount++;
    411             if (mFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
    412                 LogUtil.w(LogUtil.BUGLE_IMAGE_TAG,
    413                         "Pooled bitmap consistently not being reused. Failure count = " +
    414                                 mFailedBitmapReuseCount + ", success count = " +
    415                                 mSucceededBitmapReuseCount);
    416             }
    417         }
    418     }
    419 }
    420