Glide原始碼分析

BlackFlagBin發表於2018-05-14

Glide原始碼分析

在早期的Android開發中,圖片載入其實一直是個比較麻煩的問題。我們在處理圖片時會遇到各種各樣的問題:記憶體溢位、列表中圖片錯位等等。但到了如今,這些問題基本上是不會再遇到了。由於很多的優秀的圖片載入框架幫我們處理了圖片相關問題的痛點,所以現在Android中關於圖片載入的部分變得非常簡單。Android中最著名的圖片載入框架就是Glide了,我們今天來深入研究一下Glide的原始碼。

使用方法

以Glide3.8.0版本來分析,我們先看下最常見使用方法:

Glide.with(fragment)
    .load(myUrl)
    .into(imageView);
複製程式碼

上面的程式碼是我們非常熟悉的Glide的基本用法,分為3個步驟:

  • with(context)
  • load(url)
  • into(target) 在瞭解到Glide的3個入口方法之後,我會按照這3個方法來進行原始碼的分析

with(context)方法

看一下with(context)d的原始碼:

    public static RequestManager with(Context context) {
        RequestManagerRetriever retriever = RequestManagerRetriever.get();
        return retriever.get(context);
    }

    public static RequestManager with(Activity activity) {
        RequestManagerRetriever retriever = RequestManagerRetriever.get();
        return retriever.get(activity);
    }
    
    public static RequestManager with(FragmentActivity activity) {
        RequestManagerRetriever retriever = RequestManagerRetriever.get();
        return retriever.get(activity);
    }
    
    public static RequestManager with(android.app.Fragment fragment) {
        RequestManagerRetriever retriever = RequestManagerRetriever.get();
        return retriever.get(fragment);
    }
    
    public static RequestManager with(Fragment fragment) {
        RequestManagerRetriever retriever = RequestManagerRetriever.get();
        return retriever.get(fragment);
    }
複製程式碼

可以看到,with方法有很多,但內容基本一致,都是通過 RequestManagerRetriever.get(); 獲取一個 RequestManagerRetriever 物件 retriever ,然後通過 retriever.get(context); 獲取一個 RequestManager 物件並返回。這些with方法關鍵的不同在於傳入的引數不一致,可以是Context、Activity、Fragment等等。那麼為什麼要分這麼多種呢?其實我們應該都知道:Glide在載入圖片的時候會繫結 with(context) 方法中傳入的 context 的生命週期,如果傳入的是 Activity ,那麼在這個 Activity 銷燬的時候Glide會停止圖片的載入。這樣做的好處是顯而易見的:避免了消耗多餘的資源,也避免了在Activity銷燬之後載入圖片從而導致的空指標問題。

為了更好的分析 with(context) 中的這兩步,我們來看一下 RequestManagerRetriever

public class RequestManagerRetriever implements Handler.Callback {
    //餓漢式建立單例
    private static final RequestManagerRetriever INSTANCE = new RequestManagerRetriever();
    
    //返回單例物件
    public static RequestManagerRetriever get() {
        return INSTANCE;
    }

    //根據傳入的引數,獲取不同的RequestManager
    public RequestManager get(Context context) {
        if (context == null) {
            throw new IllegalArgumentException("You cannot start a load on a null Context");
        } else if (Util.isOnMainThread() && !(context instanceof Application)) {
            if (context instanceof FragmentActivity) {
                return get((FragmentActivity) context);
            } else if (context instanceof Activity) {
                return get((Activity) context);
            } else if (context instanceof ContextWrapper) {
                return get(((ContextWrapper) context).getBaseContext());
            }
        }

        return getApplicationManager(context);
    }
    
    //省略無關程式碼......
}
複製程式碼

很明顯,這是個餓漢式的單例模式。關鍵在於 retriever.get(context),我們繼續看程式碼:

//根據傳入的引數,獲取不同的RequestManager
    public RequestManager get(Context context) {
        //context為null則丟擲異常
        if (context == null) {
            throw new IllegalArgumentException("You cannot start a load on a null Context");
        } else if (Util.isOnMainThread() && !(context instanceof Application)) {
            //當前執行緒是主執行緒並且此context並不是Application的例項,根據context的型別做不同的處理
            if (context instanceof FragmentActivity) {
                return get((FragmentActivity) context);
            } else if (context instanceof Activity) {
                return get((Activity) context);
            } else if (context instanceof ContextWrapper) {
                return get(((ContextWrapper) context).getBaseContext());
            }
        }

        //如果以上條件都不滿足
        return getApplicationManager(context);
    }
