一步步剖析 Glide 的設計與實現

前言

Glide 源碼解析已經有大神完成了,所以這篇文章不會貼一大段一大段的源碼解析,而是嘗試從需求出發,一步一步地分析 Glide,看看爲何是這麼設計的,這麼設計有什麼好處。算法

假如如今要由咱們來設計一個圖片加載框架,名字叫 Glide,那麼應該怎麼設計呢?編程

首先分析一下需求,圖片加載框架的功能點有:數組

  1. 獲取圖像數據
  2. 顯示圖片到指定的 View 上
  3. 圖片變換效果
  4. 數據緩存
  5. 自定義內容

好,如今初步肯定了功能需求,下面就正式進入開發階段了。緩存

圖片加載流程

Request

首先,咱們定義加載圖片並顯示到 View 上的過程爲一個請求。對於一個請求,應該有開始執行、取消執行、釋放資源、狀態查詢等接口,同時,爲了不同一個請求重複執行,還應該有一個能夠和其它請求比較的方法,所以能夠建立 Request 接口以下:bash

public interface Request {
    /**
     * 開始執行
     */
    void begin();

    /**
     * 取消執行
     */
    void clear();

    // 狀態查詢
    boolean isRunning();
    boolean isComplete();
    boolean isResourceSet();
    boolean isCleared();
    boolean isFailed();

    /**
     * 釋放資源
     */
    void recycle();

    /**
     * 比較 Request 是否等價
     */
    boolean isEquivalentTo(Request other);
}
複製代碼

爲何 Request 是一個接口,而不是一個類呢?由於考慮到縮略圖、出現錯誤時的替代圖片等需求,同一次加載過程當中可能包含多張圖片、多個請求。網絡

所以,對於單張圖片,能夠定義實現類 SingleRequestapp

爲了解決同一次請求中的多張圖片之間的協調問題,好比全圖和縮略圖,能夠定義類 ThumbnailRequestCoordinator,它擁有成員 full 和 thumb,全圖沒有加載完成時,同時加載全圖和縮略圖,縮略圖加載完成後顯示縮略圖,全圖加載完成後取消縮略圖,顯示全圖。框架

出現錯誤時,原始圖片和替代圖片之間的協調問題,能夠定義類 ErrorRequestCoordinator 解決,它擁有成員 primary 和 error,一開始只會加載原始圖片,原始圖片加載失敗以後,纔開始加載替代圖片。異步

假設同一次加載過程的圖片數量是不固定的,好比縮略圖自己可能又包含它本身的縮略圖以及在出現錯誤時的替代圖片,所以,ThumbnailRequestCoordinator 和 ErrorRequestCoordinator 都須要成員 parent,以鏈表的形式將這些請求組織起來,協調多張圖片之間的顯示行爲。同時,它們都有共同的行爲,好比加載是否成功、是否能夠顯示圖片、是否能夠顯示佔位圖、加載成功後怎麼處理、失敗後怎麼處理等,所以,定義接口 RequestCoordinatoride

public interface RequestCoordinator {

    /**
     * 是否能夠顯示圖片
     */
    boolean canSetImage(Request request);

    /**
     * 是否能夠顯示佔位圖
     */
    boolean canNotifyStatusChanged(Request request);

    /**
     * 是否能夠清除資源
     */
    boolean canNotifyCleared(Request request);

    /**
     * 是否已加載完成,並設置了圖片資源
     */
    boolean isAnyResourceSet();

    /**
     * 加載完成後的回調
     */
    void onRequestSuccess(Request request);

    /**
     * 加載失敗後的回調
     */
    void onRequestFailed(Request request);
}
複製代碼

ThumbnailRequestCoordinator 和 ErrorRequestCoordinator 的定義以下:

public class ThumbnailRequestCoordinator implements RequestCoordinator, Request {
    @Nullable
    private final RequestCoordinator parent;
    private Request full;
    private Request thumb;
	...
}
複製代碼
public final class ErrorRequestCoordinator implements RequestCoordinator, Request {
    @Nullable
    private final RequestCoordinator parent;
    private Request primary;
    private Request error;
	...
}
複製代碼

