Glide 系列-2:主流程原始碼分析(4.8.0)

WngShhng發表於2019-01-06

Glide 是 Android 端比較常用的圖片載入框架,這裡我們就不再介紹它的基礎的使用方式。你可以通過檢視其官方文件學習其基礎使用。這裡,我們給出一個 Glide 的最基本的使用示例,並以此來研究這個整個過程發生了什麼:

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

上面的程式碼雖然簡單,但是整個執行過程涉及許多類,其流程也比較複雜。為了更清楚地說明這整個過程,我們將 Glide 的圖片載入按照呼叫的時間關係分成了下面幾個部分:

  1. with() 方法的執行過程
  2. load() 方法的執行過程
  3. into() 方法的執行過程
    1. 階段1:開啟 DecodeJob 的過程
    2. 階段2:開啟網路流的過程
    3. 階段3:將輸入流轉換為 Drawable 的過程
    4. 階段4:將 Drawable 展示到 ImageView 的過程

即按照上面的示例程式碼,先分成 with()load()into() 三個過程,而 into() 過程又被細化成四個階段。

下面我們就按照上面劃分的過程來分別介紹一下各個過程中都做了哪些操作。

1、with() 方法的執行過程

1.1 例項化單例的 Glide 的過程

當呼叫了 Glide 的 with() 方法的時候會得到一個 RequestManager 例項。with() 有多個過載方法,我們可以使用 Activity 或者 Fragment 等來獲取 Glide 例項。它們最終都會呼叫下面這個方法來完成最終的操作:

public static RequestManager with(Context context) {
    return getRetriever(context).get(context);
}
複製程式碼

getRetriever() 方法內部我們會先使用 Glideget() 方法獲取一個單例的 Glide 例項,然後從該 Glide 例項中得到一個 RequestManagerRetriever:

private static RequestManagerRetriever getRetriever(Context context) {
    return Glide.get(context).getRequestManagerRetriever();
}
複製程式碼

這裡呼叫了 Glide 的 get() 方法,它最終會呼叫 initializeGlide() 方法例項化一個單例Glide 例項。在之前的文中我們已經介紹了這個方法。它主要用來從註解和 Manifest 中獲取 GlideModule,並根據各 GlideModule 中的方法對 Glide 進行自定義:

《Glide 系列-1:預熱、Glide 的常用配置方式及其原理》

下面的方法中需要傳入一個 GlideBuilder 例項。很明顯這是一種構建者模式的應用,我們可以使用它的方法來實現對 Glide 的個性化配置:

private static void initializeGlide(Context context, GlideBuilder builder) {

    // ... 各種操作,略

    // 賦值給靜態的單例例項
    Glide.glide = glide;
}
複製程式碼

最終 Glide 例項由 GlideBuilderbuild() 方法構建完畢。它會直接呼叫 Glide 的構造方法來完成 Glide 的建立。在該構造方法中會將各種型別的圖片資源及其對應的載入類的對映關係註冊到 Glide 中,你可以閱讀原始碼瞭解這部分內容。

1.2 Glide 的生命週期管理

with() 方法的執行過程還有一個重要的地方是 Glide 的生命週期管理。因為當我們正在進行圖片載入的時候,Fragment 或者 Activity 的生命週期可能已經結束了,所以,我們需要對 Glide 的生命週期進行管理。

Glide 對這部分內容的處理也非常巧妙,它使用沒有 UI 的 Fragment 來管理 Glide 的生命週期。這也是一種非常常用的生命週期管理方式,比如 RxPermission 等框架都使用了這種方式。你可以通過下面的示例來了解它的作用原理:

示例程式碼:使用 Fragment 管理 onActivityResult()

with() 方法中,當我們呼叫了 RequestManagerRetrieverget() 方法之後,會根據 Context 的型別呼叫 get() 的各個過載方法。

  public RequestManager get(@NonNull 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);
  }
複製程式碼

我們以 Activity 為例。如下面的方法所示,噹噹前位於後臺執行緒的時候,會使用 Application 的 Context 獲取 RequestManager,否則會使用無 UI 的 Fragment 進行管理:

  public RequestManager get(@NonNull Activity activity) {
    if (Util.isOnBackgroundThread()) {
      return get(activity.getApplicationContext());
    } else {
      assertNotDestroyed(activity);
      android.app.FragmentManager fm = activity.getFragmentManager();
      return fragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
    }
  }
複製程式碼

