Glide 知識梳理(6) – Glide 原始碼解析之流程剖析

澤毛發表於2019-02-26

一、前言

不得不說,Glide真的是十分難啃,來來回回看了很多的文章,並對照著原始碼分析了一遍,才理清了大概的思路,希望這篇文章能對大家有一定的幫助。

為什麼要閱讀 Glide 的原始碼

在進入正題之前讓我們先談一些題外話,就是 為什麼我們要去看 Glide 的原始碼

如果大家有閱讀過之前的五篇關於Glide使用的教程:

Glide 知識梳理(1) – 基本用法
Glide 知識梳理(2) – 自定義 Target
Glide 知識梳理(3) – 自定義 transform
Glide 知識梳理(4) – 自定義 animate
Glide 知識梳理(5) – 自定義 GlideModule

可以發現其實Glide的功能已經很完備了,無論是佔位符、錯誤圖片還是請求完後對於返回圖片的變換,都提供瞭解決的方案,完全可以滿足日常的需求。

那麼,我們為什麼要花費大量的時間去看Glide的原始碼呢,我自己的觀點是以下幾點:

  • 理解API的原理。在之前介紹使用的幾篇文章中,我們談到了許多的方法,例如placeholder/error/...,還講到了自定義transform/target/animate。但是由於Glide將其封裝的很好,僅僅通過簡單使用你根本無法瞭解這些用法最後是如何生效的,只有通過閱讀原始碼才能明白。
  • 學習圖片載入框架的核心思想。無論是古老的ImageLoader,還是後來的Picassofresco,對於一個圖片載入框架來說,都離不開三點:請求管理、工作執行緒管理和圖片快取管理。閱讀原始碼將有助於我們學習到圖片請求框架對於這三個核心問題的解決方案,這也是我認為 最關鍵的一點
  • 學習Glide的架構設計,對於Glide來說,這一點可能適合於高水平的程式設計師,因為實在是太複雜了。

怎麼閱讀 Glide 的原始碼

在閱讀原始碼之前,還是要做一些準備性的工作的,不然你會發現沒過多久你就想放棄了,我的準備工作分為以下幾步:

  • 掌握Glide的高階用法。千萬不要滿足於呼叫load方法載入出圖片就滿足了,要學會去了解它的一些高階用法,例如在 Glide 知識梳理(5) – 自定義GlideModule 一文中介紹如何自定義ModelLoader,你就會對ModelLoader有個大概的印象,知道它是用來做什麼的,不然在原始碼中看到這個類的時候肯定會一臉懵逼,沒多久就放棄了。
  • 看幾篇網上寫的不錯的文章,例如郭神的 Android圖片載入框架最全解析(二),從原始碼的角度理解Glide的執行流程 ,不必過於關注實現的細節,而是注意看他們對每段程式碼的描述,先有個大概的印象。
  • 從一個最簡單的Demo入手,通過 斷點的方式,一步步地走,觀察每個變數的型別和值。
  • 最後,無論現在記得如何清楚,一定自己親自寫文件,最重要的是 畫圖,把整個呼叫的流程通過圖片的形式整理出來,不然真的是會忘的。

原始碼分析流程

我們從最簡單的例子入手,載入一個網路的圖片地址,並在ImageView上展示。

Glide.with(this).load("http://i.imgur.com/DvpvklR.png").into(mImageView);
複製程式碼

這上面的鏈式呼叫分為三步,其中前兩步是進行一些準備工作,真正進行處理的邏輯是在第三步當中:

Glide 知識梳理(6) – Glide 原始碼解析之流程剖析

二、with(Activity activity)

with(Activity activity)Glide的一個靜態方法,當呼叫該方法之後會在Activity中新增一個Glide內部自定義的RequestManagerFragment,當Activity的狀態發生變化時,該Fragment的狀態也會發生相應的變化。經過一系列的呼叫,這些變化將會通知到RequestManagerRequestManager則通過RequestTracker來管理所有的Request,這樣RequestManager在處理請求的時候,就可以根據Activity當前的狀態進行處理。

Glide 知識梳理(6) – Glide 原始碼解析之流程剖析

三、load(String string)

with方法會返回一個RequestManager物件,接下來第二步。

Glide 知識梳理(1) – 基本用法 中,我們學習了許多load的過載方法,可以從urlSDCardbyte[]陣列中來載入。這一步的目的是根據load傳入的型別,建立對應的DrawableTypeRequest物件,DrawableTypeRequest的繼承關係如下所示,它是Request請求的建立者,真正建立的邏輯是在第三步中建立的。

Glide 知識梳理(6) – Glide 原始碼解析之流程剖析

所有的load方法最終都會通過loadGeneric(Class<T> class)方法來建立一個DrawableTypeRequest物件,在DrawableTypeRequest建立時需要傳入兩個關鍵的變數:

  • streamModelLoader
  • fileDescriptorModelLoader