複製程式碼

上面這個方法主要是通過傳入context的不同型別來做不同的操作。context可以是Application、FragmentActivity、Activity或者是ContextWrapper。我們先看一下當context是Application時的操作:

private RequestManager getApplicationManager(Context context) {
        // 返回一個單例
        if (applicationManager == null) {
            synchronized (this) {
                if (applicationManager == null) {
                    applicationManager = new RequestManager(context.getApplicationContext(),
                            new ApplicationLifecycle(), new EmptyRequestManagerTreeNode());
                }
            }
        }

        return applicationManager;
    }
複製程式碼

程式碼應該都能看懂, getApplicationManager(Context context) 通過單例模式建立並返回了 applicationManager 。我們再來看一下如果傳入的context是Activity時的操作:

public RequestManager get(Activity activity) {
        //如果不在主執行緒或者Android SDK的版本低於HONEYCOMB,傳入的還是Application型別的context
        if (Util.isOnBackgroundThread() || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
            return get(activity.getApplicationContext());
        } else {
            //判斷當前activity是否被銷燬
            assertNotDestroyed(activity);
            android.app.FragmentManager fm = activity.getFragmentManager();
           //通過fragmentGet(activity, fm)獲取RequestManager
            return fragmentGet(activity, fm);
        }
    }
複製程式碼

程式碼邏輯很簡單:如果不在主執行緒或者Android SDK版本過低,走的還是傳入Application的方法,這個方法在上面提到過;反之,首先判斷當前activity是否被銷燬,如果沒有被銷燬,則通過fragmentGet(activity, fm)獲取RequestManager。關鍵是這個 fragmentGet(activity, fm) ,我們來看一下:

RequestManager fragmentGet(Context context, android.app.FragmentManager fm) {
        //在當前activity中建立一個沒有介面的的fragment並add到當前activity中
        RequestManagerFragment current = getRequestManagerFragment(fm);
        RequestManager requestManager = current.getRequestManager();
        if (requestManager == null) {
            //建立一個requestManager
            requestManager = new RequestManager(context, current.getLifecycle(), current.getRequestManagerTreeNode());
            //將requestManager與fragment繫結        
            current.setRequestManager(requestManager);
        }
        return requestManager;
    }
複製程式碼

fragmentGet 這個方法主要是在當前activity中建立一個沒有介面的的fragment並add到當前activity中,以此來實現對activity生命週期的監聽。到此, with 方法已經基本介紹完畢了,做一下總結:

  • 通過RequestManagerRetriever的get獲取RequestManagerRetriever單例物件
  • 通過retriever.get(context)獲取RequestManager,在get(context)方法中通過對context型別的判斷做不同的處理:
    • context是Application,通過getApplicationManager(Context context) 建立並返回一個RequestManager物件
    • context是Activity,通過fragmentGet(activity, fm)在當前activity建立並新增一個沒有介面的fragment,從而實現圖片載入與activity的生命週期相繫結,之後建立並返回一個RequestManager物件

load(url)方法

with(context)返回一個RequestManager,接下來我們看一下RequestManger中的load(url)方法:

public DrawableTypeRequest<String> load(String string) {
        return (DrawableTypeRequest<String>) fromString().load(string);
    }
複製程式碼

這個方法分兩步:fromString()、load(string),先看第一個方法:

public DrawableTypeRequest<String> fromString() {
        return loadGeneric(String.class);
    }
複製程式碼

這個方法返回的是 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");
        }

        //這句是核心,本質是建立並返回了一個DrawableTypeRequest
        return optionsApplier.apply(
                new DrawableTypeRequest<T>(modelClass, streamModelLoader, fileDescriptorModelLoader, context,
                        glide, requestTracker, lifecycle, optionsApplier));
    }
複製程式碼

loadGeneric(Class modelClass) 方法中,我們只需要關注核心即可。它的核心是最後一句,方法呼叫看著很複雜,其實本質是建立並返回了一個DrawableTypeRequest,Drawable型別的請求。再來看 load(string) 方法:

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

