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

轉載請註明出處:http://blog.csdn.net/guolin_blog/article/details/54895665

本文同步發表於我的微信公衆號,掃一掃文章底部的二維碼或在微信搜索 郭霖 即可關注,每天都有文章更新。

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

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

今天我們就先從緩存這一塊內容開始入手吧。不過今天文章中的源碼都建在上一篇源碼分析的基礎之上,還沒有看過上一篇文章的朋友,建議先去閱讀 Android圖片加載框架最全解析(二),從源碼的角度理解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的其他功能模塊,講一講回調方面的知識,感興趣的朋友請繼續閱讀 Android圖片加載框架最全解析(四),玩轉Glide的回調與監聽

關注我的技術公衆號,每天都有優質技術文章推送。關注我的娛樂公衆號,工作、學習累了的時候放鬆一下自己。

微信掃一掃下方二維碼即可關注:

        

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章