天天看點

Glide源碼解析(三)深入探究Glide的緩存機制Glide緩存簡介

轉載自 guolin,略有删減改動

Glide源碼解析(三)深入探究Glide的緩存機制

  • Glide緩存簡介
    • 緩存Key
    • 記憶體緩存
    • 硬碟緩存
    • 進階技巧

在本系列的上一篇文章中,我帶着大家一起閱讀了一遍Glide的源碼,初步了解了這個強大的圖檔加載架構的基本執行流程。

不過,上一篇文章隻能說是比較粗略地閱讀了Glide整個執行流程方面的源碼,搞明白了Glide的基本工作原理,但并沒有去深入分析每一處的細節(事實上也不可能在一篇文章中深入分析每一處源碼的細節)。那麼從本篇文章開始,我們就一篇篇地來針對Glide某一塊功能進行深入地分析,慢慢将Glide中的各項功能進行全面掌握。

今天我們就先從緩存這一塊内容開始入手吧。不過今天文章中的源碼都建在上一篇源碼分析的基礎之上,還沒有看過上一篇文章的朋友,建議先去閱讀 Glide源碼解析(二)從源碼的角度了解Glide的執行流程

Glide緩存簡介

Glide的緩存設計可以說是非常先進的,考慮的場景也很周全。在緩存這一功能上,Glide又将它分成了兩個子產品,一個是記憶體緩存,一個是硬碟緩存。

這兩個緩存子產品的作用各不相同,記憶體緩存的主要作用是防止應用重複将圖檔資料讀取到記憶體當中,而硬碟緩存的主要作用是防止應用重複從網絡或其他地方重複下載下傳和讀取資料。

記憶體緩存和硬碟緩存的互相結合才構成了Glide極佳的圖檔緩存效果,那麼接下來我們就分别來分析一下這兩種緩存的使用方法以及它們的實作原理。

緩存Key

既然是緩存功能,就必然會有用于進行緩存的Key。那麼Glide的緩存Key是怎麼生成的呢?我不得不說,

Glide的緩存Key生成規則非常繁瑣,決定緩存Key的參數竟然有10個之多

。不過繁瑣歸繁瑣,至少邏輯還是比較簡單的,我們先來看一下Glide緩存Key的生成邏輯。

生成緩存Key的代碼在Engine類的load()方法當中,這部分代碼我們在上一篇文章當中已經分析過了,隻不過當時忽略了緩存相關的内容,那麼我們現在重新來看一下:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {

    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());

        ...
    }

    ...
}
           

可以看到,這裡在第11行調用了fetcher.getId()方法獲得了一個id字元串,這個字元串也就是我們要加載的圖檔的唯一辨別,比如說如果是一張網絡上的圖檔的話,那麼這個id就是這張圖檔的url位址。

接下來在第12行,将這個id連同着signature、width、height等等10個參數一起傳入到EngineKeyFactory的buildKey()方法當中,進而建構出了一個EngineKey對象,這個EngineKey也就是Glide中的緩存Key了。

可見,

決定緩存Key的條件非常多,即使你用override()方法改變了一下圖檔的width或者height,也會生成一個完全不同的緩存Key

EngineKey類的源碼大家有興趣可以自己去看一下,其實主要就是重寫了equals()和hashCode()方法,保證隻有傳入EngineKey的所有參數都相同的情況下才認為是同一個EngineKey對象,我就不在這裡将源碼貼出來了。

記憶體緩存

有了緩存Key,接下來就可以開始進行緩存了,那麼我們先從記憶體緩存看起。

首先你要知道,預設情況下,Glide自動就是開啟記憶體緩存的。也就是說,當我們使用Glide加載了一張圖檔之後,這張圖檔就會被緩存到記憶體當中,隻要在它還沒從記憶體中被清除之前,下次使用Glide再加載這張圖檔都會直接從記憶體當中讀取,而不用重新從網絡或硬碟上讀取了,這樣無疑就可以大幅度提升圖檔的加載效率。比方說你在一個RecyclerView當中反複上下滑動,RecyclerView中隻要是Glide加載過的圖檔都可以直接從記憶體當中迅速讀取并展示出來,進而大大提升了使用者體驗。

而Glide最為人性化的是,你甚至不需要編寫任何額外的代碼就能自動享受到這個極為便利的記憶體緩存功能,因為Glide預設就已經将它開啟了。