需要注意的是這個方法存在於DrawableTypeRequest的父類DrawableRequestBuilder中,這個方法首先呼叫DrawableRequestBuilder的父類的load方法,然後返回自身。再看一下DrawableRequestBuilder父類中的load方法:

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

DrawableRequestBuilder的父類是GenericRequestBuilder,從名字中我們也可以看出來,前者是Drawable請求的構建者,後者是通用的請求構建者,他們是子父關係。這個load方法其實是把我們傳入的String型別的URL存入了內部的model成員變數中,再將資料來源是否已經設定的標誌位 isModelSet 設定為true,意味著我們在呼叫 Glide.with(context).load(url) 之後資料來源已經設定成功了。

說到這裡,其實Glide中的load(url)基本已經結束了,小夥伴們可能會有問題要問:我平時使用Glide會加一些配置,比如:

Glide.with(context)
    .load(url)
    .placeholder(R.drawable.place_image)
    .error(R.drawable.error_image)
    .into(imageView);
複製程式碼

其實大家在寫的時候是會有一種感覺的,這種寫法很像Builder模式。沒錯,這就是一個Builder模式。經過上面的分析我們知道,在 Glide.with(context).load(url) 之後會返回一個DrawableTypeRequest的物件,它的父類是DrawableRequestBuilder,DrawableRequestBuilder的父類是GenericRequestBuilder,我們寫的placeHolder()、error()等等相關圖片請求配置的方法都定義在GenericRequestBuilder中,我們來簡單的看一下:

    public GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> placeholder(
            int resourceId) {
        this.placeholderId = resourceId;

        return this;
    }
    
    public GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> error(
            int resourceId) {
        this.errorId = resourceId;

        return this;
    }
複製程式碼

是不是一下子就明白了,我們平時對圖片請求的配置使用的就是Builder模式。

into(imageView)方法

簡單的說,Glide中的前兩步是建立了一個Request,這個Request可以理解為對圖片載入的配置請求,需要注意的是僅僅是建立了一個 請求 ,而並沒有去執行。在Glide的最後一步into方法中,這個請求才會真實的執行。

我們來DrawableTypeRequest中找一下into方法,發現沒找到,那肯定是在他的父類DrawableRequestBuilder中,我們來看一下DrawableRequestBuilder中的into方法:

public Target<GlideDrawable> into(ImageView view) {
        return super.into(view);
    }
複製程式碼

嗯,它呼叫的是父類GenericRequestBuilder的方法,那我們繼續看GenericRequestBuilder的into方法:

public Target<TranscodeType> into(ImageView view) {
        //確保在主執行緒
        Util.assertMainThread();
        //確保view不為空
        if (view == null) {
            throw new IllegalArgumentException("You must pass in a non null View");
        }
        //對ScaleType進行配置
        if (!isTransformationSet && view.getScaleType() != null) {
            switch (view.getScaleType()) {
                case CENTER_CROP:
                    applyCenterCrop();
                    break;
                case FIT_CENTER:
                case FIT_START:
                case FIT_END:
                    applyFitCenter();
                    break;
                //$CASES-OMITTED$
                default:
                    // Do nothing.
            }
        }

        //核心
        return into(glide.buildImageViewTarget(view, transcodeClass));
    }
複製程式碼

可以看到,上面的方法就是into的核心程式碼了,它定義在GenericRequestBuilder這個通用的請求構建者中。方法的核心是最後一行: into(glide.buildImageViewTarget(view, transcodeClass)) ,首先是通過 glide.buildImageViewTarget(view, transcodeClass) 建立出一個 Target 型別的物件,然後把這個target傳入GenericRequestBuilder中的into方法中。我們先來看一下Glide中的 buildImageViewTarget(view, transcodeClass) 方法:

    <R> Target<R> buildImageViewTarget(ImageView imageView, Class<R> transcodedClass) {
        return imageViewTargetFactory.buildTarget(imageView, transcodedClass);
    }
複製程式碼

這個方法的目的是把我們傳入的imageView包裝成一個Target。內部呼叫了 imageViewTargetFactory.buildTarget(imageView, transcodedClass) 繼續跟進去看一下:

