Glide 核心設計二: 緩存管理

原文連接:Glide核心設計二:緩存管理java

引言

Glide做爲一個優秀的圖片加載框架,緩存管理是必不可少的一部分,這篇文章主要經過各個角度、從總體設計到代碼實現,深刻的分析Glide的緩存管理模塊,力求在同類分析Glide緩存的分析文章中脫穎而出。關於Glide的生命週期綁定,可查看Glide系列文章Glide核心設計一:皮皮蝦,咱們走git

前提

  1. 本文分析Glide緩存管理,將以使用Glide加載網絡圖片爲例子,如加載本地圖片、Gif資源等使用不是本文的重點。因不論是何種使用方式,緩存模塊都是同樣的,只抓住網絡加載圖片這條主線,邏輯會更清晰。
  2. 本文將先給出Glide緩存管理總體設計的結論,而後再分析源碼。

總體設計

緩存類型

Glide的緩存類型分爲兩大類,一類是Resource緩存,一類是Bitmap緩存。github

Resource緩存

爲何須要緩存圖片Resource,很好理解,由於圖片從網絡加載,將圖片緩存到本地,當須要再次使用時,直接從緩存中取出而無需再次請求網絡。算法

三層緩存

Glide在緩存Resource使用三層緩存,包括:canvas

  1. 一級緩存:緩存被回收的資源,使用LRU算法(Least Frequently Used,最近最少使用算法)。當須要再次使用到被回收的資源,直接從內存返回。
  2. 二級緩存:使用弱引用緩存正在使用的資源。當系統執行gc操做時,會回收沒有強引用的資源。使用弱引用緩存資源,既能夠緩存正在使用的強引用資源,也不阻礙系統須要回收無引用資源。
  3. 三級緩存:磁盤緩存。網絡圖片下載成功後將以文件的形式緩存到磁盤中。

Bitmap緩存

Bitmap所佔內存大小

Bitmap所佔的內存大小由三部分組成:圖片的寬度分辨率、高度分辨率和Bitmap質量參數。公式是:Bitmap內存大小 = (寬pix長pix)質量參數所佔的位數。單位是字節B。設計模式

Bitmap壓縮質量參數

質量參數決定每個像素點用多少位(bit)來顯示:緩存

  1. ALPHA_8就是Alpha由8位組成(1B)
  2. ARGB_4444就是由4個4位組成即16位(2B)
  3. ARGB_8888就是由4個8位組成即32位(4B)
  4. RGB_565就是R爲5位,G爲6位,B爲5位共16位(2B)

Glide默認使用RGB_565,比系統默認使用的ARGB_8888節省一半的資源,但RGB_565沒法顯示透明度。
舉個例子:在手機上顯示100pix*200pix的圖片,解壓前15KB,是使用Glide加載(默認RGB_565)Bitmap所佔用的內存是:(100x200)x2B = 40000B≈40Kb,比以文件的造成存儲的增長很多,由於png、jpg等格式的圖片通過壓縮。正由於Bitmap比較消耗內存,例如使用Recyclerview等滑動控件顯示大量圖片時,將大量的建立和回收Bitmap,致使內存波動影響性能。性能優化

Bitmap緩存算法

在Glide中,使用BitmapPool來緩存Bitmap,使用的也是LRU算法。當須要使用Bitmap時,從Bitmap的池子中取出合適的Bitmap,若取不到合適的,則再新建立。當Bitmap使用完後,不直接調用Bitmap.recycler()回收,而是放入Bitmap的池子。網絡

緩存的Key類型

Glide的緩存使用 的形式緩存,Resource和Bitmap都是做爲Value的部分,將value存儲時,必需要有一個Key標識緩存的內容,根據該Key可查找、移除對應的緩存。
app