Request、RequestCoordinator 的關係示意圖以下:

Request 鏈表

好,完成了對請求的定義,下面就能夠嘗試構造 Request 了。由於 Request 相關的選項不少,所以構造 Request 的行爲咱們使用建造者模式,新建一個 RequestBuilder 來提供構造 Request 的功能。而考慮到 placeholder、圖片變換、是否緩存等各類需求,咱們又另外建立一個類 RequestOptions,統一提供相關的選項給用戶設置。

除了提供各類選項、縮略圖、替代圖的設置等接口以外,RequestBuilder 還應該提供兩個核心的接口,即 load 和 into。load 方法用於接收 url、String、File 等資源類型,into 用於開始執行請求,並在請求完成以後顯示圖片到指定的 View 上面。

RequestManager

開始執行請求以前,咱們須要考慮一件事情:假如在加載圖片的過程當中,當前 Activity 退出了後臺,或者該圖片須要從網絡中獲取,可是網絡斷開了,怎麼處理呢?

首先,咱們須要監聽 Activity 的生命週期,具體實現時,只須要監聽 onStart、onStop、onDestroy 三個方法便可。基於這個考慮,定義接口 LifecycleLifecycleListener

public interface LifecycleListener {
    void onStart();
    void onStop();
    void onDestroy();
}
複製代碼
public interface Lifecycle {
    void addListener(@NonNull LifecycleListener listener);
    void removeListener(@NonNull LifecycleListener listener);
}

複製代碼

對於 Lifycycle,由於用戶傳入的 Context 類型可能爲 Application,而 Application 沒有 onStart 等生命週期方法。所以,對於 Application 類型的 Context,咱們定義 ApplicationLifecycle,它只會在 addListener 的時候回調 LifecycleListener 的 onStart 方法;對於其它非 Application 類型的 Context,定義 ActivityFragmentLifecycle,維護一組 LifecycleListener,並提供方法 onStart、onStop、onDestroy,在這些方法被調用時回調 LifecycleListener 對應的方法。

那麼問題來了,怎麼監聽 Activity 的生命週期呢?方法其實很簡單,在 Activity 上面添加一個 Fragment 便可,這個 Fragment 是 RequestManagerFragment,它不顯示 View, 只會在 onStart、onStop、onDestroy 方法回調時會執行 ActivityFragmentLifecycle 對應的方法,通知外部執行相應的操做。

這個"外部"是誰呢?很顯然,應該是一個專門用於管理 Request 的類。所以,咱們能夠建立類 RequestManager,讓它實現接口 LifecycleListener,在 onStop 回調時,暫停執行圖片加載請求;onStart 回調時,繼續執行;onDestroy 回調時,中止執行,並釋放資源。

同時,使用廣播監聽網絡狀態,在網絡可用時,從新開始執行以前由於網絡問題被中止的請求。定義接口 ConnectivityMonitor:

public interface ConnectivityMonitor extends LifecycleListener {

    interface ConnectivityListener {
        void onConnectivityChanged(boolean isConnected);
    }
}
複製代碼

另外,考慮到同一 Activity 中同時執行的請求可能有不少個,所以建立 RequestTracker 用於維護這些請求:

public class RequestTracker {
    private final Set<Request> requests = Collections.newSetFromMap(new WeakHashMap<Request, Boolean>());
    private final List<Request> pendingRequests = new ArrayList<>();
	
    public void pauseRequests() { ... }
    public void resumeRequests() { ... }
    public void clearRequests() { ... }
    public void restartRequests() { ... }	
}
複製代碼

輔助類寫好以後,下面就能夠給出 RequestManager 的實現了(爲了簡單起見,下面的代碼有通過一些省略及改動):

public class RequestManager implements LifecycleListener, ... {
	
	...
    @Override
    public void onStart() {
        requestTracker.resumeRequests(); // 繼續執行
    }
	
