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 androidx.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 SingleSizePool(final int maxPoolSize)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 */ BitmapPool(final int maxSize, @NonNull final String name)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 reclaim()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 */ getBitmapOptionsForPool(final boolean scaled, final int inputDensity, final int targetDensity)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 */ getPoolKey(final int width, final int height)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 */ findPoolBitmap(final int width, final int height)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 */ assignPoolBitmap(final BitmapFactory.Options optionsTmp, final int width, final int height)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 */ decodeSampledBitmapFromResource(final int resourceId, @NonNull final Resources resources, @NonNull final BitmapFactory.Options optionsTmp, final int width, final int height)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 */ decodeSampledBitmapFromInputStream(@onNull final InputStream inputStream, @NonNull final BitmapFactory.Options optionsTmp, final int width, final int height)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 */ decodeByteArray(@onNull final byte[] bytes, @NonNull final BitmapFactory.Options optionsTmp, final int width, final int height)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 */ createOrReuseBitmap(final int width, final int height)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 */ createBitmap(final int width, final int height)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 */ reclaimBitmap(@onNull final Bitmap b)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 */ isFull(final int width, final int height)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