緩存的key對比

  1. 從對比中可看出,Resource三層緩存所使用的key的構造形式是同樣的,包括圖片id(圖片的Url地址),寬高等參數來標識。對於其餘參數,舉一個例子理解:圖片資源從網絡加載後,通過解碼(decode)、緩存到磁盤、從磁盤中取出、變換資源(加圓角等,transformation)、磁盤緩存變換後的圖片資源、轉碼(transcode)顯示。
  2. Bitmap的緩存Key的構造相對簡單得多,由長、寬的分辨率以及圖片壓縮參數便可惟一標示一個回收的Bitmap。當須要使用的bitmap時,在BitmapPool中查找對應的長、寬和config都同樣的Bitmap並返回,而無需從新建立。

Resource緩存流程

Resource包括三層緩存,經過流程圖看它們之間的關係:

Resource加載流程

由於內存緩存優於磁盤緩存,因此當須要使用資源時,先從內存緩存中查找(一級緩存和二級緩存都是內存緩存,其功能不同,一級緩存用於在內存中緩存不是正在使用的資源,二級緩存是保存正在使用的資源),再從磁盤緩存中查找。若都找不到,則從網絡加載。

滑動控件多圖的性能優化

不管是Resource仍是Bitmap緩存,若顯示的僅是部分照片,而且不存在頻繁使用的場景,則使用Glide沒有太大的優點。設計緩存的目的就是爲了在重複顯示時,更快、更省的顯示圖片資源。Glide有針對ListView、Recyclerview等控件加載多圖時進行優化。此處討論最多見的場景:Recyclerview顯示多圖,簡略圖以下。

Glide在Recyclerview的使用

如上圖所示,當圖5劃入界面時,會複用圖一的Item,設置新的圖片以前,會先清空原有圖片的資源,清空時會把Resource資源放入一級緩存待未來複用,同時會將回收的Bitmap放入BitmapPool中;當圖5向下隱藏,圖一出現時,圖5的資源會放到一級緩存中,圖一的資源則從一級緩存中取出,無須從新網絡請求,同時所須要的Bitmap也無須從新建立,直接複用。

LRU算法

BitmapPool的LRU算法流程圖以下:

BitmapPool LRU流程

類圖

在進行代碼分析前,先給出跟Glide緩存管理相關的類圖(省略類的大部分變量和方法)。

Glide緩存管理類圖

Glide緩存管理類圖大圖地址

代碼實現

根據以上的Glide緩存管理的結論及類圖,可自主跟源碼,跳過如下內容。

Glide.with(Context).load(String).into(ImageView)

Glide.with(Context)

返回RequestManager,主要實現和Fragment、Activity生命週期的綁定,詳情請看Glide核心設計一:皮皮蝦,咱們走

.load(String)

RequestManager的load(String)方法返回DrawableTypeRequest,根據圖片地址返回一個用於建立圖片請求的Request的Builder,代碼以下:

public DrawableTypeRequest<String> load(String string) {
            return (DrawableTypeRequest<String>) fromString().load(string); //調用fromString()和load()方法
        }複製代碼

fromString()方法調用loadGeneric()方法,代碼以下:

public DrawableTypeRequest<String> fromString() {
        return loadGeneric(String.class); 
    }

  private <T> DrawableTypeRequest<T> loadGeneric(Class<T> modelClass) {
         ModelLoader<T, InputStream> streamModelLoader = Glide.buildStreamModelLoader(modelClass, context);
         ModelLoader<T, ParcelFileDescriptor> fileDescriptorModelLoader =
                 Glide.buildFileDescriptorModelLoader(modelClass, context);
         if (modelClass != null && streamModelLoader == null && fileDescriptorModelLoader == null) {
             throw new IllegalArgumentException("Unknown type " + modelClass + ". You must provide a Model of a type for"
                     + " which there is a registered ModelLoader, if you are using a custom model, you must first call"
                     + " Glide#register with a ModelLoaderFactory for your custom model class");
         }

         return optionsApplier.apply(  //傳遞的參數中建立了一個DrawableTypeRequest並返回該對象
                 new DrawableTypeRequest<T>(modelClass, streamModelLoader, fileDescriptorModelLoader, context,
                         glide, requestTracker, lifecycle, optionsApplier));  
     }複製代碼