那麼既然已經預設開啟了這個功能,還有什麼可講的用法呢?隻有一點,如果你有什麼特殊的原因需要禁用記憶體緩存功能,Glide對此提供了接口:

Glide.with(this)
     .load(url)
     .skipMemoryCache(true)
     .into(imageView);
           

可以看到,

隻需要調用skipMemoryCache()方法并傳入true,就表示禁用掉Glide的記憶體緩存功能

沒錯,關于Glide記憶體緩存的用法就隻有這麼多,可以說是相當簡單。但是我們不可能隻停留在這麼簡單的層面上,接下來就讓我們就通過閱讀源碼來分析一下Glide的記憶體緩存功能是如何實作的。

其實說到記憶體緩存的實作,非常容易就讓人想到LruCache算法(Least Recently Used),也叫近期最少使用算法。它的

主要算法原理就是把最近使用的對象用強引用存儲在LinkedHashMap中,并且把最近最少使用的對象在緩存值達到預設定值之前從記憶體中移除

。LruCache的用法也比較簡單,我在 Android高效加載大圖、多圖解決方案,有效避免程式OOM 這篇文章當中有提到過它的用法,感興趣的朋友可以去參考一下。

那麼不必多說,Glide記憶體緩存的實作自然也是使用的LruCache算法。不過除了LruCache算法之外,Glide還結合了一種弱引用的機制,共同完成了記憶體緩存功能,下面就讓我們來通過源碼分析一下。

首先回憶一下,在上一篇文章的第二步load()方法中,我們當時分析到了在loadGeneric()方法中會調用Glide.buildStreamModelLoader()方法來擷取一個ModelLoader對象。當時沒有再跟進到這個方法的裡面再去分析,那麼我們現在來看下它的源碼:

public class Glide {

    public static <T, Y> ModelLoader<T, Y> buildModelLoader(Class<T> modelClass, Class<Y> resourceClass,
            Context context) {
         if (modelClass == null) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "Unable to load null model, setting placeholder only");
            }
            return null;
        }
        return Glide.get(context).getLoaderFactory().buildModelLoader(modelClass, resourceClass);
    }

    public static Glide get(Context context) {
        if (glide == null) {
            synchronized (Glide.class) {
                if (glide == null) {
                    Context applicationContext = context.getApplicationContext();
                    List<GlideModule> modules = new ManifestParser(applicationContext).parse();
                    GlideBuilder builder = new GlideBuilder(applicationContext);
                    for (GlideModule module : modules) {
                        module.applyOptions(applicationContext, builder);
                    }
                    glide = builder.createGlide();
                    for (GlideModule module : modules) {
                        module.registerComponents(applicationContext, glide);
                    }
                }
            }
        }
        return glide;
    }

    ...
}
           

這裡我們還是隻看關鍵,在第11行去建構ModelLoader對象的時候,先調用了一個Glide.get()方法,而這個方法就是關鍵。我們可以看到,get()方法中實作的是一個單例功能,而建立Glide對象則是在第24行調用GlideBuilder的createGlide()方法來建立的,那麼我們跟到這個方法當中:

public class GlideBuilder {
    ...

    Glide createGlide() {
        if (sourceService == null) {
            final int cores = Math.max(1, Runtime.getRuntime().availableProcessors());
            sourceService = new FifoPriorityThreadPoolExecutor(cores);
        }
        if (diskCacheService == null) {
            diskCacheService = new FifoPriorityThreadPoolExecutor(1);
        }
        MemorySizeCalculator calculator = new MemorySizeCalculator(context);
        if (bitmapPool == null) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                int size = calculator.getBitmapPoolSize();
                bitmapPool = new LruBitmapPool(size);
            } else {
                bitmapPool = new BitmapPoolAdapter();
            }
        }
        if (memoryCache == null) {
            memoryCache = new LruResourceCache(calculator.getMemoryCacheSize());
        }
        if (diskCacheFactory == null) {
            diskCacheFactory = new InternalCacheDiskCacheFactory(context);
        }
        if (engine == null) {
            engine = new Engine(memoryCache, diskCacheFactory, diskCacheService, sourceService);
        }
        if (decodeFormat == null) {
            decodeFormat = DecodeFormat.DEFAULT;
        }
        return new Glide(engine, memoryCache, bitmapPool, context, decodeFormat);
    }
}
           