然後就呼叫到了 fragmentGet() 方法。這裡我們從 RequestManagerFragment 中通過 getGlideLifecycle() 獲取到了 Lifecycle 物件。Lifecycle 物件提供了一系列的、針對 Fragment 生命週期的方法。它們將會在 Fragment 的各個生命週期方法中被回撥。

  private RequestManager fragmentGet(Context context, FragmentManager fm, 
    Fragment parentHint, boolean isParentVisible) {
    RequestManagerFragment current = getRequestManagerFragment(fm, parentHint, isParentVisible);
    RequestManager requestManager = current.getRequestManager();
    if (requestManager == null) {
      Glide glide = Glide.get(context);
      requestManager =
          factory.build(
              glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
      current.setRequestManager(requestManager);
    }
    return requestManager;
  }
複製程式碼

然後,我們將該 Lifecycle 傳入到 RequestManager 中,以 RequestManager 中的兩個方法為例,RequestManager 會對 Lifecycle 進行監聽,從而達到了對 Fragment 的生命週期進行監聽的目的:

  public void onStart() {
    resumeRequests();
    targetTracker.onStart();
  }

  public void onStop() {
    pauseRequests();
    targetTracker.onStop();
  }
複製程式碼

1.3 小結

經過上述分析,我們可以使用下面的流程圖總結 Glide 的 with() 方法的執行過程:

Glide 的 with() 方法的執行過程

2、load() 方法的執行過程

2.1 load() 的過程

當我們拿到了 RequestManager 之後就可以使用它來呼叫 load() 方法了。在我們的示例中傳入的是一個 url 物件。load() 方法也是過載的,我們可以傳入包括 Bitmap, Drawable, Uri 和 String 等在內的多種資源型別。示例中會呼叫下面的這個方法得到一個 RequestBuilder 物件,顯然這是一種構建者模式的應用。我們可以使用 RequestBuilder 的其他方法來繼續構建圖片載入請求,你可以通過檢視它的原始碼瞭解 Glide 都為我們提供了哪些構建方法:

  public RequestBuilder<TranscodeType> load(@Nullable String string) {
    return loadGeneric(string);
  }
複製程式碼

RequestBuilder 的構造方法中存在一個 apply() 方法值得我們一提,其定義如下。從下面的方法定義中可以看出,我們可以通過為 RequestBuilder 指定 RequestOptions 來配置當前圖片載入請求。比如,指定磁碟快取的策略,指定佔點陣圖,指定圖片載入出錯時顯示的圖片等等。那麼我們怎麼得到 RequestOptions 呢?在 Glide 4.8.0 中的類 RequestOptions 為我們提供了一系列的靜態方法,我們可以這些方法來得到 RequestOptions 的例項:

  public RequestBuilder<TranscodeType> apply(RequestOptions requestOptions) {
    Preconditions.checkNotNull(requestOptions);
    this.requestOptions = getMutableOptions().apply(requestOptions);
    return this;
  }
複製程式碼

回過頭來,我們可以繼續跟蹤 load() 方法。其實,不論我們使用了 load() 的哪個過載方法,最終都會呼叫到下面的方法。它的邏輯也比較簡單,就是將我們的圖片資源資訊賦值給 RequestBuilder 的區域性變數就完事了。至於圖片如何被載入和顯示,則在 into() 方法中進行處理。

  public RequestBuilder<TranscodeType> load(@Nullable String string) {
    return loadGeneric(string);
  }

  private RequestBuilder<TranscodeType> loadGeneric(@Nullable Object model) {
    this.model = model;
    isModelSet = true;
    return this;
  }
複製程式碼

2.2 小結

所以,我們可以總結 Glide 的 load() 方法的執行過程如下。也就是使用 RequestManger 得到一個 RequestBuilder 的過程:

Glide 的 load() 方法執行過程

3、into() 方法的執行過程

考慮到 into() 方法流程比較長、涉及的類比較多,我們按照圖片載入的過程將其分成四個階段來進行介紹。

第一個階段是開啟 DecodeJob 的過程。DecodeJob 負責從快取或者從原始的資料來源中載入圖片資源,對圖片進行變換和轉碼,是 Glide 圖片載入過程的核心。DecodeJob 繼承了 Runnable,實際進行圖片載入的時候會將其放置到執行緒池當中執行。這個階段我們重點介紹的是從 RequestBuilder 構建一個 DecodeJob 並開啟 DecodeJob 任務的過程。即構建一個 DecodeJob 並將其丟到執行緒池裡的過程。

第二個階段是開啟網路流的過程。這個階段會根據我們的圖片資源來從資料來源中載入圖片資料。以我們的示例為例,在預設情況下會從網路當中載入圖片,並得到一個 InputStream.

第三個階段是將輸入流轉換為 Drawable 的過程。得到了 InputStream 之後還要呼叫 BitmapFactorydecodeStream() 方法來從 InputStream 中得到一個 Drawable.

第四個階段是將 Drawable 顯示到 ImageView 上面的過程。

3.1 階段1:開啟 DecodeJob 的過程

3.1.1 流程分析

我們繼續沿著 into() 方法進行分析。

into() 方法也定義在 RequestBuilder 中,並且也是過載的。不論我們呼叫哪個過載方法都會將要用來顯示圖片的物件封裝成一個 Target 型別。Target 主要用來對用來顯示圖片的物件的生命週期進行管理。當我們要將圖片載入到 ImageView 的時候,最終會呼叫下面的 buildTarget() 方法來講我們的 ImageView 封裝成一個 ViewTarget,然後呼叫 into() 的過載方法進行後續處理:

  public <Z> ViewTarget<ImageView, Z> buildTarget(ImageView view, Class<Z> clazz) {
    if (Bitmap.class.equals(clazz)) {
      return (ViewTarget<ImageView, Z>) new BitmapImageViewTarget(view);
    } else if (Drawable.class.isAssignableFrom(clazz)) {
      return (ViewTarget<ImageView, Z>) new DrawableImageViewTarget(view);
    } else {
      throw new IllegalArgumentException(
          "Unhandled class: " + clazz + ", try .as*(Class).transcode(ResourceTranscoder)");
    }
  }

  private <Y extends Target<TranscodeType>> Y into(Y target,
      RequestListener<TranscodeType> targetListener,
      RequestOptions options) {

    options = options.autoClone();
    Request request = buildRequest(target, targetListener, options); // 1

    Request previous = target.getRequest();
    if (request.isEquivalentTo(previous)
        && !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {
      request.recycle();
      if (!Preconditions.checkNotNull(previous).isRunning()) {
        previous.begin();
      }
      return target;
    }

    requestManager.clear(target);
    target.setRequest(request);
    requestManager.track(target, request); // 2

    return target;
  }
複製程式碼

在上面的 into() 方法的 1 處最終會呼叫到下面的方法來構建一個請求物件。(這裡我們忽略掉具體的引數,只給看構建請求的邏輯)。簡而言之,該方法會根據我們是否呼叫過 RequestBuildererror() 方法設定過圖片載入出錯時候顯示的圖片來決定返回 mainRequest 還是 errorRequestCoordinator。因為我們沒有設定該引數,所以會直接返回 mainRequest

  private Request buildRequestRecursive(/*各種引數*/) {

    ErrorRequestCoordinator errorRequestCoordinator = null;
    if (errorBuilder != null) {
      errorRequestCoordinator = new ErrorRequestCoordinator(parentCoordinator);
      parentCoordinator = errorRequestCoordinator;
    }

    Request mainRequest = buildThumbnailRequestRecursive(/*各種引數*/); // 1

    if (errorRequestCoordinator == null) {
      return mainRequest;
    }

    // ... 略

    Request errorRequest = errorBuilder.buildRequestRecursive(/*各種引數*/);
    errorRequestCoordinator.setRequests(mainRequest, errorRequest);
    return errorRequestCoordinator;
  }
複製程式碼

上面是根據是否設定載入失敗時顯示的圖片來決定返回的請求物件的。如果你使用過 Glide 的話,那麼一定記得除了設定載入失敗時的圖片,我們還會先載入一張小圖,即 Thumbnail。所以,在上面方法的 1 處會根據設定呼叫過 RequestBuilderthumbnail() 方法來決定返回 Thumbnail 的請求還是真實圖片的請求。同樣因為我們沒有設定過該方法,所以最終會呼叫下面的方法來構建最終的圖片載入請求。

  private Request obtainRequest(/*各種引數*/) {
    return SingleRequest.obtain(/*各種引數*/);
  }
複製程式碼

SingleRequestobtain() 方法中會先嚐試從請求的池中取出一個請求,當請求不存在的時候就會例項化一個 SingleRequest,然後呼叫它的 init() 方法完成請求的初始化工作。這裡的請求池使用了 Android 的 support v4 包中的 Pool 相關的 API. 它被設計用來構建基於陣列的請求池,具體如何使用可以參考相關的文件和原始碼。

  public static <R> SingleRequest<R> obtain(/*各種引數*/) {
    SingleRequest<R> request = (SingleRequest<R>) POOL.acquire();
    if (request == null) {
      request = new SingleRequest<>();
    }
    request.init(/*各種引數*/);
    return request;
  }
複製程式碼

得到了請求之後會用 RequestManagertrack() 方法:

  void track(@NonNull Target<?> target, @NonNull Request request) {
    targetTracker.track(target);
    requestTracker.runRequest(request);
  }
複製程式碼

該方法的主要作用有兩個:

  1. 呼叫 TargetTrackertrack() 方法對對當前 Target 的生命週期進行管理;
  2. 呼叫 RequestTrackerrunRequest() 方法對當前請求進行管理,當 Glide 未處於暫停狀態的時候,會直接使用 Requestbegin() 方法開啟請求。

下面是 SingeleRequestbegin() 方法。它會根據當前載入的狀態來判斷應該呼叫哪個方法。因為我們之前圖片載入的過程可能因為一些意想不到的原因被終止,所以當重啟的時候就需要根據之前的狀態進行恢復。對於我們第一次載入的情況,則會直接進入到下方 1 處的 onSizeReady() 方法中:

  public void begin() {
    if (model == null) {
      if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
        width = overrideWidth;
        height = overrideHeight;
      }
      int logLevel = getFallbackDrawable() == null ? Log.WARN : Log.DEBUG;
      onLoadFailed(new GlideException("Received null model"), logLevel);
      return;
    }

    if (status == Status.RUNNING) {
      throw new IllegalArgumentException("Cannot restart a running request");
    }

    // 如果我們在完成之後重新啟動(通常通過諸如 notifyDataSetChanged() 之類的方法,
    // 在相同的目標或檢視中啟動相同的請求),我們可以使用我們上次檢索的資源和大小
    // 並跳過獲取新的大小。所以,如果你因為 View 大小發生了變化而想要重新載入圖片
    // 就需要在開始新載入之前清除檢視 (View) 或目標 (Target)。
    if (status == Status.COMPLETE) {
      onResourceReady(resource, DataSource.MEMORY_CACHE);
      return;
    }

    status = Status.WAITING_FOR_SIZE;
    if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
      onSizeReady(overrideWidth, overrideHeight); // 1
    } else {
      target.getSize(this);
    }

    if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)
        && canNotifyStatusChanged()) {
      target.onLoadStarted(getPlaceholderDrawable()); // 2
    }
  }