public <Z> Target<Z> buildTarget(ImageView view, Class<Z> clazz) {
        //圖片來源是GlideDrawable
        if (GlideDrawable.class.isAssignableFrom(clazz)) {
            //建立GlideDrawable對應的target
            return (Target<Z>) new GlideDrawableImageViewTarget(view);
        } else if (Bitmap.class.equals(clazz)) {
            //如果圖片來源是Bitmap,建立Bitmap對應的target
            return (Target<Z>) new BitmapImageViewTarget(view);
        } else if (Drawable.class.isAssignableFrom(clazz)) {
            //如果圖片來源是Drawable,建立Drawable對應的target
            return (Target<Z>) new DrawableImageViewTarget(view);
        } else {
            throw new IllegalArgumentException("Unhandled class: " + clazz
                    + ", try .as*(Class).transcode(ResourceTranscoder)");
        }
    }
複製程式碼

這個方法的的本質是:通過對圖片來源型別的判斷,建立並返回與圖片來源對應的imageViewTarget。獲取到相應的target之後,我們來看GenericRequestBuilder中的into方法:

public <Y extends Target<TranscodeType>> Y into(Y target) {
        //確保在主執行緒
        Util.assertMainThread();
        //確保target不為空
        if (target == null) {
            throw new IllegalArgumentException("You must pass in a non null Target");
        }
        //確保資料來源已經確定,即已經呼叫了load(url)方法
        if (!isModelSet) {
            throw new IllegalArgumentException("You must first set a model (try #load())");
        }

        //獲取當前target已經繫結的Request物件
        Request previous = target.getRequest();

        //如果當前target已經繫結了Request物件,則清空這個Request物件
        if (previous != null) {
            previous.clear();
            //停止繫結到當前target的上一個Request的圖片請求處理
            requestTracker.removeRequest(previous);
            previous.recycle();
        }
        
        //建立Request物件
        Request request = buildRequest(target);
        //與target繫結
        target.setRequest(request);
        lifecycle.addListener(target);
        //執行request
        requestTracker.runRequest(request);

        return target;
    }
複製程式碼

我們梳理一下方法中的邏輯:

  • 獲取當前target中的Request物件,如果存在,則清空並終止這個Request物件的執行
  • 建立新的Request物件並與當前target繫結
  • 執行新建立的圖片處理請求Request 邏輯還是比較清晰的,這裡有一個問題需要說明一下。為什麼要終止並清除target之前繫結的請求呢?

在沒有Glide之前,我們處理ListView中的圖片載入其實是一件比較麻煩的事情。由於ListView中Item的複用機制,會導致網路圖片載入的錯位或者閃爍。那我們解決這個問題的辦法也很簡單,就是給當前的ImageView設定tag,這個tag可以是圖片的URL等等。當從網路中獲取到圖片時判斷這個ImageVIew中的tag是否是這個圖片的URL,如果是就載入圖片,如果不是則跳過。

在有了Glide之後,我們處理ListView或者Recyclerview中的圖片載入就很無腦了,根本不需要作任何多餘的操作,直接正常使用就行了。這其中的原理是Glide給我們處理了這些判斷,我們來看一下Glide內部是如何處理的:

    public Request getRequest() {
        //本質還是getTag
        Object tag = getTag();
        Request request = null;
        if (tag != null) {
            if (tag instanceof Request) {
                request = (Request) tag;
            } else {
                throw new IllegalArgumentException("You must not call setTag() on a view Glide is targeting");
            }
        }
        return request;
    }
    
    @Override
    public void setRequest(Request request) {
        //本質是setTag
        setTag(request);
    }
複製程式碼

可以看到, target.getRequest()target.setRequest(Request request) 本質上還是通過setTag和getTag來做的處理,這也印證了我們上面所說。

繼續回到into方法中,在建立並繫結了Request後,關鍵的就是 requestTracker.runRequest(request) 來執行我們建立的請求了。

public void runRequest(Request request) {
        //將請求加入請求集合
        requests.add(request);
        
        if (!isPaused) {
            如果處於非暫停狀態,開始執行請求
            request.begin();
        } else {
            //如果處於暫停狀態,將請求新增到等待集合
            pendingRequests.add(request);
        }
    }
複製程式碼

