Android圖片加載框架最全解析(三),深刻探究Glide的緩存機制

在本系列的上一篇文章中,我帶着你們一塊兒閱讀了一遍Glide的源碼,初步瞭解了這個強大的圖片加載框架的基本執行流程。文末有免費福利哦面試

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

今天咱們就先從緩存這一塊內容開始入手吧。不過今天文章中的源碼都建在上一篇源碼分析的基礎之上,尚未看過上一篇文章的朋友,建議先去閱讀 Android圖片加載框架最全解析(二),從源碼的角度理解Glide的執行流程 。小程序

Glide緩存簡介 Glide的緩存設計能夠說是很是先進的,考慮的場景也很周全。在緩存這一功能上,Glide又將它分紅了兩個模塊,一個是內存緩存,一個是硬盤緩存。緩存

這兩個緩存模塊的做用各不相同,內存緩存的主要做用是防止應用重複將圖片數據讀取到內存當中,而硬盤緩存的主要做用是防止應用重複從網絡或其餘地方重複下載和讀取數據。性能優化

內存緩存和硬盤緩存的相互結合才構成了Glide極佳的圖片緩存效果,那麼接下來咱們就分別來分析一下這兩種緩存的使用方法以及它們的實現原理。網絡

緩存Key 既然是緩存功能,就必然會有用於進行緩存的Key。那麼Glide的緩存Key是怎麼生成的呢?我不得不說,Glide的緩存Key生成規則很是繁瑣,決定緩存Key的參數居然有10個之多。不過繁瑣歸繁瑣,至少邏輯仍是比較簡單的,咱們先來看一下Glide緩存Key的生成邏輯。文末有免費福利哦架構

生成緩存Key的代碼在Engine類的load()方法當中,這部分代碼咱們在上一篇文章當中已經分析過了,只不過當時忽略了緩存相關的內容,那麼咱們如今從新來看一下:app

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了。ide

可見,決定緩存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](https://my.oschina.net/u/1162528)
    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](https://my.oschina.net/u/1162528)
    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 1 而使用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](https://my.oschina.net/u/1162528)
    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](https://my.oschina.net/u/1162528)
    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的其餘功能模塊,講一講回調方面的知識,感興趣的朋友請繼續閱讀

最後給你們分享一份很是系統和全面的Android進階技術大綱及進階資料,及面試題集

想學習更多Android知識,請加入Android技術開發企鵝交流 7520 16839

進羣與大牛們一塊兒討論,還可獲取Android高級架構資料、源碼、筆記、視頻

包括 高級UI、Gradle、RxJava、小程序、Hybrid、移動架構、React Native、性能優化等全面的Android高級實踐技術講解性能優化架構思惟導圖,和BATJ面試題及答案!

羣裏免費分享給有須要的朋友,但願可以幫助一些在這個行業發展迷茫的,或者想系統深刻提高以及困於瓶頸的朋友,在網上博客論壇等地方少花些時間找資料,把有限的時間,真正花在學習上,因此我在這免費分享一些架構資料及給你們。但願在這些資料中都有你須要的內容。

相關文章
相關標籤/搜索