複製程式碼

下面是 onSizeReady() 方法,我們可以看出它會先判斷當前是否處於 Status.WAITING_FOR_SIZE 狀態,並隨後將狀態更改為 Status.RUNNING 並呼叫 engineload() 方法。顯然,更改完狀態之後繼續回到上面的方法,在 2 處即呼叫了 TargetonLoadStarted() 方法。這樣 Target 的第一個生命週期就被觸發了。

  public void onSizeReady(int width, int height) {
    if (status != Status.WAITING_FOR_SIZE) {
      return;
    }
    status = Status.RUNNING;

    float sizeMultiplier = requestOptions.getSizeMultiplier();
    this.width = maybeApplySizeMultiplier(width, sizeMultiplier);
    this.height = maybeApplySizeMultiplier(height, sizeMultiplier);

    loadStatus = engine.load(/*各種引數*/);

    if (status != Status.RUNNING) {
      loadStatus = null;
    }
  }
複製程式碼

然後,讓我們將重點放到 Engineload() 方法。該方法雖然不長,但是卻包含了許多重要的內容。我們在下篇文章中將要研究的 Glide 的快取就是在這裡實現的。該方法大致的邏輯上,先嚐試從記憶體快取當中查詢指定的資源,當記憶體中不存在的時候就準備使用 DecodeJob 來載入圖片。

  public <R> LoadStatus load(/*各種引數*/) {
    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
        resourceClass, transcodeClass, options);

    EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
    if (active != null) {
      cb.onResourceReady(active, DataSource.MEMORY_CACHE);
      return null;
    }

    EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
    if (cached != null) {
      cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
      return null;
    }

    EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
    if (current != null) {
      current.addCallback(cb);
      return new LoadStatus(cb, current);
    }

    EngineJob<R> engineJob = engineJobFactory.build(
            key,
            isMemoryCacheable,
            useUnlimitedSourceExecutorPool,
            useAnimationPool,
            onlyRetrieveFromCache);

    DecodeJob<R> decodeJob = decodeJobFactory.build(/*各種引數*/);

    jobs.put(key, engineJob);

    engineJob.addCallback(cb);
    engineJob.start(decodeJob);

    return new LoadStatus(cb, engineJob);
  }