這個方法定義在 RequestTracker 中,這個類主要負責Request的執行,暫停,取消等等關於圖片請求的操作。我們著重看 request.begin() ,這句程式碼意味著開始執行圖片請求的處理。Request是個介面, request.begin() 實際呼叫的是Request的子類 GenericRequest 的begin方法,我們跟進去看一下:

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

        status = Status.WAITING_FOR_SIZE;
        if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
            //如果長寬尺寸已經確定
            onSizeReady(overrideWidth, overrideHeight);
        } else {
            //獲取長寬尺寸,獲取完之後會呼叫onSizeReady(overrideWidth, overrideHeight)
            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(overrideWidth, overrideHeight) 流程;如果未確定,先獲取長寬,再走 onSizeReady(overrideWidth, overrideHeight)
  • 圖片開始載入,首先顯示佔點陣圖 可以明白,主要的邏輯還是在 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;
        
        //核心程式碼,載入圖片
        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.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder, priority, isMemoryCacheable, diskCacheStrategy, this), 我們看一下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();
        EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
                loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
                transcoder, loadProvider.getSourceEncoder());

        //使用LruCache獲取快取
        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);
    }
複製程式碼

load方法位於 Engine 類中。load方法內部會從三個來源獲取圖片資料,我們最熟悉的就是LruCache了。如何獲取資料過於複雜,這裡就不再展開分析,我們這裡主要關注圖片資料獲取到之後的操作。獲取到圖片資料之後,通過 cb.onResourceReady(cached) 來處理,我們來看一下這個回撥的具體實現:

@Override
    public void onResourceReady(Resource<?> resource) {
        if (resource == null) {
            onException(new Exception("Expected to receive a Resource<R> with an object of " + transcodeClass
                    + " inside, but instead got null."));
            return;
        }

        Object received = resource.get();
        if (received == null || !transcodeClass.isAssignableFrom(received.getClass())) {
            releaseResource(resource);
            onException(new Exception("Expected to receive an object of " + transcodeClass
                    + " but instead got " + (received != null ? received.getClass() : "") + "{" + received + "}"
                    + " inside Resource{" + resource + "}."
                    + (received != null ? "" : " "
                        + "To indicate failure return a null Resource object, "
                        + "rather than a Resource object containing null data.")
            ));
            return;
        }

        if (!canSetResource()) {
            releaseResource(resource);
            // We can't set the status to complete before asking canSetResource().
            status = Status.COMPLETE;
            return;
        }
        
        //核心是這一句
        onResourceReady(resource, (R) received);
    }
複製程式碼

我們繼續看 onResourceReady(resource, (R) received) 這個方法:

    private void onResourceReady(Resource<?> resource, R result) {
        // We must call isFirstReadyResource before setting status.
        boolean isFirstResource = isFirstReadyResource();
        status = Status.COMPLETE;
        this.resource = resource;

        if (requestListener == null || !requestListener.onResourceReady(result, model, target, loadedFromMemoryCache,
                isFirstResource)) {
            GlideAnimation<R> animation = animationFactory.build(loadedFromMemoryCache, isFirstResource);
            
            //核心,通過呼叫target的onResourceReady方法載入圖片
            target.onResourceReady(result, animation);
        }

        notifyLoadSuccess();

        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logV("Resource ready in " + LogTime.getElapsedMillis(startTime) + " size: "
                    + (resource.getSize() * TO_MEGABYTE) + " fromCache: " + loadedFromMemoryCache);
        }
    }
複製程式碼

我們可以看到核心程式碼:target.onResourceReady(result, animation),其實在這句程式碼的內部最終是通過:

public class DrawableImageViewTarget extends ImageViewTarget<Drawable> {
    public DrawableImageViewTarget(ImageView view) {
        super(view);
    }

    @Override
    protected void setResource(Drawable resource) {
       view.setImageDrawable(resource);
    }
}
複製程式碼

本質是通過 setResource(Drawable resource) 來實現的,在這個方法的內部呼叫了Android內部最常用的載入圖片的方法 view.setImageDrawable(resource)

到此為止,into方法基本已經分析完了,我們忽略了網路圖片獲取的過程,專注於獲取圖片後的處理。現在來對into方法做個總結:

  • 將imageview包裝成imageViewTarget
  • 清除這個imageViewTarget之前繫結的請求,繫結新的請求
  • 執行新的請求
  • 獲取圖片資料之後,成功則會呼叫ImageViewTarget中的onResourceReady()方法,失敗則會呼叫ImageViewTarget中的onLoadFailed();二者的本質都是通過呼叫Android中的imageView.setImageDrawable(drawable)來實現對imageView的圖片載入

