/*
* 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 java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import com.facebook.common.internal.Preconditions;
import com.facebook.common.logging.FLog;
import com.facebook.common.references.CloseableReference;
import com.facebook.imagepipeline.image.EncodedImage;
import com.facebook.imagepipeline.memory.PooledByteBuffer;
import com.facebook.imagepipeline.memory.PooledByteBufferFactory;
import com.facebook.imagepipeline.memory.PooledByteStreams;
import com.facebook.binaryresource.BinaryResource;
import com.facebook.cache.common.CacheKey;
import com.facebook.cache.common.WriterCallback;
import com.facebook.cache.disk.FileCache;
import bolts.Task;
/**
* BufferedDiskCache provides get and put operations to take care of scheduling disk-cache
* read/writes.
*/
public class BufferedDiskCache {
private static final Class<?> TAG = BufferedDiskCache.class;
private final FileCache mFileCache;
private final PooledByteBufferFactory mPooledByteBufferFactory;
private final PooledByteStreams mPooledByteStreams;
private final Executor mReadExecutor;
private final Executor mWriteExecutor;
private final StagingArea mStagingArea;
private final ImageCacheStatsTracker mImageCacheStatsTracker;
public BufferedDiskCache(
FileCache fileCache,
PooledByteBufferFactory pooledByteBufferFactory,
PooledByteStreams pooledByteStreams,
Executor readExecutor,
Executor writeExecutor,
ImageCacheStatsTracker imageCacheStatsTracker) {
mFileCache = fileCache;
mPooledByteBufferFactory = pooledByteBufferFactory;
mPooledByteStreams = pooledByteStreams;
mReadExecutor = readExecutor;
mWriteExecutor = writeExecutor;
mImageCacheStatsTracker = imageCacheStatsTracker;
mStagingArea = StagingArea.getInstance();
}
/**
* Performs a key-value look up in the disk cache. If the value is not found in the staging area,
* then a disk cache check is scheduled on a background thread. Any error manifests itself as a
* cache miss, i.e. the returned Task resolves to false.
* @param key
* @return Task that resolves to true if the element is found, or false otherwise
*/
public Task<Boolean> contains(final CacheKey key) {
Preconditions.checkNotNull(key);
final EncodedImage pinnedImage = mStagingArea.get(key);
if (pinnedImage != null) {
pinnedImage.close();
FLog.v(TAG, "Found image for %s in staging area", key.toString());
mImageCacheStatsTracker.onStagingAreaHit();
return Task.forResult(true);
}
try {
return Task.call(
new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
EncodedImage result = mStagingArea.get(key);
if (result != null) {
result.close();
FLog.v(TAG, "Found image for %s in staging area", key.toString());
mImageCacheStatsTracker.onStagingAreaHit();
return true;
} else {
FLog.v(TAG, "Did not find image for %s in staging area", key.toString());
mImageCacheStatsTracker.onStagingAreaMiss();
try {
return mFileCache.hasKey(key);
} catch (Exception exception) {
return false;
}
}
}
},
mReadExecutor);
} catch (Exception exception) {
// Log failure
// TODO: 3697790
FLog.w(
TAG,
exception,
"Failed to schedule disk-cache read for %s",
key.toString());
return Task.forError(exception);
}
}
/**
* Performs key-value look up in disk cache. If value is not found in disk cache staging area
* then disk cache read is scheduled on background thread. Any error manifests itself as
* cache miss, i.e. the returned future resolves to null.
* @param key
* @return ListenableFuture that resolves to cached element or null if one cannot be retrieved;
* returned future never rethrows any exception
*/
public Task<EncodedImage> get(final CacheKey key, final AtomicBoolean isCancelled) {
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(isCancelled);
final EncodedImage pinnedImage = mStagingArea.get(key);
if (pinnedImage != null) {
FLog.v(TAG, "Found image for %s in staging area", key.toString());
mImageCacheStatsTracker.onStagingAreaHit();
return Task.forResult(pinnedImage);
}
try {
return Task.call(
new Callable<EncodedImage>() {
@Override
public EncodedImage call()
throws Exception {
if (isCancelled.get()) {
throw new CancellationException();
}
EncodedImage result = mStagingArea.get(key);
if (result != null) {
FLog.v(TAG, "Found image for %s in staging area", key.toString());
mImageCacheStatsTracker.onStagingAreaHit();
} else {
FLog.v(TAG, "Did not find image for %s in staging area", key.toString());
mImageCacheStatsTracker.onStagingAreaMiss();
try {
final PooledByteBuffer buffer = readFromDiskCache(key);
CloseableReference<PooledByteBuffer> ref = CloseableReference.of(buffer);
try {
result = new EncodedImage(ref);
} finally {
CloseableReference.closeSafely(ref);
}
} catch (Exception exception) {
return null;
}
}
if (Thread.interrupted()) {
FLog.v(TAG, "Host thread was interrupted, decreasing reference count");
if (result != null) {
result.close();
}
throw new InterruptedException();
} else {
return result;
}
}
},
mReadExecutor);
} catch (Exception exception) {
// Log failure
// TODO: 3697790
FLog.w(
TAG,
exception,
"Failed to schedule disk-cache read for %s",
key.toString());
return Task.forError(exception);
}
}
/**
* Associates encodedImage with given key in disk cache. Disk write is performed on background
* thread, so the caller of this method is not blocked
*/
public void put(
final CacheKey key,
EncodedImage encodedImage) {
Preconditions.checkNotNull(key);
Preconditions.checkArgument(EncodedImage.isValid(encodedImage));
// Store encodedImage in staging area
mStagingArea.put(key, encodedImage);
// Write to disk cache. This will be executed on background thread, so increment the ref count.
// When this write completes (with success/failure), then we will bump down the ref count
// again.
final EncodedImage finalEncodedImage = EncodedImage.cloneOrNull(encodedImage);
try {
mWriteExecutor.execute(
new Runnable() {
@Override
public void run() {
try {
writeToDiskCache(key, finalEncodedImage);
} finally {
mStagingArea.remove(key, finalEncodedImage);
EncodedImage.closeSafely(finalEncodedImage);
}
}
});
} catch (Exception exception) {
// We failed to enqueue cache write. Log failure and decrement ref count
// TODO: 3697790
FLog.w(
TAG,
exception,
"Failed to schedule disk-cache write for %s",
key.toString());
mStagingArea.remove(key, encodedImage);
EncodedImage.closeSafely(finalEncodedImage);
}
}
/**
* Removes the item from the disk cache and the staging area.
*/
public Task<Void> remove(final CacheKey key) {
Preconditions.checkNotNull(key);
mStagingArea.remove(key);
try {
return Task.call(
new Callable<Void>() {
@Override
public Void call() throws Exception {
mStagingArea.remove(key);
mFileCache.remove(key);
return null;
}
},
mWriteExecutor);
} catch (Exception exception) {
// Log failure
// TODO: 3697790
FLog.w(TAG, exception, "Failed to schedule disk-cache remove for %s", key.toString());
return Task.forError(exception);
}
}
/**
* Clears the disk cache and the staging area.
*/
public Task<Void> clearAll() {
mStagingArea.clearAll();
try {
return Task.call(
new Callable<Void>() {
@Override
public Void call() throws Exception {
mStagingArea.clearAll();
mFileCache.clearAll();
return null;
}
},
mWriteExecutor);
} catch (Exception exception) {
// Log failure
// TODO: 3697790
FLog.w(TAG, exception, "Failed to schedule disk-cache clear");
return Task.forError(exception);
}
}
/**
* Performs disk cache read. In case of any exception null is returned.
*/
private PooledByteBuffer readFromDiskCache(final CacheKey key) throws IOException {
try {
FLog.v(TAG, "Disk cache read for %s", key.toString());
final BinaryResource diskCacheResource = mFileCache.getResource(key);
if (diskCacheResource == null) {
FLog.v(TAG, "Disk cache miss for %s", key.toString());
mImageCacheStatsTracker.onDiskCacheMiss();
return null;
} else {
FLog.v(TAG, "Found entry in disk cache for %s", key.toString());
mImageCacheStatsTracker.onDiskCacheHit();
}
PooledByteBuffer byteBuffer;
final InputStream is = diskCacheResource.openStream();
try {
byteBuffer = mPooledByteBufferFactory.newByteBuffer(is, (int) diskCacheResource.size());
} finally {
is.close();
}
FLog.v(TAG, "Successful read from disk cache for %s", key.toString());
return byteBuffer;
} catch (IOException ioe) {
// TODO: 3697790 log failures
// TODO: 5258772 - uncomment line below
// mFileCache.remove(key);
FLog.w(TAG, ioe, "Exception reading from cache for %s", key.toString());
mImageCacheStatsTracker.onDiskCacheGetFail();
throw ioe;
}
}
/**
* Writes to disk cache
* @throws IOException
*/
private void writeToDiskCache(
final CacheKey key,
final EncodedImage encodedImage) {
FLog.v(TAG, "About to write to disk-cache for key %s", key.toString());
try {
mFileCache.insert(
key, new WriterCallback() {
@Override
public void write(OutputStream os) throws IOException {
mPooledByteStreams.copy(encodedImage.getInputStream(), os);
}
}
);
FLog.v(TAG, "Successful disk-cache write for key %s", key.toString());
} catch (IOException ioe) {
// Log failure
// TODO: 3697790
FLog.w(TAG, ioe, "Failed to write to disk-cache for key %s", key.toString());
}
}
}