/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.bitmap; import android.util.Log; import android.util.LruCache; import com.android.bitmap.util.Trace; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.LinkedBlockingQueue; /** * An alternative implementation of a pool+cache. This implementation only counts * unreferenced objects in its size calculation. Internally, it never evicts from * its cache, and instead {@link #poll()} is allowed to return unreferenced cache * entries. * <p> * You would only use this kind of cache if your objects are interchangeable and * have significant allocation cost, and if your memory footprint is somewhat * flexible. * <p> * Because this class only counts unreferenced objects toward targetSize, * it will have a total memory footprint of: * <code>(targetSize) + (# of threads concurrently writing to cache) + * (total size of still-referenced entries)</code> * */ public class UnrefedPooledCache<K, V extends Poolable> implements PooledCache<K, V> { private final LinkedHashMap<K, V> mCache; private final LinkedBlockingQueue<V> mPool; private final int mTargetSize; private final LruCache<K, V> mNonPooledCache; private static final boolean DEBUG = DecodeTask.DEBUG; private static final String TAG = UnrefedPooledCache.class.getSimpleName(); /** * @param targetSize not exactly a max size in practice * @param nonPooledFraction the fractional portion in the range [0.0,1.0] of targetSize to * dedicate to non-poolable entries */ public UnrefedPooledCache(int targetSize, float nonPooledFraction) { mCache = new LinkedHashMap<K, V>(0, 0.75f, true); mPool = new LinkedBlockingQueue<V>(); final int nonPooledSize = Math.round(targetSize * nonPooledFraction); if (nonPooledSize > 0) { mNonPooledCache = new NonPooledCache(nonPooledSize); } else { mNonPooledCache = null; } mTargetSize = targetSize - nonPooledSize; } @Override public V get(K key, boolean incrementRefCount) { Trace.beginSection("cache get"); synchronized (mCache) { V result = mCache.get(key); if (result == null && mNonPooledCache != null) { result = mNonPooledCache.get(key); } if (incrementRefCount && result != null) { result.acquireReference(); } Trace.endSection(); return result; } } @Override public V put(K key, V value) { Trace.beginSection("cache put"); // Null values not supported. if (value == null) { Trace.endSection(); return null; } synchronized (mCache) { final V prev; if (value.isEligibleForPooling()) { prev = mCache.put(key, value); } else if (mNonPooledCache != null) { prev = mNonPooledCache.put(key, value); } else { prev = null; } Trace.endSection(); return prev; } } @Override public void offer(V value) { Trace.beginSection("pool offer"); if (value.getRefCount() != 0 || !value.isEligibleForPooling()) { Trace.endSection(); throw new IllegalArgumentException("unexpected offer of an invalid object: " + value); } mPool.offer(value); Trace.endSection(); } @Override public V poll() { Trace.beginSection("pool poll"); final V pooled = mPool.poll(); if (pooled != null) { Trace.endSection(); return pooled; } synchronized (mCache) { int unrefSize = 0; Map.Entry<K, V> eldestUnref = null; for (Map.Entry<K, V> entry : mCache.entrySet()) { final V value = entry.getValue(); if (value.getRefCount() > 0 || !value.isEligibleForPooling()) { continue; } if (eldestUnref == null) { eldestUnref = entry; } unrefSize += sizeOf(value); if (unrefSize > mTargetSize) { break; } } // only return a scavenged cache entry if the cache has enough // eligible (unreferenced) items if (unrefSize <= mTargetSize) { if (DEBUG) { Log.e(TAG, "POOL SCAVENGE FAILED, cache not fully warm yet. szDelta=" + (mTargetSize-unrefSize)); } Trace.endSection(); return null; } else { mCache.remove(eldestUnref.getKey()); if (DEBUG) { Log.e(TAG, "POOL SCAVENGE SUCCESS, oldKey=" + eldestUnref.getKey()); } Trace.endSection(); return eldestUnref.getValue(); } } } protected int sizeOf(V value) { return 1; } @Override public String toDebugString() { if (DEBUG) { final StringBuilder sb = new StringBuilder("["); sb.append(super.toString()); int size = 0; synchronized (mCache) { sb.append(" poolCount="); sb.append(mPool.size()); sb.append(" cacheSize="); sb.append(mCache.size()); if (mNonPooledCache != null) { sb.append(" nonPooledCacheSize="); sb.append(mNonPooledCache.size()); } sb.append("\n---------------------"); for (V val : mPool) { size += sizeOf(val); sb.append("\n\tpool item: "); sb.append(val); } sb.append("\n---------------------"); for (Map.Entry<K, V> item : mCache.entrySet()) { final V val = item.getValue(); sb.append("\n\tcache key="); sb.append(item.getKey()); sb.append(" val="); sb.append(val); size += sizeOf(val); } sb.append("\n---------------------"); if (mNonPooledCache != null) { for (Map.Entry<K, V> item : mNonPooledCache.snapshot().entrySet()) { final V val = item.getValue(); sb.append("\n\tnon-pooled cache key="); sb.append(item.getKey()); sb.append(" val="); sb.append(val); size += sizeOf(val); } sb.append("\n---------------------"); } sb.append("\nTOTAL SIZE=" + size); } sb.append("]"); return sb.toString(); } else { return null; } } private class NonPooledCache extends LruCache<K, V> { public NonPooledCache(int maxSize) { super(maxSize); } @Override protected int sizeOf(K key, V value) { return UnrefedPooledCache.this.sizeOf(value); } } @Override public void clear() { mCache.clear(); mPool.clear(); } }