複製程式碼

上面方法中涉及兩個類,一個是 DecodeJob、一個是 EngineJob。它們之間的關係是,EngineJob 內部維護了執行緒池,用來管理資源載入,已經當資源載入完畢的時候通知回撥。 DecodeJob 繼承了 Runnable,是執行緒池當中的一個任務。就像上面那樣,我們通過呼叫 engineJob.start(decodeJob) 來開始資源載入。

3.1.2 小結

階段1:開啟 DecodeJob 的過程

根據上文中的分析,我們不難得出上面的流程圖。不考慮快取的問題,這個部分的邏輯還是比較清晰的,即:當呼叫了 into() 之後,首先構建一個請求物件 SingleRequest,然後呼叫 RequestManagertrack() 方法對 RequestTarget 進行管理;隨後,使用 Requestbegin() 方法來啟動請求;該方法中會使用 Engineload() 方法決定是從快取當中獲取資源還是從資料來源中載入資料;如果是從資料來源中載入資料的話,就構建一個 DecodeJob 交給 EngineJob 來執行即可。

3.2 階段2:開啟網路流的過程

3.2.1 開啟網路流的過程

在上面的分析中,將 DecodeJob 交給 EngineJob 就完事了。因為 DecodeJob 是一個任務,會線上程池當中進行執行。所以,如果我們繼續追蹤的話,就應該從 DecodeJobrun() 方法開始:

所以,如果想要找到載入資源和解碼的邏輯,就應該檢視 DecodeJob 的 run() 方法。下面就是這個方法的定義:

  public void run() {
    DataFetcher<?> localFetcher = currentFetcher;
    try {
      if (isCancelled) {
        notifyFailed();
        return;
      }
      runWrapped();
    } catch (Throwable t) {
      if (stage != Stage.ENCODE) {
        throwables.add(t);
        notifyFailed();
      }
      if (!isCancelled) {
        throw t;
      }
    } finally {
      if (localFetcher != null) {
        localFetcher.cleanup();
      }
      GlideTrace.endSection();
    }
  }
複製程式碼

DecodeJob 的執行過程使用了狀態模式,它會根據當前的狀態決定將要執行的方法。在上面的方法中,噹噹前任務沒有被取消的話,會進入到 runWrapped() 方法。該方法中會使用 runReason 作為當前的狀態決定要執行的邏輯:

  private void runWrapped() {
    switch (runReason) {
      case INITIALIZE:
        stage = getNextStage(Stage.INITIALIZE);
        currentGenerator = getNextGenerator();
        runGenerators();
        break;
      case SWITCH_TO_SOURCE_SERVICE:
        runGenerators();
        break;
      case DECODE_DATA:
        decodeFromRetrievedData();
        break;
      default:
        throw new IllegalStateException("Unrecognized run reason: " + runReason);
    }
  }
