Glide 系列四:Glide 的網路模組到底在哪裡?

語言: CN / TW / HK

Glide 系列四:Glide 的網路模組到底在哪裡?

本文概述:

  • 傳言

    • 曾有一位開發者花費一週時間,找不到Glide 的網路模組在哪裡;
    • Glide 相較於RxJava,其整體設計思路並不複雜,但是使用了大量的介面,導致程式碼非常繞;而RxJava 程式碼較為簡單,但是設計思路非常複雜;
  • 本文以Glide 原始碼流程詳細分析了,網路模組到底在哪裡;

Glide 使用

  • 基本使用

Glide.with(this).load(URL).into(imageView);

  • 拆分使用過程

RequestManager requstManger = Glide.with(this);  RequestBuilder requestBuilder = requstManger.load(URL);  requestBuilder.into(imageView);

使用Glide ,出現記憶體洩漏,是為了什麼?

  • 作用域問題:

    • 儘量在with的時候,傳入有生命週期的作用域(非Application作用域),儘量避免使用了Application作用域,因為Application作用域不會對頁面繫結生命週期機制,就回收不及時釋放操作等....

原始碼分析:

Glide.with:

  • 最終返回RequestManager 物件

    • 關鍵程式碼:

    public RequestManager get(@NonNull FragmentActivity activity) {      if (Util.isOnBackgroundThread()) {          // Application 作用域     } else {          // 非Application 作用域     }  }

requestManager.load(URL )

  • 最終返回RequestBuilder

ImageView 是怎麼顯示的?

requestBuilder.into(ImageView)

核心問題:

  • 知道圖片是怎麼在imageView 控制元件上顯示的?

整體流程分析:Glide 的網路訪問模組在哪裡?

編寫說明:

  • 按照into 原始碼執行順序,分為主線與支線
  • 跳過了許多細節,因為Glide 的原始碼實在是太深了;

在MainActivity 中

  • 從requestBuilder.into(imageView); 通過into,進入RequestBuilder.into()

在RequestBuilder.into()

  • 函式簽名展示:

    @NonNull  public ViewTarget<ImageView, TranscodeType> into(@NonNull ImageView view) {

  • 檢查是否為主執行緒,控制元件是否為空 [支線]

    Util.assertMainThread();  Preconditions.checkNotNull(view);

  • 根據控制元件屬性(scaleType)定義,決定requestOptions 走哪裡去 [支線]

    • 使用Switch-case 實現,沒有設定控制元件的scaleType 屬性,走預設流程:case FIT_END
    • 並且在這段程式碼中使用了clone() 操作,這是原型設計模式

    switch (view.getScaleType()) {          case FIT_END://預設流程            requestOptions = requestOptions.clone().optionalFitCenter();            break;

  • 構建ImageViewTarget (顯示圖片) [主線]

    • 後續原始碼一定會回撥到這個地方 ,伏筆一

      • 因為這個ImageViewTarget 是用來顯示圖片的 ;
    • 程式碼展示

      //構建出ImageViewTarget  return into(          glideContext.buildImageViewTarget(view, transcodeClass),   ) ;

  • 從return into,通過into 再次進入RequestBuilder.into()

在RequestBuilder.into()

  • 函式簽名展示

    private <Y extends Target<TranscodeType>> Y into(

  • 存在Request 介面:為了擴充套件性,需要面向於高層 [主線]

    • Request 介面的子類:SingleRequest

    //原始碼:  Request request = buildRequest(target, targetListener, options, callbackExecutor);  //介面  public interface Request {  //伏筆二:  Request request = new SingleRequest

  • 處理上一個請求 [支線]

    • 當新的請求來了,會去處理上一個請求

    image-20220820153925104

  • 從requestManager.track(target, request); 通過track 進入RequestManager.track()

在RequestManager.track()

  • 函式簽名展示:

    synchronized void track(@NonNull Target<?> target, @NonNull Request request) {

  • 從requestTracker.runRequest(request); 通過runRequest 進入RequestTracker.runRequest

在RequestTracker.runRequest

  • 函式簽名展示:

    public void runRequest(@NonNull Request request) {

  • 將當前請求新增到執行佇列中: [主線]

    requests.add(request);

  • 當暫停標誌位為false ,執行請求 [主線]

    if (!isPaused) {        request.begin();     }

    • 通過begin 進入SingleRequest.begin()

      image-20220820155630125

  • 當暫停標誌位為true ,將請求新增到等待佇列中 [主線]

    else {        request.clear();        if (Log.isLoggable(TAG, Log.VERBOSE)) {          Log.v(TAG, "Paused, delaying request");       }        pendingRequests.add(request);     }

在SingleRequest.begin()

  • 函式簽名展示:

    @Override  public void begin() {

    • 因為我們在之前分析過了,Request 的子類為SingleRequest 所以可以大膽向下走
    • 注意:在閱讀原始碼時,一定要去埋下伏筆,不然原始碼根本不好找,遇到介面實現類不清楚,那麼就往前走
  • begin() 中的關鍵函式:當用戶沒有設定寬高時,會去測量寬高並回調到此處

image-20220820160024447

  • 從onSizeReady(overrideWidth, overrideHeight); 通過onSizeReady 進入SingleRequest.onSizeReady()

在SingleRequest.onSizeReady()

  • 函式簽名展示:

    @Override  public void onSizeReady(int width, int height) {

  • 加了鎖的:多執行緒併發

    private final Object requestLock;  synchronized (requestLock) {

  • 通過引擎載入:

    loadStatus =            engine.load(

  • 從engine.load( 通過load 進入Engine.load()

在Engine.load() [主線]

  • 存在Enginekey

    EngineKey key =          keyFactory.buildKey(

  • 存在快取機制

    EngineResource<?> memoryResource;  synchronized (this) {      memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);

    • 補充圖片訪問流程:命中快取後直接返回

      1. 優先查詢活動快取(執行時快取)
      2. 再查詢記憶體快取(執行時快取)
      3. 再查詢磁碟快取
    • 訪問依據,就是這個key

    • 這個key 是怎麼構造的?

      • 根據圖片的寬、高、簽名為每一張圖片建立一把唯一的key
      • 構造程式碼

      EngineKey key =      keyFactory.buildKey(      model,      signature,      width,      height,      transformations,      resourceClass,      transcodeClass,      options);

  • 通過loadFromMemory 進入Engine.loadFromMemory()

Engine.loadFromMemory() 快取機制

  • 函式簽名:

    @Nullable  private EngineResource<?> loadFromMemory(

  • 優先查詢活動快取(執行時一級快取):若命中,則直接返回

    EngineResource<?> active = loadFromActiveResources(key);  if (active != null) {      if (VERBOSE_IS_LOGGABLE) {          logWithTimeAndKey("Loaded resource from active resources", startTime, key);     }      return active;  }

  • 再查詢記憶體快取(執行時二級快取):若命中,則直接返回

    EngineResource<?> cached = loadFromCache(key);      if (cached != null) {        if (VERBOSE_IS_LOGGABLE) {          logWithTimeAndKey("Loaded resource from cache", startTime, key);       }        return cached;     }

  • 備註:

    • 當任一執行時快取命中時,會回退至Engine.load() 執行

      //命中快取,回撥給外界去顯示圖片  cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);

    • 當執行時快取無一命中,返回null

      return null;

      • 此時在Engine.load() 中則會執行waitForExistingOrStartNewJob

        if (memoryResource == null) {          return waitForExistingOrStartNewJob(

      • 通過waitForExistingOrStartNewJob 進入Engine.waitForExistingOrStartNewJob

  • 為什麼要設計兩級執行時快取

    • 記憶體快取由LRUcache 管理

    • 業務場景:maxSize = 3

      • 當新增資料時,超過maxSize ,就會移除掉記憶體快取中最長未使用的圖片,如果這張圖片正在被Activity 使用,就會出現崩潰;
    • 為了解決這個崩潰:設計了活動快取

      • 將所有正在顯示的圖片,放到活動快取中;也就是Activity 真正顯示的圖片必須訪問活動快取

在Engine.waitForExistingOrStartNewJob()

  • 查詢有無正在執行的任務

    EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);  }

    • EngineJob :執行緒池大管家,在Glide 4.11 後使用工廠設計模式構建

      image-20220820163713756

    • decodeJob :代表具體任務(Runnable ),在Glide 4.11 後使用工廠設計模式構建

      image-20220820163742875

  • 將decodeJob (具體任務)交給engineJob (執行緒池)執行

    engineJob.start(decodeJob);

    • 具體執行邏輯

      public synchronized void start(DecodeJob<R> decodeJob) {      this.decodeJob = decodeJob;      GlideExecutor executor =          decodeJob.willDecodeFromCache() ? diskCacheExecutor : getActiveSourceExecutor();      executor.execute(decodeJob);  }

  • decodeJob (具體任務) 具體是怎麼執行的?

    • 通過DecodeJob 跳轉至DecodeJob.run()

在DecodeJob.run()

  • 程式碼展示:

    public void run() {   ……        runWrapped();      ……  }

  • 進入DecodeJob.runWrapped();

在DecodeJob.runWrapped();

  • 函式簽名展示:

    private void runWrapped() {

  • 拿到快取策略機制:通過switch-case

    • 沒有配置就走預設

      switch (runReason) {      case INITIALIZE:          stage = getNextStage(Stage.INITIALIZE);

      • 預設快取策略機制

      currentGenerator = getNextGenerator();  //返回這個:伏筆二  new SourceGenerator(decodeHelper, this);

      • 這樣走的

        private DataFetcherGenerator getNextGenerator() {      switch (stage) {         ……         case SOURCE://走這個              return new SourceGenerator(decodeHelper, this);

    • 執行獲取到的快取策略:runGenerators(); 進入DecodeJob.runGenerators();

在DecodeJob.runGenerators();

  • 函式簽名:

    private void runGenerators() {

  • 注意,此時我們走的是預設的快取流程

    while (!isCancelled          && currentGenerator != null          && !(isStarted = currentGenerator.startNext())) {

    • currentGenerator 指的是SourceGenerator ,因為startNext 是介面

      image-20220820165627647

  • 此時進入SourceGenerator.startNext()

在SourceGenerator.startNext()

  • 關鍵程式碼:helper.getLoadData()

    while (!started && hasNextModelLoader()) {      loadData = helper.getLoadData().get(loadDataListIndex++);

  • 通過getLoadData() 進入DecodeHelper.getLoadData()

在DecodeHelper.getLoadData()

  • 結論:最終返回根據使用者傳入引數,拿到的真實物件

    • 比如說,我們傳入URL ,那麼getLoadData 拿到HttpUrlFetcher 物件
  • 關鍵程式碼:modelLoader.buildLoadData

    LoadData<?> current = modelLoader.buildLoadData(model, width, height, options);

  • 通過buildLoadData 進入ModelLoader 介面的buildLoadData 介面

    • 介面展示:歇逼了,找不到實現類啊,

      image-20220820170208781

  • 這個實現類在哪裡:在Glide 建構函式中,有個註冊機,裡面就給我設定好了的

//註冊機:Glide.java 中的  .append(GlideUrl.class, InputStream.class, new HttpGlideUrlLoader.Factory())  //我們在使用Glide 時,第二個引數是String 型別的URL ,註冊機幫助進行了適配

  • 有了註冊機,那麼就找到上面那個介面的實現類了

    image-20220820170946055

  • 註冊機展示:第一個引數為使用者傳入的實參(就是我們自己的寫入的URL 之類的),在執行的時候是通過第三個引數,去構造第二個引數型別的資料流(註冊機的本質)

    image-20220820171441488

  • 進入HttpGlideUrlLoader.buildLoadData()

在HttpGlideUrlLoader.buildLoadData()

  • 關鍵程式碼:Glide 的網路訪問模組

    return new LoadData<>(url, new HttpUrlFetcher(url, timeout));

  • 總結:

    • HttpGlideUrlLoader :只是裝飾物件
    • HttpUrlFetcher :才是真實物件
  • 此時,向上回退至SourceGenerator.startNext()

回退至SourceGenerator.startNext()

  • 此時拿到了HttpUrlFetcher 物件

    loadData = helper.getLoadData().get(loadDataListIndex++);

  • 將拿到的HttpUrlFetcher 作為引數傳入startNextLoad(loadData);

    • 此時進入SourceGenerator.startNextLoad();

在SourceGenerator.startNextLoad();

  • 準備執行

    private void startNextLoad(final LoadData<?> toStart) {      loadData.fetcher.loadData(

  • 進入loadData :發現這個居然又是介面!!!

    • 幸好知道它的實現類是HttpUrlFetcher

    image-20220820172507552

  • 此時,進入HttpUrlFetcher.loadData()

在HttpUrlFetcher.loadData()

  • 關鍵程式碼:

    InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null, glideUrl.getHeaders());

  • 通過loadDataWithRedirects 進入HttpUrlFetcher.loadDataWithRedirects ()

在HttpUrlFetcher.loadDataWithRedirects ()

  • 這裡就是Glide 的網路訪問部分了,最終是會返回InputStream

    image-20220820172926573

  • 總結:Glide 的原始碼十分繞,RxJava 是編寫思路繞,程式碼較為簡單;而Glide 構建思路比如輕鬆,但是程式碼邏輯是非繞;

使用Glide為什麼要加入網路許可權?

  • 等待佇列/執行佇列
  • 執行Request ---> 活動快取 --->記憶體快取 ---> jobs.get檢測執行的任務有沒有執行完成 ---> HttpUrlFetcher.HttpURLConnection