blob: 1e7a9e7f16bbf20bb7ed475817b7a40c3766b18f [file] [log] [blame]
package com.bumptech.glide.load.engine;
import android.os.Looper;
import android.os.MessageQueue;
import android.util.Log;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.Key;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.engine.cache.DiskCache;
import com.bumptech.glide.load.engine.cache.MemoryCache;
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
import com.bumptech.glide.provider.DataLoadProvider;
import com.bumptech.glide.request.ResourceCallback;
import com.bumptech.glide.util.LogTime;
import com.bumptech.glide.util.Util;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
/**
* Responsible for starting loads and managing active and cached resources.
*/
public class Engine implements EngineJobListener, MemoryCache.ResourceRemovedListener, EngineResource.ResourceListener {
private static final String TAG = "Engine";
private final Map<Key, EngineJob> jobs;
private final EngineKeyFactory keyFactory;
private final MemoryCache cache;
private final DiskCache diskCache;
private final EngineJobFactory engineJobFactory;
private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
private final ReferenceQueue<EngineResource<?>> resourceReferenceQueue;
private final ResourceRecycler resourceRecycler;
/**
* Allows a request to indicate it no longer is interested in a given load.
*/
public static class LoadStatus {
private final EngineJob engineJob;
private final ResourceCallback cb;
public LoadStatus(ResourceCallback cb, EngineJob engineJob) {
this.cb = cb;
this.engineJob = engineJob;
}
public void cancel() {
engineJob.removeCallback(cb);
}
}
public Engine(MemoryCache memoryCache, DiskCache diskCache, ExecutorService diskCacheService,
ExecutorService sourceService) {
this(memoryCache, diskCache, diskCacheService, sourceService, null, null, null, null, null);
}
// Visible for testing.
Engine(MemoryCache cache, DiskCache diskCache, ExecutorService diskCacheService, ExecutorService sourceService,
Map<Key, EngineJob> jobs, EngineKeyFactory keyFactory,
Map<Key, WeakReference<EngineResource<?>>> activeResources, EngineJobFactory engineJobFactory,
ResourceRecycler resourceRecycler) {
this.cache = cache;
this.diskCache = diskCache;
if (activeResources == null) {
activeResources = new HashMap<Key, WeakReference<EngineResource<?>>>();
}
this.activeResources = activeResources;
if (keyFactory == null) {
keyFactory = new EngineKeyFactory();
}
this.keyFactory = keyFactory;
if (jobs == null) {
jobs = new HashMap<Key, EngineJob>();
}
this.jobs = jobs;
if (engineJobFactory == null) {
engineJobFactory = new EngineJobFactory(diskCacheService, sourceService, this);
}
this.engineJobFactory = engineJobFactory;
if (resourceRecycler == null) {
resourceRecycler = new ResourceRecycler();
}
this.resourceRecycler = resourceRecycler;
resourceReferenceQueue = new ReferenceQueue<EngineResource<?>>();
MessageQueue queue = Looper.myQueue();
queue.addIdleHandler(new RefQueueIdleHandler(activeResources, resourceReferenceQueue));
cache.setResourceRemovedListener(this);
}
/**
* Starts a load for the given arguments. Must be called on the main thread.
*
* <p>
* The flow for any request is as follows:
* <ul>
* <li>Check the memory cache and provide the cached resource if present</li>
* <li>Check the current set of actively used resources and return the active resource if present</li>
* <li>Check the current set of in progress loads and add the cb to the in progress load if present</li>
* <li>Start a new load</li>
* </ul>
* </p>
*
* <p>
* Active resources are those that have been provided to at least one request and have not yet been released.
* Once all consumers of a resource have released that resource, the resource then goes to cache. If the
* resource is ever returned to a new consumer from cache, it is re-added to the active resources. If the
* resource is evicted from the cache, its resources are recycled and re-used if possible and the resource is
* discarded. There is no strict requirement that consumers release their resources so active resources are
* held weakly.
* </p>
*
* @param signature A non-null unique key to be mixed into the cache key that identifies the version of the data to
* be loaded.
* @param width The target width in pixels of the desired resource.
* @param height The target height in pixels of the desired resource.
* @param fetcher The fetcher to use to retrieve data not in the disk cache.
* @param loadProvider The load provider containing various encoders and decoders use to decode and encode data.
* @param transformation The transformation to use to transform the decoded resource.
* @param transcoder The transcoder to use to transcode the decoded and transformed resource.
* @param priority The priority with which the request should run.
* @param isMemoryCacheable True if the transcoded resource can be cached in memory.
* @param diskCacheStrategy The strategy to use that determines what type of data, if any,
* will be cached in the local disk cache.
* @param cb The callback that will be called when the load completes.
*
* @param <T> The type of data the resource will be decoded from.
* @param <Z> The type of the resource that will be decoded.
* @param <R> The type of the resource that will be transcoded from the decoded resource.
*/
public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
Util.assertMainThread();
long startTime = LogTime.getLogTime();
final String id = fetcher.getId();
EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
transcoder, loadProvider.getSourceEncoder());
EngineResource<?> cached = getFromCache(key);
if (cached != null) {
cached.acquire();
activeResources.put(key, new ResourceWeakReference(key, cached, resourceReferenceQueue));
cb.onResourceReady(cached);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return null;
}
WeakReference<EngineResource<?>> activeRef = activeResources.get(key);
if (activeRef != null) {
EngineResource<?> active = activeRef.get();
if (active != null) {
active.acquire();
cb.onResourceReady(active);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return null;
} else {
activeResources.remove(key);
}
}
EngineJob current = jobs.get(key);
if (current != null) {
current.addCallback(cb);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Added to existing load", startTime, key);
}
return new LoadStatus(cb, current);
}
EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
transcoder, diskCache, diskCacheStrategy, priority);
EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
jobs.put(key, engineJob);
engineJob.addCallback(cb);
engineJob.start(runnable);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Started new load", startTime, key);
}
return new LoadStatus(cb, engineJob);
}
private static void logWithTimeAndKey(String log, long startTime, Key key) {
Log.v(TAG, log + " in " + LogTime.getElapsedMillis(startTime) + "ms, key: " + key);
}
@SuppressWarnings("unchecked")
private EngineResource<?> getFromCache(Key key) {
Resource<?> cached = cache.remove(key);
final EngineResource result;
if (cached == null) {
result = null;
} else if (cached instanceof EngineResource) {
// Save an object allocation if we've cached an EngineResource (the typical case).
result = (EngineResource) cached;
} else {
result = new EngineResource(cached, true /*isCacheable*/);
}
return result;
}
public void release(Resource resource) {
if (resource instanceof EngineResource) {
((EngineResource) resource).release();
} else {
throw new IllegalArgumentException("Cannot release anything but an EngineResource");
}
}
@SuppressWarnings("unchecked")
@Override
public void onEngineJobComplete(Key key, EngineResource<?> resource) {
// A null resource indicates that the load failed, usually due to an exception.
if (resource != null) {
resource.setResourceListener(key, this);
activeResources.put(key, new ResourceWeakReference(key, resource, resourceReferenceQueue));
}
// TODO: should this check that the engine job is still current?
jobs.remove(key);
}
@Override
public void onEngineJobCancelled(EngineJob engineJob, Key key) {
EngineJob current = jobs.get(key);
if (engineJob.equals(current)) {
jobs.remove(key);
}
}
@Override
public void onResourceRemoved(final Resource<?> resource) {
resourceRecycler.recycle(resource);
}
@Override
public void onResourceReleased(Key cacheKey, EngineResource resource) {
activeResources.remove(cacheKey);
if (resource.isCacheable()) {
cache.put(cacheKey, resource);
} else {
resourceRecycler.recycle(resource);
}
}
private static class ResourceWeakReference extends WeakReference<EngineResource<?>> {
private final Key key;
public ResourceWeakReference(Key key, EngineResource<?> r, ReferenceQueue<? super EngineResource<?>> q) {
super(r, q);
this.key = key;
}
}
// Responsible for cleaning up the active resource map by remove weak references that have been cleared.
private static class RefQueueIdleHandler implements MessageQueue.IdleHandler {
private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
private final ReferenceQueue<EngineResource<?>> queue;
public RefQueueIdleHandler(Map<Key, WeakReference<EngineResource<?>>> activeResources,
ReferenceQueue<EngineResource<?>> queue) {
this.activeResources = activeResources;
this.queue = queue;
}
@Override
public boolean queueIdle() {
ResourceWeakReference ref = (ResourceWeakReference) queue.poll();
if (ref != null) {
activeResources.remove(ref.key);
}
return true;
}
}
// Visible for testing.
static class EngineJobFactory {
private final ExecutorService diskCacheService;
private final ExecutorService sourceService;
private final EngineJobListener listener;
public EngineJobFactory(ExecutorService diskCacheService, ExecutorService sourceService,
EngineJobListener listener) {
this.diskCacheService = diskCacheService;
this.sourceService = sourceService;
this.listener = listener;
}
public EngineJob build(Key key, boolean isMemoryCacheable) {
return new EngineJob(key, diskCacheService, sourceService, isMemoryCacheable, listener);
}
}
}