DrawableTypeRequest的load()方法以下:

@Override
    public DrawableRequestBuilder<ModelType> load(ModelType model) {
        super.load(model);
        return this;
    }複製代碼

DrawableTypeRequest父類是DrawableRequestBuilder,父類的父類是GenericRequestBuilder,調用super.load()方法以下:

public GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> load(ModelType model) {
        this.model = model;
        isModelSet = true;
        return this;
    }複製代碼

以上代碼可知,緩存管理的主要實現代碼並不在.load(Sting)代碼,接下來繼續分析.into(ImageView)代碼。

.into(ImageView)

GenericRequestBuilder的into(ImageView)代碼以下:

public Target<TranscodeType> into(ImageView view) {
        Util.assertMainThread();
        if (view == null) { 
            throw new IllegalArgumentException("You must pass in a non null View");
        }

        if (!isTransformationSet && view.getScaleType() != null) {
            switch (view.getScaleType()) {  //根據圖片的scaleType作相應處理
                case CENTER_CROP:
                    applyCenterCrop();
                    break;
                case FIT_CENTER:
                case FIT_START:
                case FIT_END:
                    applyFitCenter();
                    break;
                //$CASES-OMITTED$
                default:
                    // Do nothing.
            }
        }
        //調用buildImageViewTarget()方法建立了一個Target類型的對象
        return into(glide.buildImageViewTarget(view, transcodeClass));  
    }複製代碼

以上代碼主要有兩個功能:

  1. 根據ScaleType進行圖片的變換
  2. 將ImageView轉換成一個Target

繼續查看into(Target)的代碼:

public <Y extends Target<TranscodeType>> Y into(Y target) {
        Util.assertMainThread();
        if (target == null) {
            throw new IllegalArgumentException("You must pass in a non null Target");
        }
        if (!isModelSet) {
            throw new IllegalArgumentException("You must first set a model (try #load())");
        }

        Request previous = target.getRequest();  //獲取請求體Request

        if (previous != null) { //若ImageView是複用過的,則previous不爲空
            previous.clear(); //調用clear()方法清空ImageView上的圖片資源,此方法會將回收的Resource放入內存緩存中,並不在內存中清空該資源。
            requestTracker.removeRequest(previous); //移除老的請求
            previous.recycle(); //回收Request使用
        }

        Request request = buildRequest(target); //獲取新的Request
        target.setRequest(request); //將新的request設置到target中
        lifecycle.addListener(target); //添加生命週期的監聽
        requestTracker.runRequest(request); //啓動Request

        return target;
    }複製代碼

以上代碼,主要將圖片加載的Request綁定到Target中,若原有Target具備舊的Request,得先處理舊的Request,再綁定上新的Request。target.setRequest()和target.getRequest()最終會調用ViewTarget的setRequest()方法和getRequest()方法,代碼以下:

public void setRequest(Request request) {
        setTag(request);
    }
    private void setTag(Object tag) {
                 if (tagId == null) {
                     isTagUsedAtLeastOnce = true;
                     view.setTag(tag);//調用view的setTag方法,將Request和view作綁定
                 } else {
                     view.setTag(tagId, tag);//調用view的setTag方法,將Request和view作綁定
                 }
    }
    public Request getRequest() {
                    Object tag = getTag(); //獲取view 的tag
                    Request request = null;
                    if (tag != null) {
                        if (tag instanceof Request) {  //若該tag是Request的一個實例
                            request = (Request) tag; 
                        } else {  //用戶不能給view設置tag,由於該view的tag要用於保存Glide的Request對象,不然拋出異常
                            throw new IllegalArgumentException("You must not call setTag() on a view Glide is targeting");
                        }
                    }
                    return request;
            }複製代碼

以上代碼可知,Request經過setTag的方式和View進行綁定,當View是複用時,則Request不爲空,經過Request可對原來的資源進行緩存與回收。此處經過View的setTag()方法綁定Request,可謂妙用。