複製程式碼

這裡的 runReason 是一個列舉型別,它包含的列舉值即為上面的三種型別。當我們在一個過程執行完畢之後會回撥 DecodeJob 中的方法修改 runReason,然後根據新的狀態值執行新的邏輯。

除了 runReasonDecodeJob 中還有一個變數 stage 也是用來決定 DecodeJob 狀態的變數。同樣,它也是一個列舉,用來表示將要載入資料的資料來源以及資料的載入狀態。它主要在載入資料的時候在 runGenerators()runWrapped()getNextStage() 三個方法中被修改。通常它的邏輯是,先從(大小、尺寸等)轉換之後的快取中拿資料,如果沒有的話再從沒有轉換過的快取中拿資料,最後還是拿不到的話就從原始的資料來源中載入資料。

以上就是 DecodeJob 中的狀態模式執行的原理。

對於一個新的任務,會在 DecodeJobinit() 方法中將 runReason 置為 INITIALIZE,所以,我們首先會進入到上述 switch 中的 INITIALIZE 中執行。然後,因為我們沒有設定過磁碟快取的策略,因此會使用預設的 AUTOMATIC 快取方式。於是,我們將會按照上面所說的依次從各個快取中拿資料。由於我們是第一次載入,並且暫時我們不考慮快取的問題,所以,最終資料的載入會交給 SourceGenerator 進行。

不知道你是否還記得上一篇文章中我們在講解在 Glide 中使用 OkHttp 時提到的相關的類。它們真正作用的地方就在下面的這個方法中。這是 SourceGeneratorstartNext() 方法,它會:

  1. 先使用 DecodeHelpergetLoadData() 方法從註冊的對映表中找出當前的圖片型別對應的 ModelLoader
  2. 然後使用它的 DataFetcherloadData() 方法從原始的資料來源中載入資料。
  public boolean startNext() {
    if (dataToCache != null) {
      Object data = dataToCache;
      dataToCache = null;
      cacheData(data);
    }

    if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {
      return true;
    }
    sourceCacheGenerator = null;

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      loadData = helper.getLoadData().get(loadDataListIndex++);
      if (loadData != null
          && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
          || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
        started = true;
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }
    return started;
  }
複製程式碼

由於我們的圖片時網路中的資源,在預設情況下會使用 Glide 內部的 HttpUrlFetcher 從網路中載入資料。其 loadData() 方法定義如下:

  public void loadData(Priority priority, DataCallback<? super InputStream> callback) {
    try {
      InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null, glideUrl.getHeaders());
      callback.onDataReady(result);
    } catch (IOException e) {
      callback.onLoadFailed(e);
    } finally {
    }
  }
複製程式碼

很明顯,這裡從網路中開啟輸入流之後得到了一個 InputStream 之後就使用回撥將其返回了。至於 loadDataWithRedirects() 方法的實現,就是使用 HttpURLConnection 開啟網路流的過程,這裡我們不進行詳細的說明了。

3.2.2 小結

階段2:開啟網路流的過程

這樣,into() 方法的第二個階段,即從網路中獲取一個輸入流的過程就分析完畢了。整個過程並不算複雜,主要是在 DecodeJob 中的狀態模式可能一開始看不太懂,還有就是其中涉及到的一些類不清楚其作用。如果你存在這兩個疑惑的話,那麼建議你:1).耐心思考下狀態模式的轉換過程;2).翻下上一篇文章瞭解自定義 Glide 圖片載入方式的幾個類的設計目的;3).最重要的,多看原始碼。

3.3 階段3:將輸入流轉換為 Drawable 的過程

3.3.1 轉換 Drawable 的過程

在上面的小節中我們已經開啟了網路流,按照 Android 自身提供的 BitmapFactory,我們可以很容易地從輸入流中得到 Drawable 不是?那麼為什麼這個轉換的過程還要單獨分為一個階段呢?

實際上,這裡的轉換過程並不比上面開啟輸入流的過程簡單多少。這是因為它涉及轉碼和將圖片轉換成適合控制元件大小的過程。好了,下面就讓我們來具體看一下這個過程都發生了什麼吧!

首先,從上面的 loadData(),我們可以看出當得到了輸入流之後會回撥 onDataReady() 方法。這個方法會一直從 HttpUrlFetcher 中一直回撥到 SourceGenerator 中。這裡它會使用預設的磁碟快取策略判斷資料是否可以快取,並決定對資料進行快取還是繼續回撥。

  public void onDataReady(Object data) {
    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
    if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
      dataToCache = data;
      cb.reschedule(); // 1
    } else {
      cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,
          loadData.fetcher.getDataSource(), originalKey);
    }
  }
複製程式碼

