淺談 Glide - BitmapPool 的存儲時機 & 解答 ViewTarget 在同一View顯示不一樣的圖片時,總用同一個 Bitmap 引用的緣由

做者:林冠宏 / 指尖下的幽靈java

掘金:https://juejin.im/user/587f0dfe128fe100570ce2d8git

博客:http://www.cnblogs.com/linguanh/github

GitHub : https://github.com/af913337456/緩存

騰訊雲專欄: https://cloud.tencent.com/developer/user/1148436/activities微信


這兩天在改造個人私人APP 非ROOT版微信自動回覆, 使之能夠多開的時候,碰到一個這樣的問題。app

Glide 在使用默認的Targer方式下,同一個 View 加載不一樣 URL 圖片的時候,返回的 Bitmap 引用地址是同樣的,但圖片像素不同。默認的 Target 有 : BitmapImageViewTarget.java,DrawableImageViewTarget.javaide

默認的方式代碼以下:函數

private Bitmap lastTimeQrCodeBitmap;

private void showQrCodeImage(final ImageView i){
    if(wechatCoreApi == null)
        return;
    Glide.with(context)
            .load("xxxxxxxxxxxxxxxxxxx")
            .asBitmap()
            .override(400,400)
            .skipMemoryCache(true)
            .listener(
                    new RequestListener<String, Bitmap>() {
                        @Override
                        public boolean onException(Exception e, String model, Target<Bitmap> target, boolean isFirstResource) {
                            return false;
                        }
    
                        @Override
                        public boolean onResourceReady(Bitmap resource, String model, Target<Bitmap> target, boolean isFromMemoryCache, boolean isFirstResource) {
                            if(resource != null){
                                // 這裏打印出加載回來的 Bitmap 的內存地址
                                LogUitls.e("resource ===> "+resource.toString());
                                lastTimeQrCodeBitmap = resource;
                            }
                            return false;
                        }
                    }
            )
            .into(i);
    }

很普通的一個函數,沒過多的操做,僅僅是在 onResourceReady 處作了加載回來的 Bitmap 的保存工做。之所要保存它,是由於這個APP要實現多開,每個頁面其對應的有一個二維碼圖片,每個二維碼圖片的 bitmap 是不一樣的,這樣在切換的時候,就能夠對應顯示出屬於當前頁面的 bitmap。post

上面說的是存每一個頁面對應的 Bitmap,卻沒有去存 ImageView,你可能會問爲何?緣由就是爲了節省一個 ImageView 的內存,若是存 ImageView,它天然也攜帶了當前的 Bitmap 內存,以及它內部的其餘變量的內存等。若是單獨存 Bitmap,這樣在APP中切換頁面的時候,其實也就是切換數據,更新數據便可。ui

結合上面的語言來看,那麼上面代碼應該是沒問題的。而事實上是有問題,由於同時具有了下面兩點

  • 傳參進來的 ImageView 老是同一個,即 into(ImageView)ImageView 老是同一個
  • 使用了默認的 into(ImageView) 函數,這個內部默認使用了BitmapImageViewTarget:

    BitmapImageViewTarget extends ImageViewTarget extends ViewTarget extends BaseTarget

這兩點就致使了,在 onResourceReady 返回的 resource 內存地址老是同一個。簡單修改如下,打破上面兩點任一一點,就能驗證,例以下面的代碼,咱們不採用繼承於 ViewTargerTarget。而使用 SimpleTarget extends BaseTarget

