前文分析了Glide的with()、load()以及into()方法中的大部分逻辑,今天我们来分析Glide的核心图片加载逻辑。因为Glide的封装程度比较高,之前三篇文章分析完毕其外围逻辑(参阅:〔两行哥〕提纲挈领,带你梳理Glide主要源码逻辑(一),〔两行哥〕提纲挈领,带你梳理Glide主要源码逻辑(二),〔两行哥〕提纲挈领,带你梳理Glide主要源码逻辑(三))。
那么Glide图片加载的核心逻辑在哪儿呢?就是上文结尾所说的engine.load()方法,在engine.load()方法中传入了上文已经分析过的DataFetcher实例、LoadProvider实例以及ResourceTransCoder实例。让我们进入源码分析看看。
Engine.java
/**
* 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 = loadFromCache(key, isMemoryCacheable);
if (cached != null) {
cb.onResourceReady(cached);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return null;
}
EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
if (active != null) {
cb.onResourceReady(active);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return null;
}
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, diskCacheProvider, 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 EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}
EngineResource<?> active = null;
WeakReference<EngineResource<?>> activeRef = activeResources.get(key);
if (activeRef != null) {
active = activeRef.get();
if (active != null) {
active.acquire();
} else {
activeResources.remove(key);
}
}
return active;
}
private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}
EngineResource<?> cached = getEngineResourceFromCache(key);
if (cached != null) {
cached.acquire();
activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
}
return cached;
}
首先先做一下知识的铺垫。我们知道Glide中有三级缓存机制,拥有内存缓存和硬盘缓存。而内存缓存同时采用了两种机制,一种是前文讲过的LruCache算法机制(什么是LruCache算法?请参阅前文),另外一种就是弱引用机制。缓存的作用是什么呢?内存缓存的作用就是防止把图片重复加载到内存中,而硬盘缓存的作用就是防止重复从网络中获取图片。
一、内存缓存
让我们回到load()方法中。首先调用了fetcher.getId()获得了一个唯一标识Id。这个Id是图片加载的唯一标识Id(如果是网络图片加载请求,那么其实就是图片的Url)。接着通过EngineKey的构建者模式,传入了获取到的Id、Width、Height、transformation等参数,构建了另外一个标识Key。这个Key是什么呢?上文讲到Glide的内存缓存,这个Key就是内存缓存的唯一标识Key,与传入的Id、Width、Height、transformation参数相关。为什么要与这么多参数相关呢?大家想一下,需要加载的图片只有与内存缓存中的某图片Url、宽高、解码方式等一致,才可以直接取用内存缓存。如果需要加载的图片改变了Url或者宽高或者解码方式,那么原有旧的内存缓存就不能使用了,需要重新缓存。那么在EngineKey的构建方法中传入如此多的参数也可以理解了。
接着往下看。上文说过Glide的内存缓存同时使用了两种缓存机制,一种是LruCache缓存机制,一种是弱引用缓存机制,让我们一一分析。首先,load()方法内部调用了loadFromCache()方法,源码如下:
Engine.java
private final MemoryCache cache;
private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}
EngineResource<?> cached = getEngineResourceFromCache(key);
if (cached != null) {
cached.acquire();
activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
}
return cached;
}
@SuppressWarnings("unchecked")
private EngineResource<?> getEngineResourceFromCache(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;
}
可以看到逻辑是从cache中获取到缓存(cache中存储的是曾经使用过,而当前没有在使用中的缓存。通过调用cache.remove(key)方法删除缓存并返回被删除的缓存),如果获取到的缓存result不为null,调用 activeResources.put()方法将此缓存放入激活缓存Map中(正在使用中的缓存)。而这里的cache为实现了MemoryCache接口的LruResourceCache类的实例。我们稍微看一下LruResourceCache类的源码:
LruResourceCache.java
public class LruResourceCache extends LruCache<Key, Resource<?>> implements MemoryCache {
private ResourceRemovedListener listener;
/**
* Constructor for LruResourceCache.
*
* @param size The maximum size in bytes the in memory cache can use.
*/
public LruResourceCache(int size) {
super(size);
}
@Override
public void setResourceRemovedListener(ResourceRemovedListener listener) {
this.listener = listener;
}
@Override
protected void onItemEvicted(Key key, Resource<?> item) {
if (listener != null) {
listener.onResourceRemoved(item);
}
}
@Override
protected int getSize(Resource<?> item) {
return item.getSize();
}
@SuppressLint("InlinedApi")
@Override
public void trimMemory(int level) {
if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
// Nearing middle of list of cached background apps
// Evict our entire bitmap cache
clearMemory();
} else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
// Entering list of cached background apps
// Evict oldest half of our bitmap cache
trimToSize(getCurrentSize() / 2);
}
}
}
留意一下isMemoryCacheable变量,这个布尔值是在配置Glide参数的时候设置的,决定了Glide是否使用内存缓存。如果为false,这里获取内存缓存的方法就会返回false。LruResourceCache类就不再赘述,之前的文章就已经分析过LruCache,只要知道cache是基于LruCache算法,内部是基于LinkedHashMap实现的。
回到load()方法中,如果loadFromCache()方法返回的cached对象不为null,则执行onResourceReady()回调。如果获取到的cached为null(没有从LruCache中的LinkedHashMap中获取到缓存),则调用loadFromActiveResources()方法,再去激活缓存的Map中(使用中的缓存)去寻找是否有缓存,看看内部源码的逻辑:
Engine.java
private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}
EngineResource<?> active = null;
WeakReference<EngineResource<?>> activeRef = activeResources.get(key);
if (activeRef != null) {
active = activeRef.get();
if (active != null) {
active.acquire();
} else {
activeResources.remove(key);
}
}
return active;
}
activeResources是持有了缓存弱引用的HashMap,而这里的逻辑主要是从activeResources这个HashMap中获取到图片的缓存。如果调用loadFromActiveResources()方法返回的缓存不为null,同样执行onResourceReady()回调方法,如果返回的缓存为null,则进入下一步,从磁盘中或者网络去获取图片。
二、磁盘缓存及图片获取
Glide磁盘缓存与内存缓存类似,也采用了LruCache算法。首先让我们追踪一下Glide读取磁盘缓存的逻辑。
在读取内存缓存之后,如果没有获取到内存缓存,Glide就会去读取磁盘缓存,如下代码:
EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
jobs.put(key, engineJob);
engineJob.addCallback(cb);
engineJob.start(runnable);
首先创建了一个EngineJob并放入jobs集合,然后engineJob开始执行传入的EngineRunnable任务。本地磁盘缓存的逻辑就隐藏在EngineRunnable中,让我们看看这个Runnable对象中覆写的run()方法:
EngineRunnable.java
@Override
public void run() {
if (isCancelled) {
return;
}
Exception exception = null;
Resource<?> resource = null;
try {
resource = decode();
} catch (OutOfMemoryError e) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Out Of Memory Error decoding", e);
}
exception = new ErrorWrappingGlideException(e);
} catch (Exception e) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Exception decoding", e);
}
exception = e;
}
if (isCancelled) {
if (resource != null) {
resource.recycle();
}
return;
}
if (resource == null) {
onLoadFailed(exception);
} else {
onLoadComplete(resource);
}
}
可以看到run()方法中执行了decode()方法,并在执行结束后根据执行的结果,调用了onLoadFailed()或者onLoadComplete()回调。再让我们看看decode()方法到底做了哪些工作。
EngineRunnable.java
private Resource<?> decode() throws Exception {
...
private boolean isDecodingFromCache() {
return stage == Stage.CACHE;
}
...
if (isDecodingFromCache()) {
return decodeFromCache();
} else {
return decodeFromSource();
}
...
}
decode()方法根据isDecodingFromCache()方法的返回值(这个参数在初始化Glile的时候进行过配置,是否使用磁盘缓存),分别调用decodeFromCache()和decodeFromSource()。磁盘缓存的逻辑终于被找到了,就在decodeFromCache()中!
EngineRunnable.java
private Resource<?> decodeFromCache() throws Exception {
Resource<?> result = null;
try {
result = decodeJob.decodeResultFromCache();
} catch (Exception e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Exception decoding result from cache: " + e);
}
}
if (result == null) {
result = decodeJob.decodeSourceFromCache();
}
return result;
}
可以看到decodeFromCache()方法中执行了decodeResultFromCache()或decodeSourceFromCache()方法。首先调用decodeResultFromCache()方法获取缓存,如果获取不到,就调用decodeSourceFromCache()方法获取缓存。至于什么时候执行第一个方法,什么时候又会执行到第二个方法,与我们在第一篇中讲的磁盘缓存配置策略有关:
diskCacheStrategy(DiskCacheStrategy.RESULT)//缓存处理后的图像(如尺寸调整、裁剪后的图像)
diskCacheStrategy(DiskCacheStrategy.SOURCE)//缓存原尺寸的图像
diskCacheStrategy(DiskCacheStrategy.ALL)//缓存所有图像
见名知意,两行哥就不再深入讲解了,让我们看一下源码:
DecodeJob.java
public Resource<Z> decodeResultFromCache() throws Exception {
if (!diskCacheStrategy.cacheResult()) {
return null;
}
long startTime = LogTime.getLogTime();
Resource<T> transformed = loadFromCache(resultKey);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Decoded transformed from cache", startTime);
}
startTime = LogTime.getLogTime();
Resource<Z> result = transcode(transformed);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Transcoded transformed from cache", startTime);
}
return result;
}
public Resource<Z> decodeSourceFromCache() throws Exception {
if (!diskCacheStrategy.cacheSource()) {
return null;
}
long startTime = LogTime.getLogTime();
Resource<T> decoded = loadFromCache(resultKey.getOriginalKey());
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Decoded source from cache", startTime);
}
return transformEncodeAndTranscode(decoded);
}
可以看到,它们内部都调用了loadFromCache()方法从缓存当中获取数据,如果是decodeResultFromCache()方法就直接将数据解码并返回,如果是decodeSourceFromCache()方法,还要调用一下transformEncodeAndTranscode()方法转换数据后再解码并返回。
再者,它们调用loadFromCache()方法时传入的参数却不一样,一个传入的是resultKey,另外一个却又调用了resultKey的getOriginalKey()方法,分别对应处理后图片缓存Key和原图缓存Key。上文讲过,Glide的缓存Key是由多个参数共同组成的,包括图片的width、height等。但如果我们如果缓存的原始图片,其实并不需要这么多的参数,因为并没有对原始图片进行过尺寸缩放等额外处理。那么我们来看一下getOriginalKey()方法的源码:
OriginalKey.java
public Key getOriginalKey() {
if (originalKey == null) {
originalKey = new OriginalKey(id, signature);
}
return originalKey;
}
可以看到,这里忽略了大部分的参数,只使用了id和signature两个参数来构成Key,而signature参数大多数情况下都是用不到的,因此几乎是由id(也就是图片url)来决定OriginalKey。
接下来让我们回到loadFromCache()方法,看看内部的源码:
DecodeJob.java
private Resource<T> loadFromCache(Key key) throws IOException {
File cacheFile = diskCacheProvider.getDiskCache().get(key);
if (cacheFile == null) {
return null;
}
Resource<T> result = null;
try {
result = loadProvider.getCacheDecoder().decode(cacheFile, width, height);
} finally {
if (result == null) {
diskCacheProvider.getDiskCache().delete(key);
}
}
return result;
}
loadFromCache()方法调用getDiskCache()方法获取到的就是Glide自己编写的DiskLruCache工具类的实例,然后调用它的get()方法并把缓存key传入,得到硬盘缓存的文件。如果文件为null就返回null,如果文件不为空则将它解码成Resource对象后返回。
让我们重新回到decode()方法,看看方法中另一个else分支decodeFromSource()方法执行了什么逻辑:
DecodeJob.java
public Resource<Z> decodeFromSource() throws Exception {
Resource<T> decoded = decodeSource();
return transformEncodeAndTranscode(decoded);
}
private Resource<T> decodeSource() throws Exception {
Resource<T> decoded = null;
try {
long startTime = LogTime.getLogTime();
final A data = fetcher.loadData(priority);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Fetched data", startTime);
}
if (isCancelled) {
return null;
}
decoded = decodeFromSourceData(data);
} finally {
fetcher.cleanup();
}
return decoded;
}
private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
long startTime = LogTime.getLogTime();
Resource<T> transformed = transform(decoded);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Transformed resource from source", startTime);
}
writeTransformedToCache(transformed);
startTime = LogTime.getLogTime();
Resource<Z> result = transcode(transformed);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Transcoded transformed from source", startTime);
}
return result;
}
decodeFromSource()中只有两行代码,decodeSource()顾名思义是用来解析原图片的,而transformEncodeAndTranscode()则是用来对图片进行转换和转码的。decodeSource()方法中主要做了如下逻辑:
1.通过 fetcher.loadData(priority)方法来获取到图片数据,比如从网络获取图片资源;
2.通过decodeFromSourceData(data)方法来解码数据:
3.decodeFromSourceData()方法中,判断配置的缓存策略是否支持磁盘缓存,如果支持的话,则调用cacheAndDecodeSourceData()方法来缓存数据及解码。Glide提供了DiskCacheProvider类,其本质是封装后的DiskLruCache实例。
相关代码如下:
HttpUrlFetcher.java
@Override
public InputStream loadData(Priority priority) throws Exception {
return loadDataWithRedirects(glideUrl.toURL(), 0 /*redirects*/, null /*lastUrl*/, glideUrl.getHeaders());
}
private InputStream loadDataWithRedirects(URL url, int redirects, URL lastUrl, Map<String, String> headers)
throws IOException {
if (redirects >= MAXIMUM_REDIRECTS) {
throw new IOException("Too many (> " + MAXIMUM_REDIRECTS + ") redirects!");
} else {
// Comparing the URLs using .equals performs additional network I/O and is generally broken.
// See http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html.
try {
if (lastUrl != null && url.toURI().equals(lastUrl.toURI())) {
throw new IOException("In re-direct loop");
}
} catch (URISyntaxException e) {
// Do nothing, this is best effort.
}
}
urlConnection = connectionFactory.build(url);
for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue());
}
urlConnection.setConnectTimeout(2500);
urlConnection.setReadTimeout(2500);
urlConnection.setUseCaches(false);
urlConnection.setDoInput(true);
// Connect explicitly to avoid errors in decoders if connection fails.
urlConnection.connect();
if (isCancelled) {
return null;
}
final int statusCode = urlConnection.getResponseCode();
if (statusCode / 100 == 2) {
return getStreamForSuccessfulRequest(urlConnection);
} else if (statusCode / 100 == 3) {
String redirectUrlString = urlConnection.getHeaderField("Location");
if (TextUtils.isEmpty(redirectUrlString)) {
throw new IOException("Received empty or null redirect url");
}
URL redirectUrl = new URL(url, redirectUrlString);
return loadDataWithRedirects(redirectUrl, redirects + 1, url, headers);
} else {
if (statusCode == -1) {
throw new IOException("Unable to retrieve response code from HttpUrlConnection.");
}
throw new IOException("Request failed " + statusCode + ": " + urlConnection.getResponseMessage());
}
}
DecodeJob.java
private Resource<T> decodeFromSourceData(A data) throws IOException {
final Resource<T> decoded;
if (diskCacheStrategy.cacheSource()) {
decoded = cacheAndDecodeSourceData(data);
} else {
long startTime = LogTime.getLogTime();
decoded = loadProvider.getSourceDecoder().decode(data, width, height);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Decoded from source", startTime);
}
}
return decoded;
}
private Resource<T> cacheAndDecodeSourceData(A data) throws IOException {
long startTime = LogTime.getLogTime();
SourceWriter<A> writer = new SourceWriter<A>(loadProvider.getSourceEncoder(), data);
diskCacheProvider.getDiskCache().put(resultKey.getOriginalKey(), writer);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Wrote source to cache", startTime);
}
startTime = LogTime.getLogTime();
Resource<T> result = loadFromCache(resultKey.getOriginalKey());
if (Log.isLoggable(TAG, Log.VERBOSE) && result != null) {
logWithTimeAndKey("Decoded source from cache", startTime);
}
return result;
}
注意,diskCacheProvider调用了getDiskCache()方法获取DiskLruCache实例,接着调用它的put()方法写入硬盘缓存,这里原始图片的缓存key用的是resultKey.getOriginalKey(),最后让我们再看一下decodeFromSource()方法中第二行transformEncodeAndTranscode(decoded)方法内部做了哪些逻辑:
DecodeJob.java
private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
long startTime = LogTime.getLogTime();
Resource<T> transformed = transform(decoded);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Transformed resource from source", startTime);
}
writeTransformedToCache(transformed);
startTime = LogTime.getLogTime();
Resource<Z> result = transcode(transformed);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Transcoded transformed from source", startTime);
}
return result;
}
private void writeTransformedToCache(Resource<T> transformed) {
if (transformed == null || !diskCacheStrategy.cacheResult()) {
return;
}
long startTime = LogTime.getLogTime();
SourceWriter<Resource<T>> writer = new SourceWriter<Resource<T>>(loadProvider.getEncoder(), transformed);
diskCacheProvider.getDiskCache().put(resultKey, writer);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Wrote transformed from source to cache", startTime);
}
}
transformEncodeAndTranscode()方法首先调用transform()方法来对图片进行转换,然后在writeTransformedToCache()方法中将转换过后的图片写入到硬盘缓存中,调用的同样是DiskLruCache实例的put()方法,不过这里用的缓存key是resultKey。
至此,我们已经把Glide内部主要逻辑分析完毕,可以说Glide是Android现有开源框架出色封装的典范,封装后用户完全感觉不到内部复杂的逻辑,仅仅通过一行链式调用就完成了很多逻辑。不得不惊叹作者的高超代码艺术。让我们下次再会。
网友评论