1. 程式人生 > >Android圖片載入框架Glide之探究Glide的快取機制

Android圖片載入框架Glide之探究Glide的快取機制

轉載自:http://blog.csdn.net/guolin_blog/article/details/54895665

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

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

今天我們就先從快取這一塊內容開始入手吧。不過今天文章中的原始碼都建在上一篇原始碼分析的基礎之上,還沒有看過上一篇文章的朋友,建議先去閱讀 

Android圖片載入框架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()); ... } ... }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

可以看到,這裡在第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);
  • 1
  • 2
  • 3
  • 4

可以看到,只需要呼叫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;
    }

    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

這裡我們還是隻看關鍵,在第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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

這裡也就是構建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);
    }

    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

可以看到,這裡在第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;
    }

    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

在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()方法。那麼我們現在重新來看一下這個方法