/* * 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.animated.impl; import javax.annotation.concurrent.GuardedBy; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import android.app.ActivityManager; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.Rect; import android.support.v4.util.SparseArrayCompat; import com.facebook.common.executors.SerialExecutorService; import com.facebook.common.internal.VisibleForTesting; import com.facebook.common.logging.FLog; import com.facebook.common.references.CloseableReference; import com.facebook.common.references.ResourceReleaser; import com.facebook.common.time.MonotonicClock; import com.facebook.common.util.ByteConstants; import com.facebook.imagepipeline.animated.base.AnimatedDrawableBackend; import com.facebook.imagepipeline.animated.base.AnimatedDrawableCachingBackend; import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo; import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo.DisposalMethod; import com.facebook.imagepipeline.animated.base.AnimatedDrawableOptions; import com.facebook.imagepipeline.animated.base.DelegatingAnimatedDrawableBackend; import com.facebook.imagepipeline.animated.util.AnimatedDrawableUtil; import bolts.Continuation; import bolts.Task; /** * A caching and prefetching layer that delegates to a {@link AnimatedDrawableBackend}. */ public class AnimatedDrawableCachingBackendImpl extends DelegatingAnimatedDrawableBackend implements AnimatedDrawableCachingBackend { private static final Class<?> TAG = AnimatedDrawableCachingBackendImpl.class; private static final AtomicInteger sTotalBitmaps = new AtomicInteger(); private static final int PREFETCH_FRAMES = 3; private final SerialExecutorService mExecutorService; private final AnimatedDrawableUtil mAnimatedDrawableUtil; private final ActivityManager mActivityManager; private final MonotonicClock mMonotonicClock; private final AnimatedDrawableBackend mAnimatedDrawableBackend; private final AnimatedDrawableOptions mAnimatedDrawableOptions; private final AnimatedImageCompositor mAnimatedImageCompositor; private final ResourceReleaser<Bitmap> mResourceReleaserForBitmaps; private final double mMaximumKiloBytes; private final double mApproxKiloBytesToHoldAllFrames; @GuardedBy("this") private final List<Bitmap> mFreeBitmaps; @GuardedBy("this") private final SparseArrayCompat<Task<Object>> mDecodesInFlight; @GuardedBy("this") private final SparseArrayCompat<CloseableReference<Bitmap>> mCachedBitmaps; @GuardedBy("this") private final WhatToKeepCachedArray mBitmapsToKeepCached; @GuardedBy("ui-thread") private int mCurrentFrameIndex; public AnimatedDrawableCachingBackendImpl( SerialExecutorService executorService, ActivityManager activityManager, AnimatedDrawableUtil animatedDrawableUtil, MonotonicClock monotonicClock, AnimatedDrawableBackend animatedDrawableBackend, AnimatedDrawableOptions options) { super(animatedDrawableBackend); mExecutorService = executorService; mActivityManager = activityManager; mAnimatedDrawableUtil = animatedDrawableUtil; mMonotonicClock = monotonicClock; mAnimatedDrawableBackend = animatedDrawableBackend; mAnimatedDrawableOptions = options; mMaximumKiloBytes = options.maximumBytes >= 0 ? options.maximumBytes / ByteConstants.KB: getDefaultMaxBytes(activityManager)/ ByteConstants.KB; mAnimatedImageCompositor = new AnimatedImageCompositor( animatedDrawableBackend, new AnimatedImageCompositor.Callback() { @Override public void onIntermediateResult(int frameNumber, Bitmap bitmap) { maybeCacheBitmapDuringRender(frameNumber, bitmap); } @Override public CloseableReference<Bitmap> getCachedBitmap(int frameNumber) { return getCachedOrPredecodedFrame(frameNumber); } }); mResourceReleaserForBitmaps = new ResourceReleaser<Bitmap>() { @Override public void release(Bitmap value) { releaseBitmapInternal(value); } }; mFreeBitmaps = new ArrayList<Bitmap>(); mDecodesInFlight = new SparseArrayCompat<Task<Object>>(10); mCachedBitmaps = new SparseArrayCompat<CloseableReference<Bitmap>>(10); mBitmapsToKeepCached = new WhatToKeepCachedArray(mAnimatedDrawableBackend.getFrameCount()); mApproxKiloBytesToHoldAllFrames = mAnimatedDrawableBackend.getRenderedWidth() * mAnimatedDrawableBackend.getRenderedHeight() / ByteConstants.KB * mAnimatedDrawableBackend.getFrameCount() * 4; } @Override protected synchronized void finalize() throws Throwable { super.finalize(); if (mCachedBitmaps.size() > 0) { FLog.d(TAG, "Finalizing with rendered bitmaps"); } sTotalBitmaps.addAndGet(-mFreeBitmaps.size()); mFreeBitmaps.clear(); } private Bitmap createNewBitmap() { FLog.v(TAG, "Creating new bitmap"); sTotalBitmaps.incrementAndGet(); FLog.v(TAG, "Total bitmaps: %d", sTotalBitmaps.get()); return Bitmap.createBitmap( mAnimatedDrawableBackend.getRenderedWidth(), mAnimatedDrawableBackend.getRenderedHeight(), Bitmap.Config.ARGB_8888); } @Override public void renderFrame(int frameNumber, Canvas canvas) { // renderFrame method should not be called on cache. throw new IllegalStateException(); } @Override public CloseableReference<Bitmap> getBitmapForFrame(int frameNumber) { mCurrentFrameIndex = frameNumber; CloseableReference<Bitmap> result = getBitmapForFrameInternal(frameNumber, false); schedulePrefetches(); return result; } @Override public CloseableReference<Bitmap> getPreviewBitmap() { return getAnimatedImageResult().getPreviewBitmap(); } @VisibleForTesting CloseableReference<Bitmap> getBitmapForFrameBlocking(int frameNumber) { mCurrentFrameIndex = frameNumber; CloseableReference<Bitmap> result = getBitmapForFrameInternal(frameNumber, true); schedulePrefetches(); return result; } @Override public AnimatedDrawableCachingBackend forNewBounds(Rect bounds) { AnimatedDrawableBackend newBackend = mAnimatedDrawableBackend.forNewBounds(bounds); if (newBackend == mAnimatedDrawableBackend) { return this; } return new AnimatedDrawableCachingBackendImpl( mExecutorService, mActivityManager, mAnimatedDrawableUtil, mMonotonicClock, newBackend, mAnimatedDrawableOptions); } @Override public synchronized void dropCaches() { mBitmapsToKeepCached.setAll(false); dropBitmapsThatShouldNotBeCached(); for (Bitmap freeBitmap : mFreeBitmaps) { freeBitmap.recycle(); sTotalBitmaps.decrementAndGet(); } mFreeBitmaps.clear(); mAnimatedDrawableBackend.dropCaches(); FLog.v(TAG, "Total bitmaps: %d", sTotalBitmaps.get()); } @Override public int getMemoryUsage() { int bytes = 0; synchronized (this) { for (Bitmap bitmap : mFreeBitmaps) { bytes += mAnimatedDrawableUtil.getSizeOfBitmap(bitmap); } for (int i = 0; i < mCachedBitmaps.size(); i++) { CloseableReference<Bitmap> bitmapReference = mCachedBitmaps.valueAt(i); bytes += mAnimatedDrawableUtil.getSizeOfBitmap(bitmapReference.get()); } } bytes += mAnimatedDrawableBackend.getMemoryUsage(); return bytes; } @Override public void appendDebugOptionString(StringBuilder sb) { if (mAnimatedDrawableOptions.forceKeepAllFramesInMemory) { sb.append("Pinned To Memory"); } else { if (mApproxKiloBytesToHoldAllFrames < mMaximumKiloBytes) { sb.append("within "); } else { sb.append("exceeds "); } mAnimatedDrawableUtil.appendMemoryString(sb, (int) mMaximumKiloBytes); } if (shouldKeepAllFramesInMemory() && mAnimatedDrawableOptions.allowPrefetching) { sb.append(" MT"); } } private CloseableReference<Bitmap> getBitmapForFrameInternal( int frameNumber, boolean forceImmediate) { boolean renderedOnCallingThread = false; boolean deferred = false; long startMs = mMonotonicClock.now(); try { synchronized (this) { mBitmapsToKeepCached.set(frameNumber, true); CloseableReference<Bitmap> bitmapReference = getCachedOrPredecodedFrame(frameNumber); if (bitmapReference != null) { return bitmapReference; } } if (forceImmediate) { // Give up and try to do it on the calling thread. renderedOnCallingThread = true; CloseableReference<Bitmap> bitmapReference = obtainBitmapInternal(); try { mAnimatedImageCompositor.renderFrame(frameNumber, bitmapReference.get()); maybeCacheRenderedBitmap(frameNumber, bitmapReference); return bitmapReference.clone(); } finally { bitmapReference.close(); } } deferred = true; return null; } finally { long elapsedMs = mMonotonicClock.now() - startMs; if (elapsedMs > 10) { String comment = ""; if (renderedOnCallingThread) { comment = "renderedOnCallingThread"; } else if (deferred) { comment = "deferred"; } else { comment = "ok"; } FLog.v(TAG, "obtainBitmap for frame %d took %d ms (%s)", frameNumber, elapsedMs, comment); } } } /** * Called while rendering intermediate frames into the bitmap. If this is a frame we want cached, * we'll copy it and cache it. * * @param frameNumber the index of the frame * @param bitmap the rendered bitmap for that frame */ private void maybeCacheBitmapDuringRender(int frameNumber, Bitmap bitmap) { boolean cacheBitmap = false; synchronized (this) { boolean shouldCache = mBitmapsToKeepCached.get(frameNumber); if (shouldCache) { cacheBitmap = mCachedBitmaps.get(frameNumber) == null; } } if (cacheBitmap) { copyAndCacheBitmapDuringRendering(frameNumber, bitmap); } } /** * Copies the source bitmap for the specified frame and caches it. * * @param frameNumber the frame number * @param sourceBitmap the rendered bitmap to be cached (after copying) */ private void copyAndCacheBitmapDuringRendering(int frameNumber, Bitmap sourceBitmap) { CloseableReference<Bitmap> destBitmapReference = obtainBitmapInternal(); try { Canvas copyCanvas = new Canvas(destBitmapReference.get()); copyCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.SRC); copyCanvas.drawBitmap(sourceBitmap, 0, 0, null); maybeCacheRenderedBitmap(frameNumber, destBitmapReference); } finally { destBitmapReference.close(); } } private CloseableReference<Bitmap> obtainBitmapInternal() { Bitmap bitmap; synchronized (this) { long nowNanos = System.nanoTime(); long waitUntilNanos = nowNanos + TimeUnit.NANOSECONDS.convert(20, TimeUnit.MILLISECONDS); while (mFreeBitmaps.isEmpty() && nowNanos < waitUntilNanos) { try { TimeUnit.NANOSECONDS.timedWait(this, waitUntilNanos - nowNanos); nowNanos = System.nanoTime(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } } if (mFreeBitmaps.isEmpty()) { bitmap = createNewBitmap(); } else { bitmap = mFreeBitmaps.remove(mFreeBitmaps.size() - 1); } } return CloseableReference.of(bitmap, mResourceReleaserForBitmaps); } synchronized void releaseBitmapInternal(Bitmap bitmap) { mFreeBitmaps.add(bitmap); } private synchronized void schedulePrefetches() { AnimatedDrawableFrameInfo frameInfo = mAnimatedDrawableBackend.getFrameInfo(mCurrentFrameIndex); boolean keepOnePreceding = frameInfo.disposalMethod == DisposalMethod.DISPOSE_TO_PREVIOUS; int startFrame = Math.max(0, mCurrentFrameIndex - (keepOnePreceding ? 1 : 0)); int numToPrefetch = mAnimatedDrawableOptions.allowPrefetching ? PREFETCH_FRAMES : 0; numToPrefetch = Math.max(numToPrefetch, keepOnePreceding ? 1 : 0); int endFrame = (startFrame + numToPrefetch) % mAnimatedDrawableBackend.getFrameCount(); cancelFuturesOutsideOfRange(startFrame, endFrame); if (!shouldKeepAllFramesInMemory()) { mBitmapsToKeepCached.setAll(true); mBitmapsToKeepCached.removeOutsideRange(startFrame, endFrame); // Keep one closest to startFrame that is already cached to reduce the number of frames we // need to composite together to draw startFrame. for (int frameNumber = startFrame; frameNumber >= 0; frameNumber--) { if (mCachedBitmaps.get(frameNumber) != null) { mBitmapsToKeepCached.set(frameNumber, true); break; } } dropBitmapsThatShouldNotBeCached(); } if (mAnimatedDrawableOptions.allowPrefetching) { doPrefetch(startFrame, numToPrefetch); } else { cancelFuturesOutsideOfRange(mCurrentFrameIndex, mCurrentFrameIndex); } } private static int getDefaultMaxBytes(ActivityManager activityManager) { int memory = activityManager.getMemoryClass(); if (memory > 32) { return 5 * 1024 * 1024; } else { return 3 * 1024 * 1024; } } private boolean shouldKeepAllFramesInMemory() { if (mAnimatedDrawableOptions.forceKeepAllFramesInMemory) { // This overrides everything. return true; } return mApproxKiloBytesToHoldAllFrames < mMaximumKiloBytes; } private synchronized void doPrefetch(int startFrame, int count) { for (int i = 0; i < count; i++) { final int frameNumber = (startFrame + i) % mAnimatedDrawableBackend.getFrameCount(); boolean hasCached = hasCachedOrPredecodedFrame(frameNumber); Task<Object> future = mDecodesInFlight.get(frameNumber); if (!hasCached && future == null) { final Task<Object> newFuture = Task.call( new Callable<Object>() { @Override public Object call() { runPrefetch(frameNumber); return null; } }, mExecutorService); mDecodesInFlight.put(frameNumber, newFuture); newFuture.continueWith( new Continuation<Object, Object>() { @Override public Object then(Task<Object> task) throws Exception { onFutureFinished(newFuture, frameNumber); return null; } }); } } } /** * Renders a frame and caches it. This runs on the worker thread. * * @param frameNumber the frame to render */ private void runPrefetch(int frameNumber) { synchronized (this) { if (!mBitmapsToKeepCached.get(frameNumber)) { // Looks like we're no longer supposed to keep this cached. return; } if (hasCachedOrPredecodedFrame(frameNumber)) { // Looks like it's already cached. return; } } CloseableReference<Bitmap> preDecodedFrame = mAnimatedDrawableBackend.getPreDecodedFrame(frameNumber); try { if (preDecodedFrame != null) { maybeCacheRenderedBitmap(frameNumber, preDecodedFrame); } else { CloseableReference<Bitmap> bitmapReference = obtainBitmapInternal(); try { mAnimatedImageCompositor.renderFrame(frameNumber, bitmapReference.get()); maybeCacheRenderedBitmap(frameNumber, bitmapReference); FLog.v(TAG, "Prefetch rendered frame %d", frameNumber); } finally { bitmapReference.close(); } } } finally { CloseableReference.closeSafely(preDecodedFrame); } } private synchronized void onFutureFinished(Task<?> future, int frameNumber) { int index = mDecodesInFlight.indexOfKey(frameNumber); if (index >= 0) { Task<?> futureAtIndex = mDecodesInFlight.valueAt(index); if (futureAtIndex == future) { mDecodesInFlight.removeAt(index); if (future.getError() != null) { FLog.v(TAG, future.getError(), "Failed to render frame %d", frameNumber); } } } } private synchronized void cancelFuturesOutsideOfRange(int startFrame, int endFrame) { int index = 0; while (index < mDecodesInFlight.size()) { int frameNumber = mDecodesInFlight.keyAt(index); boolean outsideRange = AnimatedDrawableUtil.isOutsideRange(startFrame, endFrame, frameNumber); if (outsideRange) { Task<?> future = mDecodesInFlight.valueAt(index); mDecodesInFlight.removeAt(index); //future.cancel(false); -- TODO } else { index++; } } } private synchronized void dropBitmapsThatShouldNotBeCached() { int index = 0; while (index < mCachedBitmaps.size()) { int frameNumber = mCachedBitmaps.keyAt(index); boolean keepCached = mBitmapsToKeepCached.get(frameNumber); if (!keepCached) { CloseableReference<Bitmap> bitmapReference = mCachedBitmaps.valueAt(index); mCachedBitmaps.removeAt(index); bitmapReference.close(); } else { index++; } } } private synchronized void maybeCacheRenderedBitmap( int frameNumber, CloseableReference<Bitmap> bitmapReference) { if (!mBitmapsToKeepCached.get(frameNumber)) { return; } int existingIndex = mCachedBitmaps.indexOfKey(frameNumber); if (existingIndex >= 0) { CloseableReference<Bitmap> oldReference = mCachedBitmaps.valueAt(existingIndex); oldReference.close(); mCachedBitmaps.removeAt(existingIndex); } mCachedBitmaps.put(frameNumber, bitmapReference.clone()); } private synchronized CloseableReference<Bitmap> getCachedOrPredecodedFrame(int frameNumber) { CloseableReference<Bitmap> ret = CloseableReference.cloneOrNull(mCachedBitmaps.get(frameNumber)); if (ret == null) { ret = mAnimatedDrawableBackend.getPreDecodedFrame(frameNumber); } return ret; } private synchronized boolean hasCachedOrPredecodedFrame(int frameNumber) { return mCachedBitmaps.get(frameNumber) != null || mAnimatedDrawableBackend.hasPreDecodedFrame(frameNumber); } @VisibleForTesting synchronized Map<Integer, Task<?>> getDecodesInFlight() { Map<Integer, Task<?>> map = new HashMap<Integer, Task<?>>(); for (int i = 0; i < mDecodesInFlight.size(); i++) { map.put(mDecodesInFlight.keyAt(i), mDecodesInFlight.valueAt(i)); } return map; } @VisibleForTesting synchronized Set<Integer> getFramesCached() { Set<Integer> set = new HashSet<Integer>(mCachedBitmaps.size()); for (int i = 0; i < mCachedBitmaps.size(); i++) { set.add(mCachedBitmaps.keyAt(i)); } return set; } }