/* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.imagepipeline.memory; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import com.facebook.common.internal.Preconditions; import com.facebook.common.internal.VisibleForTesting; import com.facebook.common.logging.FLog; import com.facebook.common.memory.MemoryTrimType; import com.facebook.common.memory.MemoryTrimmableRegistry; import com.facebook.common.references.OOMSoftReference; /** * A simple class that holds (at-most) one byte-array for use. * This buffer is allocated on first use (i.e.) when the {@link #get(int)} operation is called. * If the current byte-array is smaller than what's required for the {@link #get(int)} * operation, then the current byte-array is discarded and a new byte-array is allocated. * <p> * NOTE: There can be at most one user of the byte-array returned from this class at any * one time. This fits well with DecodeOperation, where we've synchronized the decode * call, so that only one decodeXXX call is active at any time. However, if that assumption * changes, we will need to change this as well. * This is currently enforced by the {@link #mInUse} member variable. * <p> * This looks a bit like a Pool; however, it is an extremely degenerate case because there can * be at most one byte array active at any time. */ @ThreadSafe public class SingleByteArrayPool implements ByteArrayPool { private static final Class<?> TAG = SingleByteArrayPool.class; @VisibleForTesting final int mMinByteArraySize; @VisibleForTesting final int mMaxByteArraySize; @VisibleForTesting final SingleByteArrayPoolStatsTracker mSingleByteArrayPoolStatsTracker; /** * Soft references to the single byte array maintained by the pool. */ @GuardedBy("this") @VisibleForTesting OOMSoftReference<byte[]> mByteArraySoftRef; /** * Indicates if the single byte array above is currently in use */ @GuardedBy("this") @VisibleForTesting boolean mInUse = false; /** * Creates an instance of the SingleByteArrayPool class, and registers it * @param memoryTrimmableRegistry the memory resource manager * @param singleByteArrayPoolStatsTracker stats tracker for the pool * @param params params for this pool */ public SingleByteArrayPool( MemoryTrimmableRegistry memoryTrimmableRegistry, PoolParams params, SingleByteArrayPoolStatsTracker singleByteArrayPoolStatsTracker) { this( memoryTrimmableRegistry, singleByteArrayPoolStatsTracker, params.minBucketSize, params.maxBucketSize); } /** * Creates an instance of the SingleByteArrayPool class, and registers it * @param memoryTrimmableRegistry the memory resource manager * @param singleByteArrayPoolStatsTracker stats tracker for the pool * @param minByteArraySize size of the smallest byte array we will create * @param maxByteArraySize size of the largest byte array we will create */ @VisibleForTesting SingleByteArrayPool( MemoryTrimmableRegistry memoryTrimmableRegistry, SingleByteArrayPoolStatsTracker singleByteArrayPoolStatsTracker, int minByteArraySize, int maxByteArraySize) { Preconditions.checkNotNull(memoryTrimmableRegistry); Preconditions.checkNotNull(singleByteArrayPoolStatsTracker); Preconditions.checkState(minByteArraySize > 0 && maxByteArraySize >= minByteArraySize); mSingleByteArrayPoolStatsTracker = singleByteArrayPoolStatsTracker; mMaxByteArraySize = maxByteArraySize; mMinByteArraySize = minByteArraySize; mByteArraySoftRef = new OOMSoftReference<byte[]>(); memoryTrimmableRegistry.registerMemoryTrimmable(this); } /** * Returns a byte array of the desired size. * If the locally held byte array {@link #mByteArraySoftRef} is larger than the desired size, * then the buffer is returned. Otherwise, a new byte array is allocated, and the current byte * array is discarded. * NOTE: The byte-array must not be in use already. * @param size the size of the byte array that's needed * @return a byte array of the desired size */ @Override public synchronized byte[] get(int size) { Preconditions.checkArgument(size > 0, "Invalid size %s", size); Preconditions.checkState(!mInUse, "Byte-array currently in use"); int bucketedSize = getBucketedSize(size); mSingleByteArrayPoolStatsTracker.onBucketedSizeRequested(bucketedSize); if (bucketedSize > mMaxByteArraySize) { throw new IllegalArgumentException("Size too large: " + size); } byte[] byteArray = mByteArraySoftRef.get(); if (byteArray == null || byteArray.length < bucketedSize) { bucketedSize = Math.max(bucketedSize, mMinByteArraySize); if (FLog.isLoggable(FLog.VERBOSE)) { FLog.v( TAG, "get (alloc) size = %d. Previous buffer size = %d", bucketedSize, (byteArray == null) ? 0 : byteArray.length); } if (byteArray != null) { byteArray = null; clearByteArraySoftRef(); } allocateByteArraySoftRef(bucketedSize); byteArray = mByteArraySoftRef.get(); } mInUse = true; return byteArray; } /** * 'Releases' the byte array, so another {@link #get(int)} call can use it. * If the 'value' parameter is not the same as {@link #mByteArraySoftRef}, we do nothing. * Otherwise, we mark {@link #mInUse} as false, and return true. */ @Override public synchronized void release(byte[] value) { Preconditions.checkNotNull(value); // if this value is not the if (mByteArraySoftRef.get() == value) { mInUse = false; } } /** * Responds to memory pressure by simply 'discarding' the local byte array * @param trimType kind of trimming to perform (ignored) */ @Override public synchronized void trim(MemoryTrimType trimType) { if (!mInUse && mByteArraySoftRef.get() != null) { if (FLog.isLoggable(FLog.VERBOSE)) { FLog.v(TAG, "Discarding existing buffer of size %d", mByteArraySoftRef.get().length); } mSingleByteArrayPoolStatsTracker.onMemoryTrimmed(mByteArraySoftRef.get().length); clearByteArraySoftRef(); } } /** * Gets the 'bucketed' size - the nearest power of 2 that's larger than or equal to 'size' * @param size the requested size * @return the 'bucketed' size that's greater than or equal to size */ @VisibleForTesting int getBucketedSize(int size) { int powerOfTwo = Integer.highestOneBit(size); return (size > powerOfTwo) ? powerOfTwo * 2 : powerOfTwo; } /** * Allocates byte array and sets soft references to it */ private synchronized void allocateByteArraySoftRef(int bucketedSize) { byte[] byteArray = new byte[bucketedSize]; mByteArraySoftRef.set(byteArray); mSingleByteArrayPoolStatsTracker.onMemoryAlloc(bucketedSize); } /** * Clear byte array and the soft references to it */ private synchronized void clearByteArraySoftRef() { mByteArraySoftRef.clear(); } }