写在前面
对于一个应用来讲所需要的图片不可能总是来自他自己的apk包中,总会有一些实时的图片来自于网络、服务器中,而为了流量、加载速度等方面的考虑,我们做不到每一次都是从网络中下载,为了解决这个问题,我们提出了缓存这个概念。
如果你发现本文中有任何错误,请在评论区留言或者私信我,我会第一时间改正,谢谢!
0 图片的三级缓存
对于图片来讲,缓存是十分有必要的,在Android的发展史中,图片的缓存慢慢分成了三级。
内存缓存 本地缓存 网络
相对具体的可以参看这篇文章:三级缓存
1 Glide中的disk缓存
为什么先要将disk缓存,而不是内存缓存呢?
因为disk缓存,是紧挨着网络的,而内存缓存不是紧挨网络的,内存缓存与网络中间还隔了一层disk缓存。由于已经初步了解Glide的加载过程之后,先去了解disk缓存远远比了解内存缓存要容易。
在Glide中,disk缓存的获取是在EngineRunning中,在上一篇博文中,我们可以知道第一次执行EngineRunning的状态都是Stage.CACHE
,即从缓存中获取,看一下具体的实现
//EngineRunning
private Resource<?> decode() throws Exception {
//不设置的话这个判断第一次是会放回true
if (isDecodingFromCache()) {
return decodeFromCache();
} else {
return decodeFromSource();
}
}
看一下decodeFromCache的实现
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;
}
//decodeJob.java
/**
* Returns a transcoded resource decoded from transformed resource data in the disk cache, or null if no such
* resource exists.
*
* @throws Exception
*/
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;
}
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;
}
不难看出这一层是使用diskLruCache来获取缓存的,再看下去
//EngineRunnable.java
private Resource<?> decodeFromCache() throws Exception {
...
if (result == null) {
result = decodeJob.decodeSourceFromCache();
}
return result;
}
//DecodeJob.java
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);
}
乍一看,这更前面获取disk缓存没什么差别,但是其实这里是做了小区分的。
在Glide中,对应缓存key的类是EngineKey,而这个EngineKey的构成是由很多因素构成的,至于会被什么因素影响,可以通过EngineKey的构造函数粗略的知晓。
而在这里,这两个获取disk缓存的key是不同地,第一次获取用的是完整的EngineKey,即是带有长、宽等约束条件的EngineKey,获取出来之后可以直接复用。
而第二次获取用的key带的约束条件只有id和signature,即没有加工过的原图片。如果这个原图片可以获取得到的话,需要后续加工即transformEncodeAndTranscode方法后,才能使用。
再看一下transformEncodeAndTranscode的具体实现
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);
}
}
可以看到writeTransformedToCache又把图片资源文件(带有长宽的)放入disk缓存中,至此disk缓存中就有两张照片了,他们一张是原图,一张是加工后的图片。
这以上都是取得逻辑,再看一下存的逻辑,当下载完成图片之后,会把图片存到disk缓存中,逻辑还在decodeJob类中,关注一下
//decodeJob
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<T> decodeFromSourceData(A data) throws IOException {
final Resource<T> decoded;
//判断是否要disk缓存
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;
}
下载好的图片首先会经过decodeFromSourceData方法,然后进入cacheAndDecodeSourceData这个方法是存原图的。后续的话就会过一遍transformEncodeAndTranscode这个方法(存带有长宽的图片)
到这里disk缓存也就讲完了。
2 Glide中的内存缓存
相比于disk缓存,内存缓存的相应速度要快上许多,我们最期望的图片获取方式是从内存中获取,看一下Glide是怎么获取的吧
在Glide中,内存获取的方式是在Engine中,看一下具体的实现
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);
}
可以看到这里先创建了一个EngineKey,然后再调用loadFromCache方法,看一下这个方法的实现
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;
}
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;
}
这里先判断是否需要使用内存缓存,不需要直接return null,接着调用了getEngineResourceFromCache这个方法。getEngineResourceFromCache这个方法尝试从cache中获取图片,cache是在with过程中就被初始话的参数
//GlideBuilder.java
if (memoryCache == null) {
memoryCache = new LruResourceCache(calculator.getMemoryCacheSize());
}
if (diskCacheFactory == null) {
diskCacheFactory = new InternalCacheDiskCacheFactory(context);
}
看一下这个LruResourceCache类,它是继承了LruCache的,显然他就是属于内存缓存的。那么getEngineResourceFromCache的作用就是从lru中获取出来对应的图片并吧图片转化成EngineResource类。回到loadFromCache方法,得到图片之后他做了一个很有意思的操作。
if (cached != null) {
cached.acquire();
activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
}
看一下cached.acquire()
这个的实现
//EngineResource.java
private int acquired;
void acquire() {
if (isRecycled) {
throw new IllegalStateException("Cannot acquire a recycled resource");
}
if (!Looper.getMainLooper().equals(Looper.myLooper())) {
throw new IllegalThreadStateException("Must call acquire on the main thread");
}
++acquired;
}
void release() {
if (acquired <= 0) {
throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
}
if (!Looper.getMainLooper().equals(Looper.myLooper())) {
throw new IllegalThreadStateException("Must call release on the main thread");
}
if (--acquired == 0) {
listener.onResourceReleased(key, this);
}
}
这个方法将EngineResource的acquired+1,与之对应的是在release的时候-1,还可以看到release的时候还多了一个判断,并且当acquired==0的时候调用了 listener.onResourceReleased方法。这个listener我们先暂时放一下,这里还不能确定他指的是什么。
在cached.acquire();
之后,他还调用了activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
方法,activeResources是一个HashMap<Key, WeakReference<EngineResource<?>>>()
。用于存储从memoryCache中remove出来的值。再回到load方法里面
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;
}
如果从loadFromCache从LruCache中或取出来图片,直接通过回调返回出去。再看下去
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;
}
又调用了loadFromActiveResources方法获取,看一下具体的实现
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中通过key查找缓存,如果有就返回,没有就返回null
看完了获取内存缓存,再看一下怎么放到内存缓存中的。根据图片的三级缓存规则来讲,放到内存缓存应该是从网络下载完图片之后,在顺便放到disk和内存中的。具体的代码是从EngineJob中开始的
private void handleResultOnMainThread() {
if (isCancelled) {
resource.recycle();
return;
} else if (cbs.isEmpty()) {
··········
throw new IllegalStateException("Received a resource without any callbacks to notify");
}
//关注以下部分代码
engineResource = engineResourceFactory.build(resource, isCacheable);
hasResource = true;
// Hold on to resource for duration of request so we don't recycle it in the middle of notifying if it
// synchronously released by one of the callbacks.
engineResource.acquire();
listener.onEngineJobComplete(key, engineResource);
for (ResourceCallback cb : cbs) {
if (!isInIgnoredCallbacks(cb)) {
engineResource.acquire();
cb.onResourceReady(engineResource);
}
}
// Our request is complete, so we can release the resource.
engineResource.release();
}
首先,根据资源创建相应的EngineResource,然后调用了我们之前调用的acquire方法,注意一下,每有一个callback都会调用一次acquire.再看一下listener.onEngineJobComplete(key, engineResource)
方法
//Engine.java
public void onEngineJobComplete(Key key, EngineResource<?> resource) {
Util.assertMainThread();
// A null resource indicates that the load failed, usually due to an exception.
if (resource != null) {
resource.setResourceListener(key, this);
if (resource.isCacheable()) {
activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue()));
}
}
// TODO: should this check that the engine job is still current?
jobs.remove(key);
}
这个方法里给resource设置了Listener。那么看一下这个listener干了什么吧
//EngineResource.java
void release() {
if (acquired <= 0) {
throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
}
if (!Looper.getMainLooper().equals(Looper.myLooper())) {
throw new IllegalThreadStateException("Must call release on the main thread");
}
if (--acquired == 0) {
listener.onResourceReleased(key, this);
}
}
//Engine.java
@Override
public void onResourceReleased(Key cacheKey, EngineResource resource) {
Util.assertMainThread();
activeResources.remove(cacheKey);
if (resource.isCacheable()) {
cache.put(cacheKey, resource);
} else {
resourceRecycler.recycle(resource);
}
}
这个Listener在acquired =0的时候,会把activeResources中的图片放回到LruCache中。
结合以上的总结,可以知道acquired是用来表示这个图片资源有多少正在被使用的。当这个值被清空的时候,会把弱引用中的图片放回LruCache中。
总的来讲,Glide缓存相关的逻辑也就讲完了。这里整个缓存的逻辑是很完善,非要鸡蛋里挑骨头的话,只有在内存缓存中获取的逻辑顺序应该调换一下,应该先从hashmap中获取,然后在从LruCache中获取,而在Glide的后续版本(4.0.10)上面,这里也确实被调整过来了。
/**
* Starts a load for the given arguments.
*
* <p>Must be called on the main thread.
*
* <p>The flow for any request is as follows:
*
* <ul>
* <li>Check the current set of actively used resources, return the active resource if present,
* and move any newly inactive resources into the memory cache.
* <li>Check the memory cache and provide the cached resource if present.
* <li>Check the current set of in progress loads and add the cb to the in progress load if one
* is present.
* <li>Start a new load.
* </ul>
*
* <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.
*
* @param width The target width in pixels of the desired resource.
* @param height The target height in pixels of the desired resource.
* @param cb The callback that will be called when the load completes.
*/
public <R> LoadStatus load(
GlideContext glideContext,
Object model,
Key signature,
int width,
int height,
Class<?> resourceClass,
Class<R> transcodeClass,
Priority priority,
DiskCacheStrategy diskCacheStrategy,
Map<Class<?>, Transformation<?>> transformations,
boolean isTransformationRequired,
boolean isScaleOnlyOrNoTransform,
Options options,
boolean isMemoryCacheable,
boolean useUnlimitedSourceExecutorPool,
boolean useAnimationPool,
boolean onlyRetrieveFromCache,
ResourceCallback cb,
Executor callbackExecutor) {
long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;
EngineKey key =
keyFactory.buildKey(
model,
signature,
width,
height,
transformations,
resourceClass,
transcodeClass,
options);
EngineResource<?> memoryResource;
synchronized (this) {
memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
if (memoryResource == null) {
return waitForExistingOrStartNewJob(
glideContext,
model,
signature,
width,
height,
resourceClass,
transcodeClass,
priority,
diskCacheStrategy,
transformations,
isTransformationRequired,
isScaleOnlyOrNoTransform,
options,
isMemoryCacheable,
useUnlimitedSourceExecutorPool,
useAnimationPool,
onlyRetrieveFromCache,
cb,
callbackExecutor,
key,
startTime);
}
}
// Avoid calling back while holding the engine lock, doing so makes it easier for callers to
// deadlock.
cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
return null;
}
@Nullable
private EngineResource<?> loadFromMemory(
EngineKey key, boolean isMemoryCacheable, long startTime) {
if (!isMemoryCacheable) {
return null;
}
EngineResource<?> active = loadFromActiveResources(key);
if (active != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return active;
}
EngineResource<?> cached = loadFromCache(key);
if (cached != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return cached;
}
return null;
}
3 缓存总结
1.Glide的缓存是从Engine开始的,首先生成一个EngineKey,从内存中获取,内存缓存分两类,一种是hashmap保存的弱引用的图片,这种图片是当前页面正在显示的图片,一种是lruCache缓存,保存着从hashmap中移除的照片。
2.如果需要的图片内存缓存中都没有,那么会新建一个EngineJob,然后执行EngineRunning从disk缓存中获取,第一次也是根据EngineKey去disk缓存中查找符合要求的图片,如果有返回,执行3步骤。如果没有,则再去寻找是否有这张图原图(即没有处理过宽高的图片),如果有,执行3步骤。如果没有,再去发起网络请求,将得到的图片的原图保存下来,在执行3步骤。
3.处理中这张图片的宽高,在执行3步骤
4.存入hashmap中,标记为再用图片
5.hashmap中的图片没有被引用的对象,则进入lruCache中
4.Glide into完整过程
那么我们就可以描述完整的一次Glide流程
首先,先会处理ImageView的ScaleType,然后把ImageView处理成一个Target,接着看这个target是否有前一个图片加载请求,如果有就标记无效掉他并清空状态,如果没有就创建出一个request并给这个request创建回调及监听(with中创建的空白的fragment),然后通过requestTracker执行这个request。
紧接着,会测量出target的长宽并回调onSizeReady方法,在这个同时会设置占位图。onSizeReady方法里面,除了赋值长宽之外,还会启动Engine的load方法,在load中首先会获取到一个EngineKey,这个key是由长,宽,地址等构成。组件完成之后,先从两级内存缓存中查找是否有所需要的图片,如果有直接返回。然后会查看是否有这个EngineKey的EngineJob在执行,如果有的话,就不再请求了,采用addCallBack回调集合的方式完成图片的下载。如果没有的话,就通过EngineJobFactory创建一个新的EngineJob,同时创建decodeJob和EngineRunnable,然后通过EngineJob来执行EngineRunning。
首先执行的从disk缓存中获取图片,先从disk缓存中,先获取带有长宽限制的图片,再获取原图,如果获取不到,执行从Source中获取图片的方法。这个方法就是请求网络的方法,是由之前在load方法中创建的SteamFetcher来作为下载图片的工具,这个SteamFetcher是由load的对象决定,返回的对象是InputSteam。
下载完成获取到InputSteam之后,decodeJob把InputSteam转化成Resource对象并存下原图与有长宽限制的图片。接着调用EngineRunning的onLoadComplete方法,onLoadComplete会回调EngineJob的onResourceReady方法,onResourceReady会向EngineJob中的handler发送一条消息,并切换到主线程。切换到主线程之后,EngineJob会回调Engine的方法,设置资源为内存缓存,EngineJob又会回调之前callback集合,通知图片已经准备完成,这个callback实现实在GenericRequest中。接下来,EngineJob中的callback会调用target.onResourceReady方法,这个方法最后也会调用到target.setResource方法,把获取到的图片设置到对应的ImagView中。