com.facebook.imagepipeline.animated.impl.AnimatedDrawableCachingBackendImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.imagepipeline.animated.impl.AnimatedDrawableCachingBackendImpl.java

Source

/*
 * 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;
    }
}