Glide 核心設計二: 快取管理

NEXT發表於2017-02-27

原文連結:Glide核心設計二:快取管理

引言

Glide作為一個優秀的圖片載入框架,快取管理是必不可少的一部分,這篇文章主要通過各個角度、從整體設計到程式碼實現,深入的分析Glide的快取管理模組,力求在同類分析Glide快取的分析文章中脫穎而出。關於Glide的生命週期繫結,可檢視Glide系列文章Glide核心設計一:皮皮蝦,我們走

前提

  1. 本文分析Glide快取管理,將以使用Glide載入網路圖片為例子,如載入本地圖片、Gif資源等使用不是本文的重點。因不管是何種使用方式,快取模組都是一樣的,只抓住網路載入圖片這條主線,邏輯會更清晰。
  2. 本文將先給出Glide快取管理整體設計的結論,然後再分析原始碼。

整體設計

快取型別

Glide的快取型別分為兩大類,一類是Resource快取,一類是Bitmap快取。

Resource快取

為什麼需要快取圖片Resource,很好理解,因為圖片從網路載入,將圖片快取到本地,當需要再次使用時,直接從快取中取出而無需再次請求網路。

三層快取

Glide在快取Resource使用三層快取,包括:

  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可查詢、移除對應的快取。

Glide 核心設計二: 快取管理
快取的key對比

  1. 從對比中可看出,Resource三層快取所使用的key的構造形式是一樣的,包括圖片id(圖片的Url地址),寬高等引數來標識。對於其他引數,舉一個例子理解:圖片資源從網路載入後,經過解碼(decode)、快取到磁碟、從磁碟中取出、變換資源(加圓角等,transformation)、磁碟快取變換後的圖片資源、轉碼(transcode)顯示。
  2. Bitmap的快取Key的構造相對簡單得多,由長、寬的解析度以及圖片壓縮引數即可唯一標示一個回收的Bitmap。當需要使用的bitmap時,在BitmapPool中查詢對應的長、寬和config都一樣的Bitmap並返回,而無需重新建立。

Resource快取流程

Resource包括三層快取,通過流程圖看它們之間的關係:

Glide 核心設計二: 快取管理
Resource載入流程

因為記憶體快取優於磁碟快取,所以當需要使用資源時,先從記憶體快取中查詢(一級快取和二級快取都是記憶體快取,其功能不一樣,一級快取用於在記憶體中快取不是正在使用的資源,二級快取是儲存正在使用的資源),再從磁碟快取中查詢。若都找不到,則從網路載入。

滑動控制元件多圖的效能優化

不論是Resource還是Bitmap快取,若顯示的僅是部分照片,並且不存在頻繁使用的場景,則使用Glide沒有太大的優勢。設計快取的目的就是為了在重複顯示時,更快、更省的顯示圖片資源。Glide有針對ListView、Recyclerview等控制元件載入多圖時進行優化。此處討論最常見的場景:Recyclerview顯示多圖,簡略圖如下。

Glide 核心設計二: 快取管理
Glide在Recyclerview的使用

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

LRU演算法

BitmapPool的LRU演算法流程圖如下:

Glide 核心設計二: 快取管理
BitmapPool LRU流程

類圖

在進行程式碼分析前,先給出跟Glide快取管理相關的類圖(省略類的大部分變數和方法)。

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()釋放記憶體。

相關文章