    @Override
    public void onStop() {
        requestTracker.pauseRequests(); // 暫停執行
    }
	
    @Override
    public void onDestroy() {
        requestTracker.clearRequests(); // 取消執行
    }

    private static class RequestManagerConnectivityListener implements ConnectivityMonitor.ConnectivityListener {
        private final RequestTracker requestTracker;

        RequestManagerConnectivityListener(@NonNull RequestTracker requestTracker) {
            this.requestTracker = requestTracker;
        }

        @Override
        public void onConnectivityChanged(boolean isConnected) {
            if (isConnected) {
                requestTracker.restartRequests(); // 從新開始執行
            }
        }
    }
	
}
複製代碼

如此,就完成了對請求的管理,示意圖以下:

Glide-Lifecycle.png

Resource

先不考慮具體的加載過程,假設圖片數據加載成功了,那麼咱們須要思考:圖片數據用什麼來表示呢?由於從本地文件、從網絡中獲取到的數據,多是 Bitmap、Drawable 等能夠直接顯示的類型,也多是 byte[]、File 等須要轉換的類型。所以咱們須要有一個接口,以便在成功獲取到數據以後,統一對全部數據類型進行處理。這個接口命名爲 Resource,它的做用是對圖像數據進行包裝,並提供統一的對外接口

/**
 * @param Z 指圖像的類型
 */
public interface Resource<Z> {

    /**
     * 返回圖像的類型信息
     */
    @NonNull
    Class<Z> getResourceClass();

    /**
     * 獲取該圖像
     */
    @NonNull
    Z get();

    /**
     * 獲取該圖像佔用內存的大小
     */
    int getSize();

    /**
     * 回收資源
     */
    void recycle();
}
複製代碼

所以,咱們能夠有 BitmapResource、DrawableResource、FileResource、BytesResource、GifDrawableResource 等實現類。

但這樣又會有一個新的問題,怎麼將獲取到的圖像數據轉化爲對應的 Resource 對象呢?爲了解決這個問題,能夠定義一個新的接口 ResourceDecoder,它只須要 handles、decode 兩個方法,handles 用於返回當前 ResourceDecoder 是否能夠處理該數據類型,若是能夠,則執行 decode 方法,將該數據類型轉化爲對應的 Resource 類型:

/**
 * @param <T> 圖像的數據類型
 * @param <Z> 圖像數據類型對應的 Resource 類型
 */
public interface ResourceDecoder<T, Z> {

    boolean handles(@NonNull T source, @NonNull Options options) throws IOException;

    @Nullable
    Resource<Z> decode(@NonNull T source, int width, int height, @NonNull Options options)
            throws IOException;
}
複製代碼

所以, 咱們能夠有 FileDecoder、BitmapDrawableDecoder、ByteBufferBitmapDecoder 等實現類。

Engine

準備工做完成以後,如今終於能夠加載圖像數據了,由於這一步是 Glide 的關鍵步驟之一,涉及的點比較多,例如內存緩存、本地緩存、網絡請求等等,所以咱們把這個功能獨立出來,Request 直接調用便可,不然 Request 會很是臃腫。這個類起名爲 Engine,顧名思義,它是咱們的發動機,即動力(圖像)的來源。

爲了優化性能,在 Engine 加載圖像以前,咱們應該考慮兩件事:

  1. 請求是否重複?
  2. 內存緩存中是否有對應的數據?

對於第一個問題,首先,咱們能夠用一個 Key 標誌相同的請求,所以,建立類 EngineKey。另外,假如前一個相同的請求已記載完成,而且對應的 Resource 引用還在,那麼咱們直接返回該 Resource 便可。但這又引出了一個新的問題,若是全部地方對該 Resource 的引用都釋放了,此時咱們應該移除該 Resource 對象,並使用內存緩存 Resource 的圖像數據,那麼咱們怎麼知道對該 Resource 的全部引用都被釋放了呢?爲了解決這個問題,咱們建立一個新的 Resource 實現,即 EngineResource,它用於包裝另外一個 Resource 對象,並使用引用計數,在計數爲 0 時回調對應的接口通知 Engine 執行資源釋放、內存緩存等操做