這兩個變數的型別均為ModelLoader,看到這個是不是有似曾相識的感覺,沒錯,在 Glide 知識梳理(5) – 自定義GlideModule 中我們介紹瞭如果通過OkHttpClient來替換HttpURLConnection時就已經介紹了它,這 兩個變數決定了獲取圖片資源的方式

現在,只要明白上面這點就可以了,後面在用到它其中的成員變數時,我們再進行詳細的分析。

四、into(ImageView imageView)

下面,我們來看真正的重頭戲,即into方法執行過程,該方法中包含了載入資源,設定資源到對應目標的一整套邏輯。

4.1 GenericRequestBuilder

public class GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> implements Cloneable {

    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()) {
                case CENTER_CROP:
                    applyCenterCrop();
                    break;
                case FIT_CENTER:
                case FIT_START:
                case FIT_END:
                    applyFitCenter();
                    break;
                //$CASES-OMITTED$
                default:
                    // Do nothing.
            }
        }
        //如果是 ImageView,那麼返回的是 GlideDrawableImageViewTarget。
        return into(glide.buildImageViewTarget(view, transcodeClass));
    }

}
複製程式碼

由於DrawableTypeRequest並沒有重寫into方法,因此會呼叫到它的父類DrawableRequestBuilderinto(ImageView)方法中,DrawableRequestBuilder又會呼叫它的父類GenericRequestBuilderinto(ImageView)方法。

這裡首先會根據viewscaleType進行變換,然後再通過Glide.buildImageViewTarget方法建立TargetTarget的含義是 資源載入完畢後所要傳遞的物件,也就是整個載入過程的終點,對於ImageView來說,該方法建立的是GlideDrawableImageViewTarget

public class GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> implements Cloneable {

    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())");
        }

        //如果該 Target 之前已經有關聯的請求,那麼要先將之前的請求取消。
        Request previous = target.getRequest();
        if (previous != null) {
            previous.clear();
            requestTracker.removeRequest(previous);
            previous.recycle();
        }

        //建立 Request,並將 Request 和 Target 關聯起來。
        Request request = buildRequest(target);
        target.setRequest(request);
        lifecycle.addListener(target);

        //這一步會觸發任務的執行。
        requestTracker.runRequest(request);
        return target;
    }

}
複製程式碼

接下來,看GenericRequestBuilder中的into(Target)方法,在into方法中會通過buildRequest方法建立Request,並將它和Target進行 雙向關聯Request的實現類為GenericRequest

在建立完Request之後,通過RequestTrackerrunRequest嘗試去執行任務。

4.2 RequestTracker

public class RequestTracker {

    public void runRequest(Request request) {
        requests.add(request);
        if (!isPaused) {
            request.begin();
        } else {
            pendingRequests.add(request);
        }
    }

}
複製程式碼

RequestTracker會判斷當前介面是否處於可見狀態,如果是可見的,那麼就呼叫Requestbegin方法發起請求,否則就先將請求放入到等待佇列當中,Request的實現類為GenericRequest

4.3 GenericRequest

public final class GenericRequest<A, T, Z, R> implements Request, SizeReadyCallback,
        ResourceCallback {

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

        status = Status.WAITING_FOR_SIZE;
        //首先判斷寬高是否有效,如果有效那麼就呼叫 onSizeReady。
        if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
            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));
        }
    }

    @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 的 load 方法進行載入。
        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));
        }
    }

}
複製程式碼

GenericRequestbegin()方法中,首先判斷寬高是否有效,如果有效那麼就呼叫onSizeReady方法,假如寬高無效,那麼會先計算寬高,計算完之後也會呼叫onSizeReady,這裡面會呼叫Engineload方法去載入。

4.4 Engine

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();

        //建立快取的`key`。
        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 engineJob = engineJobFactory.build(key, isMemoryCacheable);

        //DecodeJob 對應於任務的處理者。
        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);
        //將該 Runnable 放入到執行緒池當中,執行時會呼叫 run() 方法。
        engineJob.start(runnable);

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

}
複製程式碼

Enginebegin方法中,會在記憶體中查詢是否有快取,如果在記憶體當中找不到快取,那麼就會建立EngineJobDecodeJobEngineRunnable這三個類,嘗試從資料來源中載入資源,觸發的語句為engineJob.start(runnable)

4.5 EngineRunnable

class EngineRunnable implements Runnable, Prioritized {

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

        Exception exception = null;
        Resource<?> resource = null;
        try {
            //decode 方法返回 Resource 物件。
            resource = 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;
        }
        //如果成功載入資源,那麼就會回撥onLoadComplete方法。
        if (resource == null) {
            onLoadFailed(exception);
        } else {
            onLoadComplete(resource);
        }
    }

    private Resource<?> decode() throws Exception {
        if (isDecodingFromCache()) {
            //從本地快取中獲取。
            return decodeFromCache();
        } else {
            //從源地址中獲取。
            return decodeFromSource();
        }
    }

    private void onLoadComplete(Resource resource) {
        manager.onResourceReady(resource);
    }

    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;
    }

    private Resource<?> decodeFromSource() throws Exception {
        return decodeJob.decodeFromSource();
    }

}
複製程式碼

