Home | History | Annotate | Download | only in datamodel
      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.messaging.datamodel;
     18 
     19 import android.content.res.Resources;
     20 import android.graphics.Bitmap;
     21 import android.graphics.BitmapFactory;
     22 import android.support.annotation.NonNull;
     23 import android.text.TextUtils;
     24 import android.util.SparseArray;
     25 
     26 import com.android.messaging.datamodel.MemoryCacheManager.MemoryCache;
     27 import com.android.messaging.util.Assert;
     28 import com.android.messaging.util.LogUtil;
     29 
     30 import java.io.InputStream;
     31 
     32 /**
     33  * Class for creating / loading / reusing bitmaps. This class allow the user to create a new bitmap,
     34  * reuse an bitmap from the pool and to return a bitmap for future reuse.  The pool of bitmaps
     35  * allows for faster decode and more efficient memory usage.
     36  * Note: consumers should not create BitmapPool directly, but instead get the pool they want from
     37  * the BitmapPoolManager.
     38  */
     39 public class BitmapPool implements MemoryCache {
     40     public static final int MAX_SUPPORTED_IMAGE_DIMENSION = 0xFFFF;
     41 
     42     protected static final boolean VERBOSE = false;
     43 
     44     /**
     45      * Number of reuse failures to skip before reporting.
     46      */
     47     private static final int FAILED_REPORTING_FREQUENCY = 100;
     48 
     49     /**
     50      * Count of reuse failures which have occurred.
     51      */
     52     private static volatile int sFailedBitmapReuseCount = 0;
     53 
     54     /**
     55      * Overall pool data structure which currently only supports rectangular bitmaps. The size of
     56      * one of the sides is used to index into the SparseArray.
     57      */
     58     private final SparseArray<SingleSizePool> mPool;
     59     private final Object mPoolLock = new Object();
     60     private final String mPoolName;
     61     private final int mMaxSize;
     62 
     63     /**
     64      * Inner structure which holds a pool of bitmaps all the same size (i.e. all have the same
     65      * width as each other and height as each other, but not necessarily the same).
     66      */
     67     private class SingleSizePool {
     68         int mNumItems;
     69         final Bitmap[] mBitmaps;
     70 
     71         SingleSizePool(final int maxPoolSize) {
     72             mNumItems = 0;
     73             mBitmaps = new Bitmap[maxPoolSize];
     74         }
     75     }
     76 
     77     /**
     78      * Creates a pool of reused bitmaps with helper decode methods which will attempt to use the
     79      * reclaimed bitmaps. This will help speed up the creation of bitmaps by using already allocated
     80      * bitmaps.
     81      * @param maxSize The overall max size of the pool. When the pool exceeds this size, all calls
     82      * to reclaimBitmap(Bitmap) will result in recycling the bitmap.
     83      * @param name Name of the bitmap pool and only used for logging. Can not be null.
     84      */
     85     BitmapPool(final int maxSize, @NonNull final String name) {
     86         Assert.isTrue(maxSize > 0);
     87         Assert.isTrue(!TextUtils.isEmpty(name));
     88         mPoolName = name;
     89         mMaxSize = maxSize;
     90         mPool = new SparseArray<SingleSizePool>();
     91     }
     92 
     93     @Override
     94     public void reclaim() {
     95         synchronized (mPoolLock) {
     96             for (int p = 0; p < mPool.size(); p++) {
     97                 final SingleSizePool singleSizePool = mPool.valueAt(p);
     98                 for (int i = 0; i < singleSizePool.mNumItems; i++) {
     99                     singleSizePool.mBitmaps[i].recycle();
    100                     singleSizePool.mBitmaps[i] = null;
    101                 }
    102                 singleSizePool.mNumItems = 0;
    103             }
    104             mPool.clear();
    105         }
    106     }
    107 
    108     /**
    109      * Creates a new BitmapFactory.Options.
    110      */
    111     public static BitmapFactory.Options getBitmapOptionsForPool(final boolean scaled,
    112             final int inputDensity, final int targetDensity) {
    113         final BitmapFactory.Options options = new BitmapFactory.Options();
    114         options.inScaled = scaled;
    115         options.inDensity = inputDensity;
    116         options.inTargetDensity = targetDensity;
    117         options.inSampleSize = 1;
    118         options.inJustDecodeBounds = false;
    119         options.inMutable = true;
    120         return options;
    121     }
    122 
    123     /**
    124      * @return The pool key for the provided image dimensions or 0 if either width or height is
    125      * greater than the max supported image dimension.
    126      */
    127     private int getPoolKey(final int width, final int height) {
    128         if (width > MAX_SUPPORTED_IMAGE_DIMENSION || height > MAX_SUPPORTED_IMAGE_DIMENSION) {
    129             return 0;
    130         }
    131         return (width << 16) | height;
    132     }
    133 
    134     /**
    135      *
    136      * @return A bitmap in the pool with the specified dimensions or null if no bitmap with the
    137      * specified dimension is available.
    138      */
    139     private Bitmap findPoolBitmap(final int width, final int height) {
    140         final int poolKey = getPoolKey(width, height);
    141         if (poolKey != 0) {
    142             synchronized (mPoolLock) {
    143                 // Take a bitmap from the pool if one is available
    144                 final SingleSizePool singlePool = mPool.get(poolKey);
    145                 if (singlePool != null && singlePool.mNumItems > 0) {
    146                     singlePool.mNumItems--;
    147                     final Bitmap foundBitmap = singlePool.mBitmaps[singlePool.mNumItems];
    148                     singlePool.mBitmaps[singlePool.mNumItems] = null;
    149                     return foundBitmap;
    150                 }
    151             }
    152         }
    153         return null;
    154     }
    155 
    156     /**
    157      * Internal function to try and find a bitmap in the pool which matches the desired width and
    158      * height and then set that in the bitmap options properly.
    159      *
    160      * TODO: Why do we take a width/height? Shouldn't this already be in the
    161      * BitmapFactory.Options instance? Can we assert that they match?
    162      * @param optionsTmp The BitmapFactory.Options to update with the bitmap for the system to try
    163      * to reuse.
    164      * @param width The width of the reusable bitmap.
    165      * @param height The height of the reusable bitmap.
    166      */
    167     private void assignPoolBitmap(final BitmapFactory.Options optionsTmp, final int width,
    168             final int height) {
    169         if (optionsTmp.inJustDecodeBounds) {
    170             return;
    171         }
    172         optionsTmp.inBitmap = findPoolBitmap(width, height);
    173     }
    174 
    175     /**
    176      * Load a resource into a bitmap. Uses a bitmap from the pool if possible to reduce memory
    177      * turnover.
    178      * @param resourceId Resource id to load.
    179      * @param resources Application resources. Cannot be null.
    180      * @param optionsTmp Should be the same options returned from getBitmapOptionsForPool(). Cannot
    181      * be null.
    182      * @param width The width of the bitmap.
    183      * @param height The height of the bitmap.
    184      * @return The decoded Bitmap with the resource drawn in it.
    185      */
    186     public Bitmap decodeSampledBitmapFromResource(final int resourceId,
    187             @NonNull final Resources resources, @NonNull final BitmapFactory.Options optionsTmp,
    188             final int width, final int height) {
    189         Assert.notNull(resources);
    190         Assert.notNull(optionsTmp);
    191         Assert.isTrue(width > 0);
    192         Assert.isTrue(height > 0);
    193         assignPoolBitmap(optionsTmp, width, height);
    194         Bitmap b = null;
    195         try {
    196             b = BitmapFactory.decodeResource(resources, resourceId, optionsTmp);
    197         } catch (final IllegalArgumentException e) {
    198             // BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
    199             if (optionsTmp.inBitmap != null) {
    200                 optionsTmp.inBitmap = null;
    201                 b = BitmapFactory.decodeResource(resources, resourceId, optionsTmp);
    202                 sFailedBitmapReuseCount++;
    203                 if (sFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
    204                     LogUtil.w(LogUtil.BUGLE_TAG,
    205                             "Pooled bitmap consistently not being reused count = " +
    206                             sFailedBitmapReuseCount);
    207                 }
    208             }
    209         } catch (final OutOfMemoryError e) {
    210             LogUtil.w(LogUtil.BUGLE_TAG, "Oom decoding resource " + resourceId);
    211             reclaim();
    212         }
    213         return b;
    214     }
    215 
    216     /**
    217      * Load an input stream into a bitmap. Uses a bitmap from the pool if possible to reduce memory
    218      * turnover.
    219      * @param inputStream InputStream load. Cannot be null.
    220      * @param optionsTmp Should be the same options returned from getBitmapOptionsForPool(). Cannot
    221      * be null.
    222      * @param width The width of the bitmap.
    223      * @param height The height of the bitmap.
    224      * @return The decoded Bitmap with the resource drawn in it.
    225      */
    226     public Bitmap decodeSampledBitmapFromInputStream(@NonNull final InputStream inputStream,
    227             @NonNull final BitmapFactory.Options optionsTmp,
    228             final int width, final int height) {
    229         Assert.notNull(inputStream);
    230         Assert.isTrue(width > 0);
    231         Assert.isTrue(height > 0);
    232         assignPoolBitmap(optionsTmp, width, height);
    233         Bitmap b = null;
    234         try {
    235             b = BitmapFactory.decodeStream(inputStream, null, optionsTmp);
    236         } catch (final IllegalArgumentException e) {
    237             // BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
    238             if (optionsTmp.inBitmap != null) {
    239                 optionsTmp.inBitmap = null;
    240                 b = BitmapFactory.decodeStream(inputStream, null, optionsTmp);
    241                 sFailedBitmapReuseCount++;
    242                 if (sFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
    243                     LogUtil.w(LogUtil.BUGLE_TAG,
    244                             "Pooled bitmap consistently not being reused count = " +
    245                             sFailedBitmapReuseCount);
    246                 }
    247             }
    248         } catch (final OutOfMemoryError e) {
    249             LogUtil.w(LogUtil.BUGLE_TAG, "Oom decoding inputStream");
    250             reclaim();
    251         }
    252         return b;
    253     }
    254 
    255     /**
    256      * Turn encoded bytes into a bitmap. Uses a bitmap from the pool if possible to reduce memory
    257      * turnover.
    258      * @param bytes Encoded bytes to draw on the bitmap. Cannot be null.
    259      * @param optionsTmp The bitmap will set here and the input should be generated from
    260      * getBitmapOptionsForPool(). Cannot be null.
    261      * @param width The width of the bitmap.
    262      * @param height The height of the bitmap.
    263      * @return A Bitmap with the encoded bytes drawn in it.
    264      */
    265     public Bitmap decodeByteArray(@NonNull final byte[] bytes,
    266             @NonNull final BitmapFactory.Options optionsTmp, final int width,
    267             final int height) throws OutOfMemoryError {
    268         Assert.notNull(bytes);
    269         Assert.notNull(optionsTmp);
    270         Assert.isTrue(width > 0);
    271         Assert.isTrue(height > 0);
    272         assignPoolBitmap(optionsTmp, width, height);
    273         Bitmap b = null;
    274         try {
    275             b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp);
    276         } catch (final IllegalArgumentException e) {
    277             if (VERBOSE) {
    278                 LogUtil.v(LogUtil.BUGLE_TAG, "BitmapPool(" + mPoolName +
    279                         ") Unable to use pool bitmap");
    280             }
    281             // BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
    282             // (i.e. without the bitmap from the pool)
    283             if (optionsTmp.inBitmap != null) {
    284                 optionsTmp.inBitmap = null;
    285                 b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp);
    286                 sFailedBitmapReuseCount++;
    287                 if (sFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
    288                     LogUtil.w(LogUtil.BUGLE_TAG,
    289                             "Pooled bitmap consistently not being reused count = " +
    290                             sFailedBitmapReuseCount);
    291                 }
    292             }
    293         }
    294         return b;
    295     }
    296 
    297     /**
    298      * Creates a bitmap with the given size, this will reuse a bitmap in the pool, if one is
    299      * available, otherwise this will create a new one.
    300      * @param width The desired width of the bitmap.
    301      * @param height The desired height of the bitmap.
    302      * @return A bitmap with the desired width and height, this maybe a reused bitmap from the pool.
    303      */
    304     public Bitmap createOrReuseBitmap(final int width, final int height) {
    305         Bitmap b = findPoolBitmap(width, height);
    306         if (b == null) {
    307             b = createBitmap(width, height);
    308         }
    309         return b;
    310     }
    311 
    312     /**
    313      * This will create a new bitmap regardless of pool state.
    314      * @param width The desired width of the bitmap.
    315      * @param height The desired height of the bitmap.
    316      * @return A bitmap with the desired width and height.
    317      */
    318     private Bitmap createBitmap(final int width, final int height) {
    319         return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    320     }
    321 
    322     /**
    323      * Called when a bitmap is finished being used so that it can be used for another bitmap in the
    324      * future or recycled. Any bitmaps returned should not be used by the caller again.
    325      * @param b The bitmap to return to the pool for future usage or recycled. This cannot be null.
    326      */
    327     public void reclaimBitmap(@NonNull final Bitmap b) {
    328         Assert.notNull(b);
    329         final int poolKey = getPoolKey(b.getWidth(), b.getHeight());
    330         if (poolKey == 0 || !b.isMutable()) {
    331             // Unsupported image dimensions or a immutable bitmap.
    332             b.recycle();
    333             return;
    334         }
    335         synchronized (mPoolLock) {
    336             SingleSizePool singleSizePool = mPool.get(poolKey);
    337             if (singleSizePool == null) {
    338                 singleSizePool = new SingleSizePool(mMaxSize);
    339                 mPool.append(poolKey, singleSizePool);
    340             }
    341             if (singleSizePool.mNumItems < singleSizePool.mBitmaps.length) {
    342                 singleSizePool.mBitmaps[singleSizePool.mNumItems] = b;
    343                 singleSizePool.mNumItems++;
    344             } else {
    345                 b.recycle();
    346             }
    347         }
    348     }
    349 
    350     /**
    351      * @return whether the pool is full for a given width and height.
    352      */
    353     public boolean isFull(final int width, final int height) {
    354         final int poolKey = getPoolKey(width, height);
    355         synchronized (mPoolLock) {
    356             final SingleSizePool singleSizePool = mPool.get(poolKey);
    357             if (singleSizePool != null &&
    358                     singleSizePool.mNumItems >= singleSizePool.mBitmaps.length) {
    359                 return true;
    360             }
    361             return false;
    362         }
    363     }
    364 }
    365