package org.ebookdroid.common.bitmaps; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.util.SparseArray; import java.util.Iterator; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReentrantLock; import org.emdev.BaseDroidApp; import org.emdev.common.log.LogContext; import org.emdev.common.log.LogManager; import org.emdev.utils.collections.ArrayDeque; import org.emdev.utils.collections.SparseArrayEx; import org.emdev.utils.collections.TLIterator; public class BitmapManager { static final LogContext LCTX = LogManager.root().lctx("BitmapManager", false); private final static long BITMAP_MEMORY_LIMIT = Runtime.getRuntime().maxMemory() / 2; private static final int GENERATION_THRESHOLD = 10; private static SparseArrayEx<AbstractBitmapRef> used = new SparseArrayEx<AbstractBitmapRef>(); private static ArrayDeque<AbstractBitmapRef> pool = new ArrayDeque<AbstractBitmapRef>(); private static SparseArray<Bitmap> resources = new SparseArray<Bitmap>(); private static Queue<Object> releasing = new ConcurrentLinkedQueue<Object>(); private static final AtomicLong created = new AtomicLong(); private static final AtomicLong reused = new AtomicLong(); private static final AtomicLong memoryUsed = new AtomicLong(); private static final AtomicLong memoryPooled = new AtomicLong(); private static AtomicLong generation = new AtomicLong(); private static ReentrantLock lock = new ReentrantLock(); public static Bitmap getResource(final int resourceId) { synchronized (resources) { Bitmap bitmap = resources.get(resourceId); if (bitmap == null || bitmap.isRecycled()) { final Resources resources = BaseDroidApp.context.getResources(); bitmap = BitmapFactory.decodeResource(resources, resourceId); } return bitmap; } } public static IBitmapRef addBitmap(final String name, final Bitmap bitmap) { lock.lock(); try { final BitmapRef ref = new BitmapRef(bitmap, generation.get()); used.append(ref.id, ref); created.incrementAndGet(); memoryUsed.addAndGet(ref.size); if (LCTX.isDebugEnabled()) { LCTX.d("Added bitmap: [" + ref.id + ", " + name + ", " + ref.width + ", " + ref.height + "], created=" + created + ", reused=" + reused + ", memoryUsed=" + used.size() + "/" + (memoryUsed.get() / 1024) + "KB" + ", memoryInPool=" + pool.size() + "/" + (memoryPooled.get() / 1024) + "KB"); } ref.name = name; return ref; } finally { lock.unlock(); } } public static IBitmapRef getBitmap(final String name, final int width, final int height, final Bitmap.Config config) { lock.lock(); try { if (LCTX.isDebugEnabled()) { if (memoryUsed.get() + memoryPooled.get() == 0) { LCTX.d("!!! Bitmap pool size: " + (BITMAP_MEMORY_LIMIT / 1024) + "KB"); } } final TLIterator<AbstractBitmapRef> it = pool.iterator(); try { while (it.hasNext()) { final AbstractBitmapRef ref = it.next(); if (!ref.isRecycled() && ref.config == config && ref.width == width && ref.height >= height) { if (ref.used.compareAndSet(false, true)) { it.remove(); ref.gen = generation.get(); used.append(ref.id, ref); reused.incrementAndGet(); memoryPooled.addAndGet(-ref.size); memoryUsed.addAndGet(ref.size); if (LCTX.isDebugEnabled()) { LCTX.d("Reuse bitmap: [" + ref.id + ", " + ref.name + " => " + name + ", " + width + ", " + height + "], created=" + created + ", reused=" + reused + ", memoryUsed=" + used.size() + "/" + (memoryUsed.get() / 1024) + "KB" + ", memoryInPool=" + pool.size() + "/" + (memoryPooled.get() / 1024) + "KB"); } ref.name = name; return ref; } else { if (LCTX.isDebugEnabled()) { LCTX.d("Attempt to re-use used bitmap: " + ref); } } } } } finally { it.release(); } final BitmapRef ref = new BitmapRef(Bitmap.createBitmap(width, height, config), generation.get()); used.put(ref.id, ref); created.incrementAndGet(); memoryUsed.addAndGet(ref.size); if (LCTX.isDebugEnabled()) { LCTX.d("Create bitmap: [" + ref.id + ", " + name + ", " + width + ", " + height + "], created=" + created + ", reused=" + reused + ", memoryUsed=" + used.size() + "/" + (memoryUsed.get() / 1024) + "KB" + ", memoryInPool=" + pool.size() + "/" + (memoryPooled.get() / 1024) + "KB"); } shrinkPool(BITMAP_MEMORY_LIMIT); ref.name = name; return ref; } finally { lock.unlock(); } } public static void clear(final String msg) { lock.lock(); try { generation.addAndGet(GENERATION_THRESHOLD * 2); removeOldRefs(); release(); shrinkPool(0); } finally { lock.unlock(); } } public static void release() { lock.lock(); try { generation.incrementAndGet(); removeOldRefs(); int count = 0; final int queueBefore = LCTX.isDebugEnabled() ? releasing.size() : 0; while (!releasing.isEmpty()) { final Object ref = releasing.poll(); if (ref instanceof AbstractBitmapRef) { releaseImpl((AbstractBitmapRef) ref); count++; } else { LCTX.e("Unknown object in release queue: " + ref); } } shrinkPool(BITMAP_MEMORY_LIMIT); if (LCTX.isDebugEnabled()) { LCTX.d("Return " + count + " bitmap(s) to pool: " + "memoryUsed=" + used.size() + "/" + (memoryUsed.get() / 1024) + "KB" + ", memoryInPool=" + pool.size() + "/" + (memoryPooled.get() / 1024) + "KB" + ", releasing queue size " + queueBefore + " => 0"); } } finally { lock.unlock(); } } public static void release(final IBitmapRef ref) { if (ref != null) { if (LCTX.isDebugEnabled()) { LCTX.d("Adding 1 ref to release queue"); } releasing.add(ref); } } static void releaseImpl(final AbstractBitmapRef ref) { assert ref != null; if (ref.used.compareAndSet(true, false)) { if (used.get(ref.id, null) == ref) { used.remove(ref.id); memoryUsed.addAndGet(-ref.size); } else { LCTX.e("The bitmap " + ref + " not found in used ones"); } } else { if (LCTX.isDebugEnabled()) { LCTX.d("Attempt to release unused bitmap"); } } pool.add(ref); memoryPooled.addAndGet(ref.size); } private static void removeOldRefs() { final long gen = generation.get(); int recycled = 0; final Iterator<AbstractBitmapRef> it = pool.iterator(); while (it.hasNext()) { final AbstractBitmapRef ref = it.next(); if (gen - ref.gen > GENERATION_THRESHOLD) { it.remove(); ref.recycle(); recycled++; memoryPooled.addAndGet(-ref.size); } } if (recycled > 0) { if (LCTX.isDebugEnabled()) { LCTX.d("Recycled " + recycled + " pooled bitmap(s): " + "memoryUsed=" + used.size() + "/" + (memoryUsed.get() / 1024) + "KB" + ", memoryInPool=" + pool.size() + "/" + (memoryPooled.get() / 1024) + "KB"); } } } private static void shrinkPool(final long limit) { int recycled = 0; while (memoryPooled.get() + memoryUsed.get() > limit && !pool.isEmpty()) { final AbstractBitmapRef ref = pool.poll(); if (ref != null) { ref.recycle(); memoryPooled.addAndGet(-ref.size); recycled++; } } if (recycled > 0) { if (LCTX.isDebugEnabled()) { LCTX.d("Recycled " + recycled + " pooled bitmap(s): " + "memoryUsed=" + used.size() + "/" + (memoryUsed.get() / 1024) + "KB" + ", memoryInPool=" + pool.size() + "/" + (memoryPooled.get() / 1024) + "KB"); } } } public static int getBitmapBufferSize(final int width, final int height, final Bitmap.Config config) { return getPixelSizeInBytes(config) * width * height; } public static int getPixelSizeInBytes(final Bitmap.Config config) { switch (config) { case ALPHA_8: return 1; case ARGB_4444: case RGB_565: return 2; case ARGB_8888: default: return 4; } } }