以上代碼建立了一個Request,requestTracker.runRequest(request);啓動了Request,調用Request的begin()方法,該Request實例是GenericRequest,begin()代碼以下:

@Override
    public void begin() {
        startTime = LogTime.getLogTime();
        if (model == null) {
            onException(null);
            return;
        }

        status = Status.WAITING_FOR_SIZE; //設置等待圖片size的寬高狀態
        if (Util.isValidDimensions(overrideWidth, overrideHeight)) { //必需要肯定圖片的寬高,肯定了則調用onSizeReady
            onSizeReady(overrideWidth, overrideHeight);
        } else { //設置回調,監聽界面的繪製,當檢測到寬高有效時,回調onSizeReady方法
            target.getSize(this);
        }

        if (!isComplete() && !isFailed() && canNotifyStatusChanged()) {
            target.onLoadStarted(getPlaceholderDrawable());
        }
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logV("finished run method in " + LogTime.getElapsedMillis(startTime));
        }
    }複製代碼

加載圖片前,必需要肯定圖片的寬高,由於須要根據肯定的寬高來獲取資源。onSizeReady代碼以下:

@Override
    public void onSizeReady(int width, int height) {
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime));
        }
        if (status != Status.WAITING_FOR_SIZE) {//寬高沒準備好,返回
            return;
        }
        status = Status.RUNNING;  //狀態改成加載運行中

        width = Math.round(sizeMultiplier * width);
        height = Math.round(sizeMultiplier * height);

        ModelLoader<A, T> modelLoader = loadProvider.getModelLoader();
        final DataFetcher<T> dataFetcher = modelLoader.getResourceFetcher(model, width, height);

        if (dataFetcher == null) {
            onException(new Exception("Failed to load model: \'" + model + "\'"));
            return;
        }
        ResourceTranscoder<Z, R> transcoder = loadProvider.getTranscoder();
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logV("finished setup for calling load in " + LogTime.getElapsedMillis(startTime));
        }
        loadedFromMemoryCache = true;
        //真正的加載任務交給engine
        loadStatus = engine.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder,
                priority, isMemoryCacheable, diskCacheStrategy, this);
        loadedFromMemoryCache = resource != null;
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime));
        }
    }複製代碼

以上代碼可知,在肯定寬高後,將圖片加載的任務交給類型爲Engine的對象engine,並調用其load方法,代碼以下:

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(); //該id爲圖片的網絡地址
        //緩存key的組成部分,使用工廠模式
        EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
                loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
                transcoder, loadProvider.getSourceEncoder());
        //使用一級緩存,從回收的內存緩存中查找EngineResource
        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);//建立EngineJob
        DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
                transcoder, diskCacheProvider, diskCacheStrategy, priority); //建立DecodeJob
        EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority); 
        jobs.put(key, engineJob);
        engineJob.addCallback(cb);
        engineJob.start(runnable); //啓動EngineRunnable runnable,使用線程池FifoPriorityThreadPoolExecutor管理

        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Started new load", startTime, key);
        }
        return new LoadStatus(cb, engineJob);
    }複製代碼

分析至此,咱們終於看到實現一級緩存和二級緩存的相關代碼,能夠猜想三級緩存的實現跟EngineRunnable有關。engineJob.start(runnable)會啓動EngineRunnable的start()方法。代碼以下:

@Override
    public void run() {
        if (isCancelled) {
            return;
        }

        Exception exception = null;
        Resource<?> resource = null;
        try {
            resource = decode();  //調用decode()方法
        } catch (Exception e) {
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                Log.v(TAG, "Exception decoding", e);
            }
            exception = e;
        }

        if (isCancelled) { //請求被取消
            if (resource != null) {
                resource.recycle();
            }
            return;
        }

        if (resource == null) { //加載失敗
            onLoadFailed(exception);
        } else { //加載成功
            onLoadComplete(resource);
        }
    }複製代碼

查看decode()方法以下:

private Resource<?> decode() throws Exception {
        if (isDecodingFromCache()) {
            return decodeFromCache();  //從磁盤緩存中獲取
        } else {
            return decodeFromSource(); //從網絡中獲取資源
        }
    }複製代碼

至此,咱們看到磁盤緩存和網絡請求獲取圖片資源的代碼。查看onLoadFailed()的代碼邏輯可知,默認先從磁盤獲取,失敗則從網絡獲取。

BitmapPool緩存邏輯

以上就是Resource三層緩存的代碼,接下來看BitmapPool的緩存實現代碼。
在decodeFromSource()的代碼中,會返回一個類型爲BitmapResource的對象。在RecyclerView的例子中,當ImageView被複用時,會在Tag中取出Request,調用request.clear()代碼。該方法最終會調用BitmapResource的recycler()方法,代碼以下:

public void recycle() {
        if (!bitmapPool.put(bitmap)) {
            bitmap.recycle();
        }
    }複製代碼

該代碼調用bitmapPool.put(bitmap),bitmapPool的實例是LruBitmapPool代碼以下:

public synchronized boolean put(Bitmap bitmap) {
         if (bitmap == null) {
             throw new NullPointerException("Bitmap must not be null");
         }
         if (!bitmap.isMutable() || strategy.getSize(bitmap) > maxSize || !allowedConfigs.contains(bitmap.getConfig())) {
             if (Log.isLoggable(TAG, Log.VERBOSE)) {
                 Log.v(TAG, "Reject bitmap from pool"
                         + ", bitmap: " + strategy.logBitmap(bitmap)
                         + ", is mutable: " + bitmap.isMutable()
                         + ", is allowed config: " + allowedConfigs.contains(bitmap.getConfig()));
             }
             return false;
         }

         final int size = strategy.getSize(bitmap);
         strategy.put(bitmap);//該strategy的實例是Lru算法
         tracker.add(bitmap); //log跟蹤

         puts++; //緩存的bitmap數量標記加一
         currentSize += size;//緩存bitmap的總大小

         if (Log.isLoggable(TAG, Log.VERBOSE)) {
             Log.v(TAG, "Put bitmap in pool=" + strategy.logBitmap(bitmap));
         }
         dump(); //僅用於Log

         evict();  //判斷是否超出指定的內存大小,若超出則移除
         return true;
     }複製代碼

能夠看出,正常狀況下調用put方法返回true,證實緩存該Bitmap成功,緩存成功則不調用bitmap.recycler()方法。當須要使用Bitmap時,先從Bitmap中查找是否有符合條件的Bitmap。在RecyclerView中使用Glide的例子中,將大量複用寬高及Bitmap.Config都相等的Bitmap,極大的優化系統內存性能,減小頻繁的建立回收Bitmap。

小結

Glide的緩存管理至此就分析完了,主要抓住Resource和Bitmap的緩存來說解。在代碼的閱讀中還發現了工廠、裝飾者等設計模式。Glide的解耦給開發者提供很大的便利性,可根據自身需求設置緩存參數,例如默認Bitmap.Config、BitmapPool緩存大小等。最後,針對Glide的緩存設計,提出幾點小建議:

  1. Glide雖然默認使用的Bitmap.Config是RGB_565,但在進行transform(例如圓角顯示圖片)時每每默認是ARGB_8888,由於RGB_565沒有透明色,此時可重寫圓角變換的代碼,繼續使用RGB_565,同時給canvas設置背景色。
  2. BitmapPool緩存的Bitmap大小跟Bitmap的分辨率也有關係,在加載圖片的過程當中,可調用.override(width, height)指定圖片的寬高,再調整ImageView控件的大小適應佈局。
  3. Resource的一級緩存和Bitmap都是內存緩存,雖然極大的提高了複用,但也會致使部份內存在系統執行GC時沒法釋放。若內存達到手機性能瓶頸,應在合適的時機調用Glide.get(this).clearMemory()釋放內存。
相關文章
相關標籤/搜索