這裡也就是建構Glide對象的地方了。那麼觀察第22行,你會發現這裡new出了一個LruResourceCache,并把它指派到了memoryCache這個對象上面。你沒有猜錯,

這個就是Glide實作記憶體緩存所使用的LruCache對象了

。不過我這裡并不打算展開來講LruCache算法的具體實作,如果你感興趣的話可以自己研究一下它的源碼。

現在建立好了LruResourceCache對象隻能說是把準備工作做好了,接下來我們就一步步研究Glide中的記憶體緩存到底是如何實作的。

剛才在Engine的load()方法中我們已經看到了生成緩存Key的代碼,而記憶體緩存的代碼其實也是在這裡實作的,那麼我們重新來看一下Engine類load()方法的完整源碼:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {
    ...    

    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);
    }

    ...
}
           

可以看到,這裡在第17行調用了loadFromCache()方法來擷取緩存圖檔,如果擷取到就直接調用cb.onResourceReady()方法進行回調。如果沒有擷取到,則會在第26行調用loadFromActiveResources()方法來擷取緩存圖檔,擷取到的話也直接進行回調。隻有在兩個方法都沒有擷取到緩存的情況下,才會繼續向下執行,進而開啟線程來加載圖檔。

也就是說,Glide的圖檔加載過程中會調用兩個方法來擷取記憶體緩存,loadFromCache()和loadFromActiveResources()。

這兩個方法中一個使用的就是LruCache算法,另一個使用的就是弱引用

。我們來看一下它們的源碼:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {

    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) {
            result = (EngineResource) cached;
        } else {
            result = new EngineResource(cached, true /*isCacheable*/);
        }
        return result;
    }

    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;
    }

    ...
}
           

在loadFromCache()方法的一開始,首先就判斷了isMemoryCacheable是不是false,如果是false的話就直接傳回null。這是什麼意思呢?其實很簡單,我們剛剛不是學了一個skipMemoryCache()方法嗎?如果在這個方法中

傳入true,那麼這裡的isMemoryCacheable就會是false,表示記憶體緩存已被禁用

我們繼續住下看,接着調用了getEngineResourceFromCache()方法來擷取緩存。

在這個方法中,會使用緩存Key來從cache當中取值,而這裡的cache對象就是在建構Glide對象時建立的LruResourceCache

,那麼說明這裡其實使用的就是LruCache算法了。

但是呢,觀察第22行,當我們從LruResourceCache中擷取到緩存圖檔之後會将它從緩存中移除,然後在第16行将這個緩存圖檔存儲到activeResources當中。

activeResources就是一個弱引用的HashMap,用來緩存正在使用中的圖檔

,我們可以看到,loadFromActiveResources()方法就是從activeResources這個HashMap當中取值的。

使用activeResources來緩存正在使用中的圖檔,可以保護這些圖檔不會被LruCache算法回收掉

好的,從記憶體緩存中讀取資料的邏輯大概就是這些了。概括一下來說,就是如果能從記憶體緩存當中讀取到要加載的圖檔,那麼就直接進行回調,如果讀取不到的話,才會開啟線程執行後面的圖檔加載邏輯。

現在我們已經搞明白了記憶體緩存讀取的原理,接下來的問題就是記憶體緩存是在哪裡寫入的呢?這裡我們又要回顧一下上一篇文章中的内容了。還記不記得我們之前分析過,當圖檔加載完成之後,會在EngineJob當中通過Handler發送一條消息将執行邏輯切回到主線程當中,進而執行handleResultOnMainThread()方法。那麼我們現在重新來看一下這個方法,代碼如下所示:

class EngineJob implements EngineRunnable.EngineRunnableManager {

    private final EngineResourceFactory engineResourceFactory;
    ...

    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;
        engineResource.acquire();
        listener.onEngineJobComplete(key, engineResource);
        for (ResourceCallback cb : cbs) {
            if (!isInIgnoredCallbacks(cb)) {
                engineResource.acquire();
                cb.onResourceReady(engineResource);
            }
        }
        engineResource.release();
    }

    static class EngineResourceFactory {
        public <R> EngineResource<R> build(Resource<R> resource, boolean isMemoryCacheable) {
            return new EngineResource<R>(resource, isMemoryCacheable);
        }
    }
    ...
}
           