Glide.with(context)
        .load("xxxxxx")
        .asBitmap()
        .override(400,400)
        .skipMemoryCache(true)
        .listener(
                new RequestListener<String, Bitmap>() {
                    @Override
                    public boolean onException(Exception e, String model, Target<Bitmap> target, boolean isFirstResource) {
                        return false;
                    }

                    @Override
                    public boolean onResourceReady(Bitmap resource, String model, Target<Bitmap> target, boolean isFromMemoryCache, boolean isFirstResource) {
                        if(resource != null){
                            LogUitls.e("resource ===> "+resource.toString());
                            lastTimeQrCodeBitmap = resource;
                            i.setImageBitmap(lastTimeQrCodeBitmap); // 手動顯示
                        }
                        return false;
                    }
                }
        )
        .into(
                new SimpleTarget<Bitmap>() {
                    @Override
                    public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) {
                        // 這裏的 onResourceReady:resource 和上面的是同樣的
                    }
                }
        );

這個時候依然傳參是同一個 ImageView不會形成 onResourceReady 返回的 resource 內存地址老是同一個的狀況。

那麼究竟是什麼緣由致使了:

Glide 在知足下面兩點的時候,加載返回的 Bitmap 引用地址是同樣的,但圖片像素不同?

  • 傳參進來的 ImageView 老是同一個,即 into(ImageView)ImageView 老是同一個
  • 使用了默認的 into(ImageView) 函數,這個內部默認使用了BitmapImageViewTarget:

    BitmapImageViewTarget extends ImageViewTarget extends ViewTarget extends BaseTarget

爲了解答此問題,我在網上搜索了不少,幾乎不沾邊。後面經過分析源碼調試源碼找出調用鏈獲得以下的答案。

我先給出結論,下面再作基於 Glide 4.0 的源碼簡析。

  1. ViewTarget 內部使用 View.setTag 作了 Request 的緩存保存。致使同一個 View 屢次傳入 into(...)
    方法的時候,總能找到上一次請求的 RequestRequestGlide 源碼裏面的一個接口,這裏的緩存保存是保存的都是它的實現類。

  2. glide 默認的加載形式中 Target 都繼承了 ViewTarget

  3. SimpleTarget 沒有繼承 ViewTarget

  4. glide 在每次請求開始的時候會去調用 target.getRequest(),若是獲取的 request 不爲 null,那麼它就會去釋放上一個請求的一些資源,最後會調用到 BitmapPool.put(Bitmap) 把上一次的 Bitmap 緩存起來。若是 request 獲取的是 null,那麼就不會緩存上一次加載成功的 Bitmap
  5. 最後在加載圖片並解碼完成後,在從 BitmapPool 中尋找緩存的時候,就能找到上面的緩存的,擦除像素,加入新圖片的像素,最終返回 Bitmap

其中第4點就是 BitmapPool 的存儲時機。具體見下面的源碼簡析

源碼簡析:

Glideinto 方法,位於 RequestBuilder.java

private <Y extends Target<TranscodeType>> Y into(
      @NonNull Y target,
      @Nullable RequestListener<TranscodeType> targetListener,
      @NonNull RequestOptions options) 
{
    Util.assertMainThread();
    Preconditions.checkNotNull(target);
    if (!isModelSet) {
      throw new IllegalArgumentException("You must call #load() before calling #into()");
    }
    options = options.autoClone();
    Request request = buildRequest(target, targetListener, options);

    Request previous = target.getRequest();
    if (request.isEquivalentTo(previous) && !isSkipMemoryCacheWithCompletePreviousRequest(options, previous))
    {
        request.recycle();
        previous.begin();
      }
      return target;
    }
    requestManager.clear(target);  // 進入這裏
    target.setRequest(request);
    requestManager.track(target, request);

    return target;
}

進入 requestManager.clear(target); 裏面。位於 RequestManager.java

public void clear(@Nullable final Target<?> target) {
    if (target == null) {
      return;
    }
    if (Util.isOnMainThread()) {
      untrackOrDelegate(target); // 進入這裏 --- ①
    } else {
      mainHandler.post(new Runnable() {
        @Override
        public void run() {
          clear(target); // 若是是子線程調用 glide,那麼最終 post 了這個 msg 也是進入到上面 ① 處
        }
      });
    }
  }
  
  