因為我們的資料是使用 HttpUrlFetcher 載入的,所以將會進入到 1 處繼續進行處理。此時,DecodeJob 將會根據當前的狀態從 run() 方法開始執行一遍,並再次呼叫 DataCacheGeneratorstartNext() 方法。但是,此次與上一次不同的地方在於,這次已經存在可以用於快取的資料了。所以,下面的方法將會被觸發:

  private void cacheData(Object dataToCache) {
    try {
      Encoder<Object> encoder = helper.getSourceEncoder(dataToCache);
      DataCacheWriter<Object> writer =
          new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());
      originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
      helper.getDiskCache().put(originalKey, writer);
    } finally {
      loadData.fetcher.cleanup();
    }

    sourceCacheGenerator =
        new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
  }
複製程式碼

這裡的主要邏輯是構建一個用於將資料快取到磁碟上面的 DataCacheGeneratorDataCacheGenerator 的流程基本與 SourceGenerator 一致,也就是根據資原始檔的型別找到 ModelLoader,然後使用 DataFetcher 載入快取的資源。與之前不同的是,這次是用 DataFecher 來載入 File 型別的資源。也就是說,當我們從網路中拿到了資料之後 Glide 會先將其快取到磁碟上面,然後再從磁碟上面讀取圖片並將其顯示到控制元件上面。所以,當從網路開啟了輸入流之後 SourceGenerator 的任務基本結束了,而後的顯示的任務都由 DataCacheGenerator 來完成。

HttpUrlFetcher 一樣,File 型別的資源將由 ByteBufferFetcher 來載入,當它載入完畢之後也也會回撥 onDataReady() 方法。此時,將會呼叫 DataCacheGeneratoronDataReady()

  public void onDataReady(Object data) {
    cb.onDataFetcherReady(sourceKey, data, loadData.fetcher, DataSource.DATA_DISK_CACHE, sourceKey);
  }
複製程式碼

該方法會繼續回撥到 DecodeJobonDataFetcherReady() 方法,後續的邏輯比較清晰,只是在不斷繼續呼叫方法,我們依次給出這些方法:

  // DecodeJob#onDataFetcherReady()
  public void onDataFetcherReady(Key sourceKey, Object data, DataFetcher<?> fetcher,
      DataSource dataSource, Key attemptedKey) {
    // ... 賦值,略
    if (Thread.currentThread() != currentThread) {
      runReason = RunReason.DECODE_DATA;
      callback.reschedule(this);
    } else {
      try {
        // decode 資料以得到期待的資源型別
        decodeFromRetrievedData();
      } finally {
        GlideTrace.endSection();
      }
    }
  }

  // DecodeJob#decodeFromRetrievedData()
  private void decodeFromRetrievedData() {
    Resource<R> resource = null;
    try {
      resource = decodeFromData(currentFetcher, currentData, currentDataSource);
    } catch (GlideException e) {
      // ... 異常處理
    }
    // ... 釋放資源和錯誤重試等
  }

  // DecodeJob#decodeFromData()
  private <Data> Resource<R> decodeFromData(DataFetcher<?> fetcher, Data data,
      DataSource dataSource) throws GlideException {
    try {
      // ... 略
      Resource<R> result = decodeFromFetcher(data, dataSource);
      return result;
    } finally {
      fetcher.cleanup();
    }
  }

  // DecodeJob#decodeFromFetcher()
  private <Data> Resource<R> decodeFromFetcher(Data data, DataSource dataSource)
      throws GlideException {
    LoadPath<Data, ?, R> path = decodeHelper.getLoadPath((Class<Data>) data.getClass());
    return runLoadPath(data, dataSource, path);
  }

  // DecodeJob#runLoadPath()
  private <Data, ResourceType> Resource<R> runLoadPath(Data data, DataSource dataSource,
      LoadPath<Data, ResourceType, R> path) throws GlideException {
    // ... 獲取引數資訊
    try {
      // 使用 LoadPath 繼續處理
      return path.load(
          rewinder, options, width, height, new DecodeCallback<ResourceType>(dataSource));
    } finally {
      rewinder.cleanup();
    }
  }

  // LoadPath#load()
  public Resource<Transcode> load(DataRewinder<Data> rewinder, @NonNull Options options, int width,
      int height, DecodePath.DecodeCallback<ResourceType> decodeCallback) throws GlideException {
    try {
      // 繼續載入
      return loadWithExceptionList(rewinder, options, width, height, decodeCallback, throwables);
    } finally {
      listPool.release(throwables);
    }
  }

  // LoadPath#loadWithExceptionList()
  private Resource<Transcode> loadWithExceptionList(/*各種引數*/) throws GlideException {
    Resource<Transcode> result = null;
    for (int i = 0, size = decodePaths.size(); i < size; i++) {
      DecodePath<Data, ResourceType, Transcode> path = decodePaths.get(i);
      try {
        // 使用 DecodePath 繼續處理
        result = path.decode(rewinder, width, height, options, decodeCallback);
      } catch (GlideException e) {
        exceptions.add(e);
      }
      if (result != null) {
        break;
      }
    }
    return result;
  }
複製程式碼