在第13行,這裡通過EngineResourceFactory建構出了一個包含圖檔資源的EngineResource對象,然後會在第16行将這個對象回調到Engine的onEngineJobComplete()方法當中,如下所示:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {
    ...    

    @Override
    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()));
            }
        }
        jobs.remove(key);
    }

    ...
}
           

現在就非常明顯了,可以看到,在第13行,回調過來的EngineResource被put到了activeResources當中,也就是在這裡寫入的緩存。

那麼這隻是弱引用緩存,還有另外一種LruCache緩存是在哪裡寫入的呢?這就要介紹一下EngineResource中的一個引用機制了。觀察剛才的handleResultOnMainThread()方法,在第15行和第19行有調用EngineResource的acquire()方法,在第23行有調用它的release()方法。其實,EngineResource是用一個acquired變量用來記錄圖檔被引用的次數,調用acquire()方法會讓變量加1,調用release()方法會讓變量減1,代碼如下所示:

class EngineResource<Z> implements Resource<Z> {

    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);
        }
    }
}
           

也就是說,當acquired變量大于0的時候,說明圖檔正在使用中,也就應該放到activeResources弱引用緩存當中。而經過release()之後,如果acquired變量等于0了,說明圖檔已經不再被使用了,那麼此時會在第24行調用listener的onResourceReleased()方法來釋放資源,這個listener就是Engine對象,我們來看下它的onResourceReleased()方法:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {

    private final MemoryCache cache;
    private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
    ...    

    @Override
    public void onResourceReleased(Key cacheKey, EngineResource resource) {
        Util.assertMainThread();
        activeResources.remove(cacheKey);
        if (resource.isCacheable()) {
            cache.put(cacheKey, resource);
        } else {
            resourceRecycler.recycle(resource);
        }
    }

    ...
}
           

可以看到,這裡首先會将緩存圖檔從activeResources中移除,然後再将它put到LruResourceCache當中。這樣也就實作了正在使用中的圖檔使用弱引用來進行緩存,不在使用中的圖檔使用LruCache來進行緩存的功能。

這就是Glide記憶體緩存的實作原理。

硬碟緩存

接下來我們開始學習硬碟緩存方面的内容。

不知道你還記不記得,在本系列的第一篇文章中我們就使用過硬碟緩存的功能了。當時為了禁止Glide對圖檔進行硬碟緩存而使用了如下代碼:

Glide.with(this)
     .load(url)
     .diskCacheStrategy(DiskCacheStrategy.NONE)
     .into(imageView);
           

調用diskCacheStrategy()方法并傳入DiskCacheStrategy.NONE,就可以禁用掉Glide的硬碟緩存功能了。

這個diskCacheStrategy()方法基本上就是Glide硬碟緩存功能的一切,它可以接收四種參數:

  • DiskCacheStrategy.NONE: 表示不緩存任何内容。
  • DiskCacheStrategy.SOURCE: 表示隻緩存原始圖檔。
  • DiskCacheStrategy.RESULT: 表示隻緩存轉換過後的圖檔(預設選項)。
  • DiskCacheStrategy.ALL : 表示既緩存原始圖檔,也緩存轉換過後的圖檔。

上面四種參數的解釋本身并沒有什麼難了解的地方,但是有一個概念大家需要了解,就是當我們使用Glide去加載一張圖檔的時候,Glide預設并不會将原始圖檔展示出來,而是會對圖檔進行壓縮和轉換(我們會在後面學習這方面的内容)。總之就是經過種種一系列操作之後得到的圖檔,就叫轉換過後的圖檔。而Glide預設情況下在硬碟緩存的就是轉換過後的圖檔,我們通過調用diskCacheStrategy()方法則可以改變這一預設行為。

好的,關于Glide硬碟緩存的用法也就隻有這麼多,那麼接下來還是老套路,我們通過閱讀源碼來分析一下,Glide的硬碟緩存功能是如何實作的。

首先,和記憶體緩存類似,硬碟緩存的實作也是使用的LruCache算法,而且Google還提供了一個現成的工具類DiskLruCache。我之前也專門寫過一篇文章對這個DiskLruCache工具進行了比較全面的分析,感興趣的朋友可以參考一下 Android DiskLruCache完全解析,硬碟緩存的最佳方案 。當然,Glide是使用的自己編寫的DiskLruCache工具類,但是基本的實作原理都是差不多的。

