/* * 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.memory.PooledByteBuffer; import com.facebook.imagepipeline.memory.PooledByteBufferInputStream; 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 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<CloseableReference<PooledByteBuffer>> get( final CacheKey key, final AtomicBoolean isCancelled) { Preconditions.checkNotNull(key); Preconditions.checkNotNull(isCancelled); final CloseableReference<PooledByteBuffer> 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<CloseableReference<PooledByteBuffer>>() { @Override public CloseableReference<PooledByteBuffer> call() throws Exception { if (isCancelled.get()) { throw new CancellationException(); } CloseableReference<PooledByteBuffer> 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); result = CloseableReference.of(buffer); } 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 byteBuffer 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, CloseableReference<PooledByteBuffer> byteBuffer) { Preconditions.checkNotNull(key); Preconditions.checkArgument(CloseableReference.isValid(byteBuffer)); // Store byteBuffer in staging area mStagingArea.put(key, byteBuffer); // 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 CloseableReference<PooledByteBuffer> finalByteBuffer = byteBuffer.clone(); try { mWriteExecutor.execute( new Runnable() { @Override public void run() { try { writeToDiskCache(key, finalByteBuffer.get()); } finally { mStagingArea.remove(key, finalByteBuffer); finalByteBuffer.close(); } } }); } 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, byteBuffer); finalByteBuffer.close(); } } /** * 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 PooledByteBuffer buffer) { 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(new PooledByteBufferInputStream(buffer), 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()); } } }