class EngineResource<Z> implements Resource<Z> {
    private final Resource<Z> resource;

    interface ResourceListener {
        void onResourceReleased(Key key, EngineResource<?> resource);
    }	
}
複製代碼

同時,建立 ActiveResources 類,用於保存全部計數大於 0 的 Resource 對象:

final class ActiveResources {
    final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();
}
複製代碼

可是,假如前一個相同的請求還沒有加載完成,還在執行中,此時咱們應該使用一個新的類標誌該工做,所以,建立 EngineJob,同時,使用一個集合保存全部正在執行中的工做:

final class Jobs {
    private final Map<Key, EngineJob<?>> jobs = new HashMap<>();
}
複製代碼

假如記載圖像以前,發現 Jobs 已經有對應的工做了,那麼直接返回便可,不須要重複執行。

第一個問題解決了,接着看第二個問題。假如 ActiveResources、Jobs 都沒有獲取到對應的記錄,那麼咱們應該嘗試去內存緩存中獲取圖像數據,並使用 Resource 包裝該圖像數據返回給調用方。所以這一步涉及到緩存,所以具體過程先跳過,後面再講。

經過檢查 ActiveResources、 Jobs、內存緩存都沒有找到相同的記錄,那麼就是時候執行 EngineJob 了,由於這個過程是異步的,所以咱們須要有一個接口監聽結果,所以,建立 EngineJobListener

interface EngineJobListener {

    void onEngineJobComplete(EngineJob<?> engineJob, Key key, EngineResource<?> resource);

    void onEngineJobCancelled(EngineJob<?> engineJob, Key key);
}
複製代碼

前期的檢查工做完成以後,如今就能夠去獲取圖像數據了。此時,咱們有兩個選擇:

  1. 從本地緩存獲取
  2. 從數據源獲取

其中,本地緩存又分爲兩種:

  1. 通過變換的圖像數據
  2. 原始的圖像數據

所以,咱們總共有三個選擇,按優先級排列是:

  1. 本地緩存的通過變換的圖像數據
  2. 本地緩存的原始的圖像數據
  3. 從數據源獲取

爲了統一上述三種行爲,並在成功獲取到數據後執行後續的操做,咱們定義接口:

interface DataFetcherGenerator {

    interface FetcherReadyCallback {

        /**
         * 從新開始執行 startNext
         */
        void reschedule();

        /**
         * 數據加載完成
         */
        void onDataFetcherReady(Key sourceKey, @Nullable Object data, DataFetcher<?> fetcher,
                                DataSource dataSource, Key attemptedKey);

        /**
         * 數據加載失敗
         */
        void onDataFetcherFailed(Key attemptedKey, Exception e, DataFetcher<?> fetcher,
                                 DataSource dataSource);
    }

    // 嘗試獲取數據,若是當前實現沒法獲取,則交給下一個
    boolean startNext();

    // 取消
    void cancel();
}
複製代碼

所以,咱們能夠有 ResourceCacheGenerator、DataCacheGenerator、SourceGenerator 三個實現類。

先無論緩存的實現,直接看 SourceGenerator,由於用戶傳入的資源參數類型有不少個,好比 Uri、File、String 等,對於每一種資源類型,咱們應該至少有一種獲取數據的方法,所以咱們定義接口 DataFetcher 以統一數據獲取的行爲:

/**
 * @param <T> 準備獲取的數據類型
 */
public interface DataFetcher<T> {

    interface DataCallback<T> {
        // 數據獲取成功
        void onDataReady(@Nullable T data);
        // 數據獲取失敗
        void onLoadFailed(@NonNull Exception e);
    }