接下來我們看一下Glide是在哪裡讀取硬碟緩存的。這裡又需要回憶一下上篇文章中的内容了,Glide開啟線程來加載圖檔後會執行EngineRunnable的run()方法,run()方法中又會調用一個decode()方法,那麼我們重新再來看一下這個decode()方法的源碼:

private Resource<?> decode() throws Exception {
    if (isDecodingFromCache()) {
        return decodeFromCache();
    } else {
        return decodeFromSource();
    }
}
           

可以看到,

這裡會分為兩種情況,一種是調用decodeFromCache()方法從硬碟緩存當中讀取圖檔,一種是調用decodeFromSource()來讀取原始圖檔

。預設情況下Glide會優先從緩存當中讀取,隻有緩存中不存在要讀取的圖檔時,才會去讀取原始圖檔。那麼我們現在來看一下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的decodeResultFromCache()方法來擷取緩存,如果擷取不到,會再調用decodeSourceFromCache()方法擷取緩存,這兩個方法的差別其實就是DiskCacheStrategy.RESULT和DiskCacheStrategy.SOURCE這兩個參數的差別,相信不需要我再做什麼解釋吧。

那麼我們來看一下這兩個方法的源碼吧,如下所示:

public Resource<Z> decodeResultFromCache() throws Exception {
    if (!diskCacheStrategy.cacheResult()) {
        return null;
    }
    long startTime = LogTime.getLogTime();
    Resource<T> transformed = loadFromCache(resultKey);
    startTime = LogTime.getLogTime();
    Resource<Z> result = transcode(transformed);
    return result;
}

public Resource<Z> decodeSourceFromCache() throws Exception {
    if (!diskCacheStrategy.cacheSource()) {
        return null;
    }
    long startTime = LogTime.getLogTime();
    Resource<T> decoded = loadFromCache(resultKey.getOriginalKey());
    return transformEncodeAndTranscode(decoded);
}
           

可以看到,它們都是調用了loadFromCache()方法從緩存當中讀取資料,如果是decodeResultFromCache()方法就直接将資料解碼并傳回,如果是decodeSourceFromCache()方法,還要調用一下transformEncodeAndTranscode()方法先将資料轉換一下再解碼并傳回。

然而我們注意到,這兩個方法中在調用loadFromCache()方法時傳入的參數卻不一樣,一個傳入的是resultKey,另外一個卻又調用了resultKey的getOriginalKey()方法。這個其實非常好了解,剛才我們已經解釋過了,

Glide的緩存Key是由10個參數共同組成的,包括圖檔的width、height等等。但如果我們是緩存的原始圖檔,其實并不需要這麼多的參數,因為不用對圖檔做任何的變化

。那麼我們來看一下getOriginalKey()方法的源碼:

public Key getOriginalKey() {
    if (originalKey == null) {
        originalKey = new OriginalKey(id, signature);
    }
    return originalKey;
}
           

可以看到,這裡其實就是忽略了絕大部分的參數,隻使用了id和signature這兩個參數來構成緩存Key。而

signature參數絕大多數情況下都是用不到的,是以基本上可以說就是由id(也就是圖檔url)來決定的Original緩存Key

搞明白了這兩種緩存Key的差別,那麼接下來我們看一下loadFromCache()方法的源碼吧:

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;
}
           

這個方法的邏輯非常簡單,調用getDiskCache()方法擷取到的就是Glide自己編寫的DiskLruCache工具類的執行個體,然後調用它的get()方法并把緩存Key傳入,就能得到硬碟緩存的檔案了。

如果檔案為空就傳回null,如果檔案不為空則将它解碼成Resource對象後傳回即可

這樣我們就将硬碟緩存讀取的源碼分析完了,那麼硬碟緩存又是在哪裡寫入的呢?趁熱打鐵我們趕快繼續分析下去。

剛才已經分析過了,在沒有緩存的情況下,會調用decodeFromSource()方法來讀取原始圖檔。那麼我們來看下這個方法:

public Resource<Z> decodeFromSource() throws Exception {
    Resource<T> decoded = decodeSource();
    return transformEncodeAndTranscode(decoded);
}
           

