寫在前面
對於一個應用來講所需要的圖片不可能總是來自他自己的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中。