    // 加載圖像數據
    void loadData(@NonNull Priority priority, @NonNull DataCallback<? super T> callback);
    // 釋放資源
    void cleanup();
    // 取消加載
    void cancel();
    // 獲取數據類型
    Class<T> getDataClass();
	...
}
複製代碼

DataFetcher 的實現類應該包括 AssetPathFetcher、ByteBufferFetcher、FileFetcher、HttpUrlFetcher 等等,以對應不一樣的資源類型。同時,對於網絡請求,除了經過 HttpURLConnection獲取以外,由於 OkHttp 等第三方庫應用很是普遍,咱們還應該提供經過第三方庫獲取數據的接口,所以有實現類 OkHttpStreamFetcher、VolleyStreamFetcher 等。

可是咱們怎麼知道應該使用哪一個 DataFetcher 呢?爲了解決這個問題,能夠定義工廠接口 ModelLoader,和 ResourceDecoder 同樣,提供一個 handles 方法:

/**
 * @param <Model> 傳入的資源類型,好比 String、File、Uri 等
 * @param <Data>  成功獲取數據後,可提供給 ResourceDecoder 使用的數據類型
 */
public interface ModelLoader<Model, Data> {

	class LoadData<Data> {
        public final Key sourceKey;
        public final List<Key> alternateKeys;
        public final DataFetcher<Data> fetcher;

        public LoadData(@NonNull Key sourceKey, @NonNull DataFetcher<Data> fetcher) {
            this(sourceKey, Collections.<Key>emptyList(), fetcher);
        }

        public LoadData(@NonNull Key sourceKey, @NonNull List<Key> alternateKeys,
                        @NonNull DataFetcher<Data> fetcher) {
            this.sourceKey = Preconditions.checkNotNull(sourceKey);
            this.alternateKeys = Preconditions.checkNotNull(alternateKeys);
            this.fetcher = Preconditions.checkNotNull(fetcher);
        }
    }

    // 構建 LoadData
    @Nullable
    LoadData<Data> buildLoadData(@NonNull Model model, int width, int height,
                                 @NonNull Options options);

    // 是否能夠處理該數據類型
    boolean handles(@NonNull Model model);
}
複製代碼

這樣,不管用戶傳入什麼類型的資源,咱們均可以遍歷 ModelLoader,找到可以處理該類型的 DataFetcher,並執行 loadData 方法便可,數據獲取成功以後,就會回調 DataCallback、FetcherReadyCallback、EngineJobListener 等接口,將數據回調給調用方。

Target

經過 DataFetcher 獲取到數據,並使用 ResourceDecoder 將圖像數據轉化爲 Resource 對象以後,咱們就能夠把它顯示到 View 上面了。通常狀況下,這個 View 會是一個 ImageView,但也多是其它類型的 View。所以,咱們定義一個統一的接口 Target,這個 Target 應該具有什麼行爲呢?首先,它應該監聽結果的回調、數據加載的過程;接着,它要測量本身的寬高,並回調給外部;最後,它應該繼承 LifecycleListener,爲何呢?由於對於 GIF 圖而言,若是 Activity 的 onStop 執行了,它須要中止動畫,以節省資源,並避免出現問題。所以,Target 接口定義以下:

public interface Target<R> extends LifecycleListener {

    /**
     * 開始加載
     */
    void onLoadStarted(@Nullable Drawable placeholder);

    /**
     * 加載失敗
     */
    void onLoadFailed(@Nullable Drawable errorDrawable);

    /**
     * 加載完成
     */
    void onResourceReady(@NonNull R resource, @Nullable Transition<? super R> transition);

    /**
     * 取消加載
     */
    void onLoadCleared(@Nullable Drawable placeholder);

    /**
     * 返回尺寸
     */
    void getSize(@NonNull SizeReadyCallback cb);

    /**
     * 移除回調
     */
    void removeCallback(@NonNull SizeReadyCallback cb);

    /**
     * 設置對應的 Request
     */
    void setRequest(@Nullable Request request);

    /**
     * 獲取對應的 Request
     */
    @Nullable
    Request getRequest();
}
複製代碼