這個方法中隻有兩行代碼,decodeSource()顧名思義是用來解析原圖檔的,而transformEncodeAndTranscode()則是用來對圖檔進行轉換和轉碼的。我們先來看decodeSource()方法:

private Resource<T> decodeSource() throws Exception {
    Resource<T> decoded = null;
    try {
        long startTime = LogTime.getLogTime();
        final A data = fetcher.loadData(priority);
        if (isCancelled) {
            return null;
        }
        decoded = decodeFromSourceData(data);
    } finally {
        fetcher.cleanup();
    }
    return decoded;
}

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);
    }
    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);
    startTime = LogTime.getLogTime();
    Resource<T> result = loadFromCache(resultKey.getOriginalKey());
    return result;
}
           

這裡會在第5行先調用fetcher的loadData()方法讀取圖檔資料,然後在第9行調用decodeFromSourceData()方法來對圖檔進行解碼。接下來會在第18行先判斷是否允許緩存原始圖檔,如果允許的話又會調用cacheAndDecodeSourceData()方法。而在這個方法中同樣調用了getDiskCache()方法來擷取DiskLruCache執行個體,接着調用它的put()方法就可以寫入硬碟緩存了,注意原始圖檔的緩存Key是用的resultKey.getOriginalKey()。

好的,原始圖檔的緩存寫入就是這麼簡單,接下來我們分析一下transformEncodeAndTranscode()方法的源碼,來看看轉換過後的圖檔緩存是怎麼寫入的。代碼如下所示:

private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
    long startTime = LogTime.getLogTime();
    Resource<T> transformed = transform(decoded);
    writeTransformedToCache(transformed);
    startTime = LogTime.getLogTime();
    Resource<Z> result = transcode(transformed);
    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);
}
           

這裡的邏輯就更加簡單明了了。先是在第3行調用transform()方法來對圖檔進行轉換,然後在writeTransformedToCache()方法中将轉換過後的圖檔寫入到硬碟緩存中,調用的同樣是DiskLruCache執行個體的put()方法,不過這裡用的緩存Key是resultKey。

這樣我們就将Glide硬碟緩存的實作原理也分析完了。雖然這些源碼看上去如此的複雜,但是經過Glide出色的封裝,使得我們隻需要通過skipMemoryCache()和diskCacheStrategy()這兩個方法就可以輕松自如地控制Glide的緩存功能了。

了解了Glide緩存的實作原理之後,接下來我們再來學習一些Glide緩存的進階技巧吧。

進階技巧

雖說Glide将緩存功能高度封裝之後,使得用法變得非常簡單,但同時也帶來了一些問題。

比如之前有一位群裡的朋友就跟我說過,他們項目的圖檔資源都是存放在七牛雲上面的,而七牛雲為了對圖檔資源進行保護,會在圖檔url位址的基礎之上再加上一個token參數。也就是說,一張圖檔的url位址可能會是如下格式:

http://url.com/image.jpg?token=d9caa6e02c990b0a
           

而使用Glide加載這張圖檔的話,也就會使用這個url位址來組成緩存Key。

但是接下來問題就來了,token作為一個驗證身份的參數并不是一成不變的,很有可能時時刻刻都在變化。而如果token變了,那麼圖檔的url也就跟着變了,圖檔url變了,緩存Key也就跟着變了。結果就造成了,明明是同一張圖檔,就因為token不斷在改變,導緻Glide的緩存功能完全失效了。

這其實是個挺棘手的問題,而且我相信絕對不僅僅是七牛雲這一個個例,大家在使用Glide的時候很有可能都會遇到這個問題。

那麼該如何解決這個問題呢?我們還是從源碼的層面進行分析,首先再來看一下Glide生成緩存Key這部分的代碼:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {

    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());

        ...
    }

    ...
}
           

來看一下第11行,剛才已經說過了,這個id其實就是圖檔的url位址。那麼,這裡是通過調用fetcher.getId()方法來擷取的圖檔url位址,而我們在上一篇文章中已經知道了,fetcher就是HttpUrlFetcher的執行個體,我們就來看一下它的getId()方法的源碼吧,如下所示:

public class HttpUrlFetcher implements DataFetcher<InputStream> {

    private final GlideUrl glideUrl;
    ...

    public HttpUrlFetcher(GlideUrl glideUrl) {
        this(glideUrl, DEFAULT_CONNECTION_FACTORY);
    }

