浅谈 Glide - BitmapPool 的存储时机 & 解答 ViewTarget 在同一View显示不同的图片时,总用同一个 Bitmap 引用的原因

Posted 林冠宏 / 指尖下的幽灵 -- 现主要研发区块链应用:交易所

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈 Glide - BitmapPool 的存储时机 & 解答 ViewTarget 在同一View显示不同的图片时,总用同一个 Bitmap 引用的原因相关的知识,希望对你有一定的参考价值。

作者:林冠宏 / 指尖下的幽灵

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

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

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

腾讯云专栏: https://cloud.tencent.com/developer/user/1148436/activities


这两天在改造我的私人APP 非ROOT版微信自动回复, 使之可以多开的时候,碰到一个这样的问题。

Glide 在使用默认的Targer方式下,同一个 View 加载不同 URL 图片的时候,返回的 Bitmap 引用地址是一样的,但图片像素不一样。默认的 Target 有 : BitmapImageViewTarget.java,DrawableImageViewTarget.java

默认的方式代码如下:


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。

上面说的是存每个页面对应的 Bitmap,却没有去存 ImageView,你可能会问为什么?原因就是为了节省一个 ImageView 的内存,如果存 ImageView,它自然也携带了当前的 Bitmap 内存,以及它内部的其他变量的内存等。如果单独存 Bitmap,这样在APP中切换页面的时候,其实也就是切换数据,更新数据即可。

结合上面的语言来看,那么上面代码应该是没问题的。而事实上是有问题,因为同时具备了下面两点

  • 传参进来的 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;
}

全文终

以上是关于浅谈 Glide - BitmapPool 的存储时机 & 解答 ViewTarget 在同一View显示不同的图片时,总用同一个 Bitmap 引用的原因的主要内容,如果未能解决你的问题,请参考以下文章

Android Glide:无法加载 Firebase 存储的资源

如何使用 Glide 将图像保存到内部存储?

Glide 无法从内部存储加载文件

如何事先获取将存储在 Android/Java 缓存中的未来 Glide 图像大小?

Android Glide 和 Firebase 存储:将连续图像从 Firebase 存储加载到 Imageview - 闪烁

Glide 4.12.0 找不到 GeneratedAppGlideModule