LruCache原始碼分析

Glide的基本原始碼分析其實到這裡已經結束了,但提起圖片載入,LruCache是一個不可忽視的關鍵點,在Glide原始碼分析的最後我們再來分析一下LruCache的原始碼,這個LruCache來自於 android.support.v4.util 中:

public class LruCache<K, V> {
    //儲存快取
    private final LinkedHashMap<K, V> map;
    //當前快取的總大小
    private int size;
    //最大快取大小
    private int maxSize;
    //新增到快取的個數
    private int putCount;
    //建立的個數
    private int createCount;
    //移除的個數
    private int evictionCount;
    //命中個數
    private int hitCount;
    //未命中個數
    private int missCount;

    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }

    //重新設定最大快取
    public void resize(int maxSize) {
        //確保最大快取大於0
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }

        synchronized (this) {
            this.maxSize = maxSize;
        }
        //對當前的快取做一些操作以適應新的最大快取大小
        trimToSize(maxSize);
    }

    //獲取快取
    public final V get(K key) {
        //確保key不為null
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V mapValue;
        synchronized (this) {
            mapValue = map.get(key);
            //如果可以獲取key對應的value
            if (mapValue != null) {
                //命中數加一
                hitCount++;
                return mapValue;
            }
            //如果根據key獲取的value為null,未命中數加一
            missCount++;
        }
        
        //省略無關程式碼......
    }

    
    public final V put(K key, V value) {
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;
        synchronized (this) {
            //新增到快取的個數加一
            putCount++;
            //更新當前快取大小
            size += safeSizeOf(key, value);
            previous = map.put(key, value);
            if (previous != null) {
                //如果之前map中對應key存在value不為null,由於重複的key新新增的value會覆蓋上一個value,所以當前快取大小應該再減去之前value的大小
                size -= safeSizeOf(key, previous);
            }
        }
        //根據快取最大值調整快取
        trimToSize(maxSize);
        return previous;
    }

    //根據最大快取大小對map中的快取做調整
    public void trimToSize(int maxSize) {
        while (true) {
            K key;
            V value;
            synchronized (this) {
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }
                //當前快取大小小於最大快取,或LinkedHashMap為空時跳出迴圈
                if (size <= maxSize || map.isEmpty()) {
                    break;
                }
                
                //遍歷LinkedHashMap,刪除頂部的(也就是最先新增的)元素,直到當前快取大小小於最大快取,或LinkedHashMap為空
                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

          //省略無關程式碼......
        }
    }

    public final V remove(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V previous;
        synchronized (this) {
            previous = map.remove(key);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

        return previous;
    }

    private int safeSizeOf(K key, V value) {
        int result = sizeOf(key, value);
        if (result < 0) {
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        }
        return result;
    }

     protected int sizeOf(K key, V value) {
        return 1;
    }


    public final void evictAll() {
        trimToSize(-1); // -1 will evict 0-sized elements
    }

    @Override public synchronized final String toString() {
        int accesses = hitCount + missCount;
        int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;
        return String.format(Locale.US, "LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
                maxSize, hitCount, missCount, hitPercent);
    }
}
複製程式碼

其實從上面的程式碼可以看出,LruCache內部主要靠一個LinkedHashMap來儲存快取,這裡使用LinkedHashMap而不使用普通的HashMap正是看中了它的順序性,即LinkedHashMap中元素的儲存順序就是我們存入的順序,而HashMap則無法保證這一點。

我們都知道Lru演算法就是最近最少使用的演算法,而LruCache是如何保證在快取大於最大快取大小之後移除的就是最近最少使用的元素呢?關鍵在於 trimToSize(int maxSize) 這個方法內部,在它的內部開啟了一個迴圈,遍歷LinkedHashMap,刪除頂部的(也就是最先新增的)元素,直到當前快取大小小於最大快取,或LinkedHashMap為空。這裡需要注意的是由於LinkedHashMap的特點,它的儲存順序就是存放的順序,所以位於頂部的元素就是最近最少使用的元素,正是由於這個特點,從而實現了當快取不足時優先刪除最近最少使用的元素。

相關文章