    HttpUrlFetcher(GlideUrl glideUrl, HttpUrlConnectionFactory connectionFactory) {
        this.glideUrl = glideUrl;
        this.connectionFactory = connectionFactory;
    }

    @Override
    public String getId() {
        return glideUrl.getCacheKey();
    }

    ...
}
           

可以看到,getId()方法中又調用了GlideUrl的getCacheKey()方法。那麼這個GlideUrl對象是從哪裡來的呢?

其實就是我們在load()方法中傳入的圖檔url位址,然後Glide在内部把這個url位址包裝成了一個GlideUrl對象

很明顯,接下來我們就要看一下GlideUrl的getCacheKey()方法的源碼了,如下所示:

public class GlideUrl {

    private final URL url;
    private final String stringUrl;
    ...

    public GlideUrl(URL url) {
        this(url, Headers.DEFAULT);
    }

    public GlideUrl(String url) {
        this(url, Headers.DEFAULT);
    }

    public GlideUrl(URL url, Headers headers) {
        ...
        this.url = url;
        stringUrl = null;
    }

    public GlideUrl(String url, Headers headers) {
        ...
        this.stringUrl = url;
        this.url = null;
    }

    public String getCacheKey() {
        return stringUrl != null ? stringUrl : url.toString();
    }

    ...
}
           

這裡我将代碼稍微進行了一點簡化,這樣看上去更加簡單明了。GlideUrl類的構造函數接收兩種類型的參數,一種是url字元串,一種是URL對象。然後

getCacheKey()方法中的判斷邏輯非常簡單,如果傳入的是url字元串,那麼就直接傳回這個字元串本身,如果傳入的是URL對象,那麼就傳回這個對象toString()後的結果

其實看到這裡,我相信大家已經猜到解決方案了,因為

getCacheKey()方法中的邏輯太直白了,直接就是将圖檔的url位址進行傳回來作為緩存Key的

。那麼其實我們隻需要重寫這個getCacheKey()方法,加入一些自己的邏輯判斷,就能輕松解決掉剛才的問題了。

建立一個MyGlideUrl繼承自GlideUrl,代碼如下所示:

public class MyGlideUrl extends GlideUrl {

    private String mUrl;

    public MyGlideUrl(String url) {
        super(url);
        mUrl = url;
    }

    @Override
    public String getCacheKey() {
        return mUrl.replace(findTokenParam(), "");
    }

    private String findTokenParam() {
        String tokenParam = "";
        int tokenKeyIndex = mUrl.indexOf("?token=") >= 0 ? mUrl.indexOf("?token=") : mUrl.indexOf("&token=");
        if (tokenKeyIndex != -1) {
            int nextAndIndex = mUrl.indexOf("&", tokenKeyIndex + 1);
            if (nextAndIndex != -1) {
                tokenParam = mUrl.substring(tokenKeyIndex + 1, nextAndIndex + 1);
            } else {
                tokenParam = mUrl.substring(tokenKeyIndex);
            }
        }
        return tokenParam;
    }

}
           

可以看到,這裡我們重寫了getCacheKey()方法,在裡面加入了一段邏輯用于将圖檔url位址中token參數的這一部分移除掉。這樣getCacheKey()方法得到的就是一個沒有token參數的url位址,進而不管token怎麼變化,最終Glide的緩存Key都是固定不變的了。

當然,定義好了MyGlideUrl,我們還得使用它才行,将加載圖檔的代碼改成如下方式即可:

Glide.with(this)
     .load(new MyGlideUrl(url))
     .into(imageView);
           

也就是說,我們需要在load()方法中傳入這個自定義的MyGlideUrl對象,而不能再像之前那樣直接傳入url字元串了。不然的話Glide在内部還是會使用原始的GlideUrl類,而不是我們自定義的MyGlideUrl類。

這樣我們就将這個棘手的緩存問題給解決掉了。

好了,關于Glide緩存方面的内容今天就分析到這裡,現在我們不光掌握了Glide緩存的基本用法和進階技巧,還了解了它背後的實作原理,又是收獲滿滿的一篇文章啊。下一篇文章當中,我會繼續帶着大家深入分析Glide的其他功能子產品,講一講回調方面的知識,感興趣的朋友請繼續閱讀 Glide源碼解析(四)玩轉Glide的回調與監聽 。