EngineJobstart方法會通過內部執行緒池的submit方法提交任務,當任務被排程執行時,會呼叫到EngineRunnablerun()方法。

run()方法中,會根據任務的型別來判斷是從磁碟快取還是從原始資料來源中獲取資源,即分別呼叫DecodeJobdecodeFromCache或者decodeFromSource,最終會將資源封裝為Resource<T>物件。

假如成功獲取到了資源,那麼會通過manager.onResourceReady返回,manager的型別為EngineRunnableManager,其實現類為之前我們看到的EngineJob

4.6 EngineJob

class EngineJob implements EngineRunnable.EngineRunnableManager {

    @Override
    public void onResourceReady(final Resource<?> resource) {
        this.resource = resource;
        MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget();
    }

    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;

        // Hold on to resource for duration of request so we don`t recycle it in the middle of notifying if it
        // synchronously released by one of the callbacks.
        engineResource.acquire();
        listener.onEngineJobComplete(key, engineResource);

        for (ResourceCallback cb : cbs) {
            if (!isInIgnoredCallbacks(cb)) {
                engineResource.acquire();
                //ResourceCallback 的實現類為 GenericRequest
                cb.onResourceReady(engineResource);
            }
        }
        // Our request is complete, so we can release the resource.
        engineResource.release();
    }

}
複製程式碼

EngineJobonResourceReady方法中,會將Resource通過Handler的方法傳遞到主執行緒,並呼叫handleResultOnMainThread,注意該方法中帶有註釋的部分,這裡的ResourceCallback就是我們之前看到GenericRequest

4.7 GenericRequest

public final class GenericRequest<A, T, Z, R> implements Request, SizeReadyCallback,
        ResourceCallback {

    @SuppressWarnings("unchecked")
    @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);
    }

    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(result, animation);
        }

        notifyLoadSuccess();

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

}
複製程式碼

GenericRequestonResourceReady方法中,會呼叫TargetonResourceReady方法,也就是我們最開始講到的GlideDrawableImageViewTarget

4.8 GlideDrawableImageViewTarget

public class GlideDrawableImageViewTarget extends ImageViewTarget<GlideDrawable> {

    @Override
    public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> animation) {
        if (!resource.isAnimated()) {
            //TODO: Try to generalize this to other sizes/shapes.
            // This is a dirty hack that tries to make loading square thumbnails and then square full images less costly
            // by forcing both the smaller thumb and the larger version to have exactly the same intrinsic dimensions.
            // If a drawable is replaced in an ImageView by another drawable with different intrinsic dimensions,
            // the ImageView requests a layout. Scrolling rapidly while replacing thumbs with larger images triggers
            // lots of these calls and causes significant amounts of jank.
            float viewRatio = view.getWidth() / (float) view.getHeight();
            float drawableRatio = resource.getIntrinsicWidth() / (float) resource.getIntrinsicHeight();
            if (Math.abs(viewRatio - 1f) <= SQUARE_RATIO_MARGIN
                    && Math.abs(drawableRatio - 1f) <= SQUARE_RATIO_MARGIN) {
                resource = new SquaringDrawable(resource, view.getWidth());
            }
        }
        super.onResourceReady(resource, animation);
        this.resource = resource;
        resource.setLoopCount(maxLoopCount);
        resource.start();
    }

    /**
     * Sets the drawable on the view using
     * {@link android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)}.
     *
     * @param resource The {@link android.graphics.drawable.Drawable} to display in the view.
     */
    @Override
    protected void setResource(GlideDrawable resource) {
        view.setImageDrawable(resource);
    }
}
複製程式碼

GlideDrawableImageViewTargetonResourceReady方法中,會首先回撥父類的super.onResourceReady,父類的該方法又會回撥setResource方法,而GlideDrawableImageViewTargetsetResource就會通過setResource將載入好的圖片資源設定進去。

由於夾雜著程式碼和文字看著比較亂,整個的流程圖如下所示,並在有道雲筆記上整理了一下關鍵的類和函式呼叫語句,直達連結

第三步呼叫流程

五、小結

這篇文章目的是讓大家對整個流程有一個大致的瞭解,所以對於很多細節問題沒有深究,例如:

  • Engineload()方法中,會執行兩次記憶體快取的判斷,這裡面實現的機制是怎麼樣的?
  • EngineRunnabledecode()方法中,採用DecodeJob去資料來源載入資源,資源載入以及解碼的過程是怎麼樣的?
  • EngineRunnable中回撥給EngineResource<T>是一個什麼樣的物件?

對於以上的這些問題,會在後面單獨的一個個章節進行分析。

相關文章