/* * 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.cache; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import java.util.ArrayList; import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.TimeUnit; import android.graphics.Bitmap; import android.os.SystemClock; import com.facebook.common.internal.Preconditions; import com.facebook.common.internal.Supplier; import com.facebook.common.internal.VisibleForTesting; import com.facebook.common.memory.MemoryTrimType; import com.facebook.common.memory.MemoryTrimmable; import com.facebook.common.references.CloseableReference; import com.facebook.common.references.ResourceReleaser; import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory; import com.android.internal.util.Predicate; /** * Layer of memory cache stack responsible for managing eviction of the the cached items. * * <p> This layer is responsible for LRU eviction strategy and for maintaining the size boundaries * of the cached items. * * <p> Only the exclusively owned elements, i.e. the elements not referenced by any client, can be * evicted. * * @param <K> the key type * @param <V> the value type */ @ThreadSafe public class CountingMemoryCache<K, V> implements MemoryCache<K, V>, MemoryTrimmable { /** * Interface used to specify the trimming strategy for the cache. */ public interface CacheTrimStrategy { double getTrimRatio(MemoryTrimType trimType); } /** * Interface used to observe the state changes of an entry. */ public interface EntryStateObserver<K> { /** * Called when the exclusivity status of the entry changes. * * <p> The item can be reused if it is exclusively owned by the cache. */ void onExclusivityChanged(K key, boolean isExclusive); } /** * The internal representation of a key-value pair stored by the cache. */ @VisibleForTesting static class Entry<K, V> { public final K key; public final CloseableReference<V> valueRef; // The number of clients that reference the value. public int clientCount; // Whether or not this entry is tracked by this cache. Orphans are not tracked by the cache and // as soon as the last client of an orphaned entry closes their reference, the entry's copy is // closed too. public boolean isOrphan; @Nullable public final EntryStateObserver<K> observer; private Entry(K key, CloseableReference<V> valueRef, @Nullable EntryStateObserver<K> observer) { this.key = Preconditions.checkNotNull(key); this.valueRef = Preconditions.checkNotNull(CloseableReference.cloneOrNull(valueRef)); this.clientCount = 0; this.isOrphan = false; this.observer = observer; } /** Creates a new entry with the usage count of 0. */ @VisibleForTesting static <K, V> Entry<K, V> of( final K key, final CloseableReference<V> valueRef, final @Nullable EntryStateObserver<K> observer) { return new Entry<>(key, valueRef, observer); } } // How often the cache checks for a new cache configuration. @VisibleForTesting static final long PARAMS_INTERCHECK_INTERVAL_MS = TimeUnit.MINUTES.toMillis(5); // Contains the items that are not being used by any client and are hence viable for eviction. @GuardedBy("this") @VisibleForTesting final CountingLruMap<K, Entry<K, V>> mExclusiveEntries; // Contains all the cached items including the exclusively owned ones. @GuardedBy("this") @VisibleForTesting final CountingLruMap<K, Entry<K, V>> mCachedEntries; @GuardedBy("this") @VisibleForTesting final Map<Bitmap, Object> mOtherEntries = new WeakHashMap<>(); private final ValueDescriptor<V> mValueDescriptor; private final CacheTrimStrategy mCacheTrimStrategy; // Cache size constraints. private final Supplier<MemoryCacheParams> mMemoryCacheParamsSupplier; @GuardedBy("this") protected MemoryCacheParams mMemoryCacheParams; @GuardedBy("this") private long mLastCacheParamsCheck; public CountingMemoryCache( ValueDescriptor<V> valueDescriptor, CacheTrimStrategy cacheTrimStrategy, Supplier<MemoryCacheParams> memoryCacheParamsSupplier, PlatformBitmapFactory platformBitmapFactory, boolean isExternalCreatedBitmapLogEnabled) { mValueDescriptor = valueDescriptor; mExclusiveEntries = new CountingLruMap<>(wrapValueDescriptor(valueDescriptor)); mCachedEntries = new CountingLruMap<>(wrapValueDescriptor(valueDescriptor)); mCacheTrimStrategy = cacheTrimStrategy; mMemoryCacheParamsSupplier = memoryCacheParamsSupplier; mMemoryCacheParams = mMemoryCacheParamsSupplier.get(); mLastCacheParamsCheck = SystemClock.uptimeMillis(); if (isExternalCreatedBitmapLogEnabled) { platformBitmapFactory.setCreationListener( new PlatformBitmapFactory.BitmapCreationObserver() { @Override public void onBitmapCreated( Bitmap bitmap, Object callerContext) { mOtherEntries.put(bitmap, callerContext); } }); } } private ValueDescriptor<Entry<K, V>> wrapValueDescriptor( final ValueDescriptor<V> evictableValueDescriptor) { return new ValueDescriptor<Entry<K,V>>() { @Override public int getSizeInBytes(Entry<K, V> entry) { return evictableValueDescriptor.getSizeInBytes(entry.valueRef.get()); } }; } /** * Caches the given key-value pair. * * <p> Important: the client should use the returned reference instead of the original one. * It is the caller's responsibility to close the returned reference once not needed anymore. * * @return the new reference to be used, null if the value cannot be cached */ public CloseableReference<V> cache(final K key, final CloseableReference<V> valueRef) { return cache(key, valueRef, null); } /** * Caches the given key-value pair. * * <p> Important: the client should use the returned reference instead of the original one. * It is the caller's responsibility to close the returned reference once not needed anymore. * * @return the new reference to be used, null if the value cannot be cached */ public CloseableReference<V> cache( final K key, final CloseableReference<V> valueRef, final EntryStateObserver<K> observer) { Preconditions.checkNotNull(key); Preconditions.checkNotNull(valueRef); maybeUpdateCacheParams(); Entry<K, V> oldExclusive; CloseableReference<V> oldRefToClose = null; CloseableReference<V> clientRef = null; synchronized (this) { // remove the old item (if any) as it is stale now oldExclusive = mExclusiveEntries.remove(key); Entry<K, V> oldEntry = mCachedEntries.remove(key); if (oldEntry != null) { makeOrphan(oldEntry); oldRefToClose = referenceToClose(oldEntry); } if (canCacheNewValue(valueRef.get())) { Entry<K, V> newEntry = Entry.of(key, valueRef, observer); mCachedEntries.put(key, newEntry); clientRef = newClientReference(newEntry); } } CloseableReference.closeSafely(oldRefToClose); maybeNotifyExclusiveEntryRemoval(oldExclusive); maybeEvictEntries(); return clientRef; } /** Checks the cache constraints to determine whether the new value can be cached or not. */ private synchronized boolean canCacheNewValue(V value) { int newValueSize = mValueDescriptor.getSizeInBytes(value); return (newValueSize <= mMemoryCacheParams.maxCacheEntrySize) && (getInUseCount() <= mMemoryCacheParams.maxCacheEntries - 1) && (getInUseSizeInBytes() <= mMemoryCacheParams.maxCacheSize - newValueSize); } /** * Gets the item with the given key, or null if there is no such item. * * <p> It is the caller's responsibility to close the returned reference once not needed anymore. */ @Nullable public CloseableReference<V> get(final K key) { Preconditions.checkNotNull(key); Entry<K, V> oldExclusive; CloseableReference<V> clientRef = null; synchronized (this) { oldExclusive = mExclusiveEntries.remove(key); Entry<K, V> entry = mCachedEntries.get(key); if (entry != null) { clientRef = newClientReference(entry); } } maybeNotifyExclusiveEntryRemoval(oldExclusive); maybeUpdateCacheParams(); maybeEvictEntries(); return clientRef; } /** Creates a new reference for the client. */ private synchronized CloseableReference<V> newClientReference(final Entry<K, V> entry) { increaseClientCount(entry); return CloseableReference.of( entry.valueRef.get(), new ResourceReleaser<V>() { @Override public void release(V unused) { releaseClientReference(entry); } }); } /** Called when the client closes its reference. */ private void releaseClientReference(final Entry<K, V> entry) { Preconditions.checkNotNull(entry); boolean isExclusiveAdded; CloseableReference<V> oldRefToClose; synchronized (this) { decreaseClientCount(entry); isExclusiveAdded = maybeAddToExclusives(entry); oldRefToClose = referenceToClose(entry); } CloseableReference.closeSafely(oldRefToClose); maybeNotifyExclusiveEntryInsertion(isExclusiveAdded ? entry : null); maybeUpdateCacheParams(); maybeEvictEntries(); } /** Adds the entry to the exclusively owned queue if it is viable for eviction. */ private synchronized boolean maybeAddToExclusives(Entry<K, V> entry) { if (!entry.isOrphan && entry.clientCount == 0) { mExclusiveEntries.put(entry.key, entry); return true; } return false; } /** * Gets the value with the given key to be reused, or null if there is no such value. * * <p> The item can be reused only if it is exclusively owned by the cache. */ @Nullable public CloseableReference<V> reuse(K key) { Preconditions.checkNotNull(key); CloseableReference<V> clientRef = null; boolean removed = false; Entry<K, V> oldExclusive = null; synchronized (this) { oldExclusive = mExclusiveEntries.remove(key); if (oldExclusive != null) { Entry<K, V> entry = mCachedEntries.remove(key); Preconditions.checkNotNull(entry); Preconditions.checkState(entry.clientCount == 0); // optimization: instead of cloning and then closing the original reference, // we just do a move clientRef = entry.valueRef; removed = true; } } if (removed) { maybeNotifyExclusiveEntryRemoval(oldExclusive); } return clientRef; } /** * Removes all the items from the cache whose key matches the specified predicate. * * @param predicate returns true if an item with the given key should be removed * @return number of the items removed from the cache */ public int removeAll(Predicate<K> predicate) { ArrayList<Entry<K, V>> oldExclusives; ArrayList<Entry<K, V>> oldEntries; synchronized (this) { oldExclusives = mExclusiveEntries.removeAll(predicate); oldEntries = mCachedEntries.removeAll(predicate); makeOrphans(oldEntries); } maybeClose(oldEntries); maybeNotifyExclusiveEntryRemoval(oldExclusives); maybeUpdateCacheParams(); maybeEvictEntries(); return oldEntries.size(); } /** Removes all the items from the cache. */ public void clear() { ArrayList<Entry<K, V>> oldExclusives; ArrayList<Entry<K, V>> oldEntries; synchronized (this) { oldExclusives = mExclusiveEntries.clear(); oldEntries = mCachedEntries.clear(); makeOrphans(oldEntries); } maybeClose(oldEntries); maybeNotifyExclusiveEntryRemoval(oldExclusives); maybeUpdateCacheParams(); } /** * Check if any items from the cache whose key matches the specified predicate. * * @param predicate returns true if an item with the given key matches * @return true is any items matches from the cache */ @Override public synchronized boolean contains(Predicate<K> predicate) { return !mCachedEntries.getMatchingEntries(predicate).isEmpty(); } /** * Check if an item with the given cache key is currently in the cache. * * @param key returns true if an item with the given key matches * @return true is any items matches from the cache */ public synchronized boolean contains(K key) { return mCachedEntries.contains(key); } /** Trims the cache according to the specified trimming strategy and the given trim type. */ @Override public void trim(MemoryTrimType trimType) { ArrayList<Entry<K, V>> oldEntries; final double trimRatio = mCacheTrimStrategy.getTrimRatio(trimType); synchronized (this) { int targetCacheSize = (int) (mCachedEntries.getSizeInBytes() * (1 - trimRatio)); int targetEvictionQueueSize = Math.max(0, targetCacheSize - getInUseSizeInBytes()); oldEntries = trimExclusivelyOwnedEntries(Integer.MAX_VALUE, targetEvictionQueueSize); makeOrphans(oldEntries); } maybeClose(oldEntries); maybeNotifyExclusiveEntryRemoval(oldEntries); maybeUpdateCacheParams(); maybeEvictEntries(); } /** * Updates the cache params (constraints) if enough time has passed since the last update. */ private synchronized void maybeUpdateCacheParams() { if (mLastCacheParamsCheck + PARAMS_INTERCHECK_INTERVAL_MS > SystemClock.uptimeMillis()) { return; } mLastCacheParamsCheck = SystemClock.uptimeMillis(); mMemoryCacheParams = mMemoryCacheParamsSupplier.get(); } /** * Removes the exclusively owned items until the cache constraints are met. * * <p> This method invokes the external {@link CloseableReference#close} method, * so it must not be called while holding the <code>this</code> lock. */ private void maybeEvictEntries() { ArrayList<Entry<K, V>> oldEntries; synchronized (this) { int maxCount = Math.min( mMemoryCacheParams.maxEvictionQueueEntries, mMemoryCacheParams.maxCacheEntries - getInUseCount()); int maxSize = Math.min( mMemoryCacheParams.maxEvictionQueueSize, mMemoryCacheParams.maxCacheSize - getInUseSizeInBytes()); oldEntries = trimExclusivelyOwnedEntries(maxCount, maxSize); makeOrphans(oldEntries); } maybeClose(oldEntries); maybeNotifyExclusiveEntryRemoval(oldEntries); } /** * Removes the exclusively owned items until there is at most <code>count</code> of them * and they occupy no more than <code>size</code> bytes. * * <p> This method returns the removed items instead of actually closing them, so it is safe to * be called while holding the <code>this</code> lock. */ @Nullable private synchronized ArrayList<Entry<K, V>> trimExclusivelyOwnedEntries(int count, int size) { count = Math.max(count, 0); size = Math.max(size, 0); // fast path without array allocation if no eviction is necessary if (mExclusiveEntries.getCount() <= count && mExclusiveEntries.getSizeInBytes() <= size) { return null; } ArrayList<Entry<K, V>> oldEntries = new ArrayList<>(); while (mExclusiveEntries.getCount() > count || mExclusiveEntries.getSizeInBytes() > size) { K key = mExclusiveEntries.getFirstKey(); mExclusiveEntries.remove(key); oldEntries.add(mCachedEntries.remove(key)); } return oldEntries; } /** * Notifies the client that the cache no longer tracks the given items. * * <p> This method invokes the external {@link CloseableReference#close} method, * so it must not be called while holding the <code>this</code> lock. */ private void maybeClose(@Nullable ArrayList<Entry<K, V>> oldEntries) { if (oldEntries != null) { for (Entry<K, V> oldEntry : oldEntries) { CloseableReference.closeSafely(referenceToClose(oldEntry)); } } } private void maybeNotifyExclusiveEntryRemoval(@Nullable ArrayList<Entry<K, V>> entries) { if (entries != null) { for (Entry<K, V> entry : entries) { maybeNotifyExclusiveEntryRemoval(entry); } } } private static <K, V> void maybeNotifyExclusiveEntryRemoval(@Nullable Entry<K, V> entry) { if (entry != null && entry.observer != null) { entry.observer.onExclusivityChanged(entry.key, false); } } private static <K, V> void maybeNotifyExclusiveEntryInsertion(@Nullable Entry<K, V> entry) { if (entry != null && entry.observer != null) { entry.observer.onExclusivityChanged(entry.key, true); } } /** Marks the given entries as orphans. */ private synchronized void makeOrphans(@Nullable ArrayList<Entry<K, V>> oldEntries) { if (oldEntries != null) { for (Entry<K, V> oldEntry : oldEntries) { makeOrphan(oldEntry); } } } /** Marks the entry as orphan. */ private synchronized void makeOrphan(Entry<K, V> entry) { Preconditions.checkNotNull(entry); Preconditions.checkState(!entry.isOrphan); entry.isOrphan = true; } /** Increases the entry's client count. */ private synchronized void increaseClientCount(Entry<K, V> entry) { Preconditions.checkNotNull(entry); Preconditions.checkState(!entry.isOrphan); entry.clientCount++; } /** Decreases the entry's client count. */ private synchronized void decreaseClientCount(Entry<K, V> entry) { Preconditions.checkNotNull(entry); Preconditions.checkState(entry.clientCount > 0); entry.clientCount--; } /** Returns the value reference of the entry if it should be closed, null otherwise. */ @Nullable private synchronized CloseableReference<V> referenceToClose(Entry<K, V> entry) { Preconditions.checkNotNull(entry); return (entry.isOrphan && entry.clientCount == 0) ? entry.valueRef : null; } /** Gets the total number of all currently cached items. */ public synchronized int getCount() { return mCachedEntries.getCount(); } /** Gets the total size in bytes of all currently cached items. */ public synchronized int getSizeInBytes() { return mCachedEntries.getSizeInBytes(); } /** Gets the number of the cached items that are used by at least one client. */ public synchronized int getInUseCount() { return mCachedEntries.getCount() - mExclusiveEntries.getCount(); } /** Gets the total size in bytes of the cached items that are used by at least one client. */ public synchronized int getInUseSizeInBytes() { return mCachedEntries.getSizeInBytes() - mExclusiveEntries.getSizeInBytes(); } /** Gets the number of the exclusively owned items. */ public synchronized int getEvictionQueueCount() { return mExclusiveEntries.getCount(); } /** Gets the total size in bytes of the exclusively owned items. */ public synchronized int getEvictionQueueSizeInBytes() { return mExclusiveEntries.getSizeInBytes(); } }