經過了上面的一系列猛如虎的操作之後,我們進入了 loadWithExceptionList() 方法,這裡會對 DecodePath 進行過濾,以得到我們期望的圖片的型別。這個方法中呼叫了 DecodePathdecode() 方法。這個方法比較重要,它像一個岔路口:1 處的程式碼是將資料轉換成我們期望的圖片的過程;2 處的程式碼是當得到了期望的圖片之後對處理繼續處理並顯示的過程。

  // DecodePath#decode()
  public Resource<Transcode> decode(DataRewinder<DataType> rewinder, int width, int height,
      Options options, DecodeCallback<ResourceType> callback) throws GlideException {
    Resource<ResourceType> decoded = decodeResource(rewinder, width, height, options); // 1
    Resource<ResourceType> transformed = callback.onResourceDecoded(decoded); // 2
    return transcoder.transcode(transformed, options);
  }
複製程式碼

然後,讓我們繼續沿著 decodeResource() 走。它會呼叫下面的這個迴圈對當前的資料型別和期望的、最終的圖片型別匹配從而決定用來繼續處理的 ResourceDecoder

  private Resource<ResourceType> decodeResourceWithList(/*各種引數*/) throws GlideException {
    Resource<ResourceType> result = null;
    for (int i = 0, size = decoders.size(); i < size; i++) {
      ResourceDecoder<DataType, ResourceType> decoder = decoders.get(i);
      try {
        DataType data = rewinder.rewindAndGet();
        if (decoder.handles(data, options)) {
          data = rewinder.rewindAndGet();
          result = decoder.decode(data, width, height, options);
        }
      } catch (IOException | RuntimeException | OutOfMemoryError e) {
        exceptions.add(e);
      }

      if (result != null) {
        break;
      }
    }
    return result;
  }
複製程式碼

ResourceDecoder 具有多個實現類,比如 BitmapDrawableDecoderByteBufferBitmapDecoder等。從名字也可以看出來是用來將一個型別轉換成另一個型別的。

在我們的程式中會使用 ByteBufferBitmapDecoder 來將 ByteBuffer 專成 Bitmap。它最終會在 DownsamplerdecodeStream() 方法中呼叫 BitmapFactorydecodeStream() 方法來從輸入流中得到 Bitmap。(我們的 ByteBufferByteBufferBitmapDecoder 中先被轉換成了輸入流。)

  private static Bitmap decodeStream(InputStream is, BitmapFactory.Options options,
      DecodeCallbacks callbacks, BitmapPool bitmapPool) throws IOException {
    // ... 略
    TransformationUtils.getBitmapDrawableLock().lock();
    try {
      result = BitmapFactory.decodeStream(is, null, options);
    } catch (IllegalArgumentException e) {
      // ... 錯誤處理,略
    } finally {
      TransformationUtils.getBitmapDrawableLock().unlock();
    }

    if (options.inJustDecodeBounds) {
      is.reset();
    }
    return result;
  }
複製程式碼

這樣剩下的就只有不斷繼續向上回撥或者返回,最終回到了我們上面所說的岔路口。這樣從輸入流中載入圖片的邏輯就結束了:)

3.3.2 小結

階段3:將輸入流轉換為 Drawable 的過程

怎麼樣,是不是覺得這個過程比開啟輸入流的過程複雜多了?畢竟這個部分涉及到了從快取當中取資料以及向快取寫資料的過程,算的上是核心部分了。整體而言,這部分的設計還是非常巧的,即使用了狀態模式,根據當前的狀態來決定下一個 Generator。從網路中拿到輸入流之後又使用 DataCacheGenerator 從快取當中讀取資料,這個過程連我第一次讀原始碼的時候都沒發現,以至於後來除錯驗證了推理之後才確信這部分是這樣設計的……

3.4 階段4:將 Drawable 展示到 ImageView 的過程

根據上面的分析,我們已經從網路中得到了圖片資料,並且已經將其放置到了快取中,又從快取當中取出資料進行準備進行顯示。上面的過程比較複雜,下面將要出場的這個階段也並不輕鬆……

3.4.1 最終展示圖片的過程

在上面分析中,我們已經進入到了之前所謂的岔路口,這裡我們再給出這個方法的定義如下。上面的分析到了程式碼 1 處,現在我們繼續從程式碼 2 處進行分析。

  // DecodePath#decode()
  public Resource<Transcode> decode(DataRewinder<DataType> rewinder, int width, int height,
      Options options, DecodeCallback<ResourceType> callback) throws GlideException {
    Resource<ResourceType> decoded = decodeResource(rewinder, width, height, options); // 1
    Resource<ResourceType> transformed = callback.onResourceDecoded(decoded); // 2
    return transcoder.transcode(transformed, options); // 3
  }
複製程式碼

這裡會呼叫 callback 的方法進行回撥,它最終會回撥到 DecodeJobonResourceDecoded() 方法。其主要的邏輯是根據我們設定的引數進行變化,也就是說,如果我們使用了 centerCrop 等引數,那麼這裡將會對其進行處理。這裡的 Transformation 是一個介面,它的一系列的實現都是對應於 scaleType 等引數的。

  <Z> Resource<Z> onResourceDecoded(DataSource dataSource, Resource<Z> decoded) {
    Class<Z> resourceSubClass = (Class<Z>) decoded.get().getClass();
    Transformation<Z> appliedTransformation = null;
    Resource<Z> transformed = decoded;
    // 對得到的圖片資源進行變換
    if (dataSource != DataSource.RESOURCE_DISK_CACHE) {
      appliedTransformation = decodeHelper.getTransformation(resourceSubClass);
      transformed = appliedTransformation.transform(glideContext, decoded, width, height);
    }
    if (!decoded.equals(transformed)) {
      decoded.recycle();
    }

    // ... 快取相關的邏輯,略
    return result;
  }