對於 Target,咱們有實現類 ImageViewTarget、AppWidgetTarget、FutureTarget 等。

小結

也許你已經發現了,Glide 的設計是很符合針對接口編程這一原則的,閱讀源碼時,最重要的也是上述幾個接口,具體實現反而不用太在乎。

圖片加載的流程圖以下所示:

圖片加載流程

緩存

下面簡單說一下 Glide 的緩存機制。Glide 的緩存機制主要包括數組對象緩存池、Bitmap 緩存池、內存緩存、本地緩存等,分別對應 ArrayPool、BitmapPool、MemoryCache、DiskCache 幾個接口,主要的實現類分別是 LruArrayPool、LruBitmapPool、LruResourceCache、DiskLruCacheWrapper,即 Glide 的緩存行爲基本都是經過 LRU 算法實現的,實現原理基本都是:每 put 一個元素就檢查當前已緩存的資源所佔用的內存大小是否大於限定值,若是是,則移除最近最少使用的資源,直到符合要求。

限於篇幅,這裏就不展開講了,說一下緩存相關的值得注意的幾個接口或類。

  1. MemorySizeCalculator。用於計算分配給各個緩存池的內存大小,之內存足夠的 Android 8.0 及以上的設備爲例,ArrayPool 固定 4MB,BitmapPool 佔用一個手機屏幕的像素大小(例如 1920 * 1080 分辨率的手機約等於 8MB),MemoryCache 佔用 2 個手機屏幕的像素大小(例如 1920 * 1080 分辨率的手機約等於 16MB)。

  2. BitmapPool。Android API 11 以後能夠重用 Bitmap,但須要 width、height 等屬性都符合要求才能夠;而 Android API 19 以後,只要複用的 Bitmap 是可變的,且內存佔用大於等於目標 Bitmap 便可。所以 BitmapPool 緩存 Bitmap 時,會根據設備的 API 選擇 SizeConfigStrategy 或 AttributeStrategy 兩種策略。

  3. DiskCache。本地緩存接口,主要實現類爲 DiskLruCacheWrapper,是 DiskLruCache 的封裝類,默認最大緩存 250MB 的數據,默認緩存文件夾默認爲 /data/data/< package name>/cache/image_manager_disk_cache。

  4. Encoder。用於編寫圖像數據到本地文件中,是實現 DiskCache 的主要輔助類,不一樣的圖像類型對應不一樣的 Encoder,例如 BitmapEncoder,是使用 Bitmap 的 compress 方法寫入到 FileOutputStream 實現的。

  5. EncodeStrategy。本地緩存行爲,共有 SOURCE、TRANSFORMED、NONE 三種,即緩存原始圖像數據、緩存通過變換的圖像數據、不緩存。

其它

最後再簡單說一下其它幾個值得注意的接口和類:

  1. Transition。用於在更換圖片時實現漸變等動畫效果。

  2. Transformation。用於實現圖片的變換效果,包括 CenterCrop、FitCenter、RoundedCorners 等。

  3. ResourceTranscoder。用於將一種 Resource 類型轉換爲另外一種 Resource 類型,好比 BitmapBytesTranscoder、BitmapDrawableTranscoder、GifDrawableBytesTranscoder 等。

  4. GlideExecutor。用於獲取在加載圖片、緩存圖片、加載動畫幀等狀況下使用的線程池。

  5. Registry。用於註冊各類 ResourceDecoder、Encoder、ResourceTranscoder、ModelLoader 等等,在須要使用上述幾個接口的實現類時,能夠方便地經過 Registry 來獲取。

  6. AppGlideModule。用於指定 Glide 的自定義行爲。好比能夠經過 GlideBuilder 設置 本身的 ArrayPool、BitmapPool、MemoryCache、默認 RequestOptions 等,還能夠經過 Registry 註冊本身的 ResourceDecoder、ModelLoader,以自定義圖像數據獲取的行爲。

相關文章
相關標籤/搜索