private void untrackOrDelegate(@NonNull Target<?> target) {
    boolean isOwnedByUs = untrack(target); // 進入這裏
    if (!isOwnedByUs && !glide.removeFromManagers(target) && target.getRequest() != null) {
      Request request = target.getRequest();
      target.setRequest(null);
      request.clear();
    }
}

boolean untrack(@NonNull Target<?> target) {
    Request request = target.getRequest();
    if (request == null) {  // 對應結論中的第一點,若是是同一個 View,那麼它不爲 null
      return true;
    }
    if (requestTracker.clearRemoveAndRecycle(request)) { // 不爲 null,進入這裏的判斷
      targetTracker.untrack(target);
      target.setRequest(null);
      return true;
    } else {
      return false;
    }
}

進入到 clearRemoveAndRecycle,位於 RequestTracker.java

public boolean clearRemoveAndRecycle(@Nullable Request request) {
    return clearRemoveAndMaybeRecycle(request, /*isSafeToRecycle=*/ true);
}

private boolean clearRemoveAndMaybeRecycle(@Nullable Request request, boolean isSafeToRecycle) {
    if (request == null) {
      return true;
    }
    boolean isOwnedByUs = requests.remove(request); // 這裏的 remove 是會返回 true 的,由於這個 request 不是 null
    isOwnedByUs = pendingRequests.remove(request) || isOwnedByUs;
    if (isOwnedByUs) {
      request.clear(); // 最後進入這裏,這裏的 Request 的實現類是 SingleRequest
      if (isSafeToRecycle) {
        request.recycle();
      }
    }
    return isOwnedByUs;
}

進入 SingleRequest.javaclear()

@Override
public void clear() {
    Util.assertMainThread();
    assertNotCallingCallbacks();
    stateVerifier.throwIfRecycled();
    if (status == Status.CLEARED) {
      return;
    }
    cancel();
    if (resource != null) {
      releaseResource(resource); // 進入這裏
    }
    if (canNotifyCleared()) {
      target.onLoadCleared(getPlaceholderDrawable());
    }
    status = Status.CLEARED;
}

private void releaseResource(Resource<?> resource) {
    engine.release(resource);
    this.resource = null;
}

以後的流程還不少步,至關之複雜。它們最終會走到 BitmapResource.java 裏面的

@Override
public void recycle() {
    bitmapPool.put(bitmap); // 這裏就把上一次加載返回過的 bitmap 給緩存起來了。
}

當咱們不使用 ViewTargetTarget 的時候,就不會有上面的流程,由於 BaseTarget.java 內部的 getRequest 是 null,而 SimpleTarget extends BaseTarget,這也是爲何 SimpleTarget.java 可以達到每次請求返回的 Bitmap 內存地址不同的緣由。

BitmapPool.get 的時機。

Glide 加載圖片最後的解碼代碼在 Downsampler.java 裏面。它在裏面調用了 decodeFromWrappedStreams,並在 decodeStream 以前,調用了 setInBitmap,而 setInBitmap 內部就有這麼一行:

options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);

它從 bitmapPool 獲取擦除了像素的 Bitmap 對象。

private Bitmap decodeFromWrappedStreams(InputStream is,
      BitmapFactory.Options options, DownsampleStrategy downsampleStrategy,
      DecodeFormat decodeFormat, boolean isHardwareConfigAllowed, int requestedWidth,
      int requestedHeight, boolean fixBitmapToRequestedDimensions,
      DecodeCallbacks callbacks) throws IOException
{
    ....
    if ((options.inSampleSize == 1 || isKitKatOrGreater) && shouldUsePool(imageType))
    {
      ....
      if (expectedWidth > 0 && expectedHeight > 0) {
        // setInBitmap
        setInBitmap(options, bitmapPool, expectedWidth, expectedHeight);
      }
    }
    Bitmap downsampled = decodeStream(is, options, callbacks, bitmapPool);
    ...
    return rotated;
}

全文終

相關文章
相關標籤/搜索