複製程式碼

在上面的方法中對圖形進行變換之後還會根據圖片的快取策略決定對圖片進行快取。然後這個方法就直接返回了我們變換之後的圖象。這樣我們就又回到了之前的岔路口。程式繼續執行就到了岔路口方法的第 3 行。這裡還會使用 BitmapDrawableTranscodertranscode() 方法返回 Resouces<BitmapDrawable>。只是這裡會使用 BitmapDrawableTranscoder 包裝一層,即做了延遲初始化處理。

這樣,當第 3 行方法也執行完畢,我們的岔路口方法就分析完了。然後就是不斷向上 return 進行返回。所以,我們又回到了 DecodeJobdecodeFromRetrievedData() 方法如下。這裡會進入到下面方法的 1 處來完成最終的圖片顯示操作。

  private void decodeFromRetrievedData() {
    Resource<R> resource = null;
    try {
      resource = decodeFromData(currentFetcher, currentData, currentDataSource);
    } catch (GlideException e) {
      throwables.add(e);
    }
    if (resource != null) {
      notifyEncodeAndRelease(resource, currentDataSource); // 1
    } else {
      runGenerators();
    }
  }
複製程式碼

接著程式會達到 DecodeJobonResourceReady() 方法如下。因為達到下面的方法的過程的邏輯比較簡單,我們就不貼出這部分的程式碼了。

  public void onResourceReady(Resource<R> resource, DataSource dataSource) {
    this.resource = resource;
    this.dataSource = dataSource;
    MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget();
  }
複製程式碼

這裡會獲取到一個訊息並將其傳送到 Handler 中進行處理。當 Handler 收到訊息之後會呼叫 EncodeJobhandleResultOnMainThread() 方法繼續處理:

  void handleResultOnMainThread() {
    // ... 略
    engineResource = engineResourceFactory.build(resource, isCacheable);
    hasResource = true;

    engineResource.acquire();
    listener.onEngineJobComplete(this, key, engineResource);

    for (int i = 0, size = cbs.size(); i < size; i++) {
      ResourceCallback cb = cbs.get(i);
      if (!isInIgnoredCallbacks(cb)) {
        engineResource.acquire();
        cb.onResourceReady(engineResource, dataSource); // 1
      }
    }
    engineResource.release();

    release(false /*isRemovedFromQueue*/);
  }
複製程式碼

經過一系列的判斷之後程式進入到程式碼 1 處,然後繼續進行回撥。這裡的 cb 就是 SingeleRequest

程式到了 SingleRequest 的方法中之後在下面的程式碼 1 處回撥 Target 的方法。而這裡的 Target 就是我們之前所說的 ImageViewTarget.

  private void onResourceReady(Resource<R> resource, R result, DataSource dataSource) {
    boolean isFirstResource = isFirstReadyResource();
    status = Status.COMPLETE;
    this.resource = resource;

    isCallingCallbacks = true;
    try {
      // ... 略

      if (!anyListenerHandledUpdatingTarget) {
        Transition<? super R> animation =
            animationFactory.build(dataSource, isFirstResource);
        target.onResourceReady(result, animation); // 1
      }
    } finally {
      isCallingCallbacks = false;
    }

    notifyLoadSuccess();
  }
複製程式碼

當程式到了 ImageViewTarget 之後會使用 setResource() 方法最終呼叫 ImageView 的方法將 Drawable 顯示到控制元件上面。

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

這樣,我們的 Glide 的載入過程就結束了。

3.4.2 小結

階段4:將 Drawable 展示到 ImageView 的過程

上面是我們將之前得到的 Drawable 顯示到控制元件上面的過程。這個方法包含了一定的邏輯,涉及的程式碼比較多,但是整體的邏輯比較簡單,所以這部分的篇幅並不長。

4、總結

以上的內容便是我們的 Glide 載入圖片的整個流程。從文章的篇幅和涉及的程式碼也可以看出,整個完整的過程是比較複雜的。從整體來看,Glide 之前啟動和最終顯示圖片的過程比較簡單、邏輯也比較清晰。最複雜的地方也是核心的地方在於 DecodeJob 的狀態切換。

上面的文章中,我們重點梳理圖片載入的整個流程,對於圖片快取和快取的圖片的載入的過程我沒有做過多的介紹。我們會在下一篇文章中專門來介紹這部分內容。

以上。

Glide 系列文章:

  1. Glide 系列-1:預熱、Glide 的常用配置方式及其原理
  2. Glide 系列-2:主流程原始碼分析(4.8.0)
  3. Glide 系列-3:Glide 快取的實現原理(4.8.0)

如果您喜歡我的文章,可以在以下平臺關注我:

更多文章:Gihub: Android-notes

相關文章