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