浅谈 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 的存储时机。具体见下面的源码简析

源码简析:

咱们先看看 ViewTarget.java 内部的处理 Request 的方法。

@Override
    public void setRequest(Request request) {
        setTag(request); // 里面调用的是 view.setTag 借助系统的 API 进行存储
    }
    @Override
    public Request getRequest() {
        Object tag = getTag(); // 里面调用的是 view.setTag 
        Request request = null;
        if (tag != null) {
            if (tag instanceof Request) {
                request = (Request) tag;
            } else {
                throw new IllegalArgumentException("You must not call setTag() on a view Glide is targeting");
            }
        }
        return request;
    }
    private void setTag(Object tag) {
        if (tagId == null) {
            isTagUsedAtLeastOnce = true;
            view.setTag(tag);
        } else {
            view.setTag(tagId, tag);
        }
    }
    private Object getTag() {
        if (tagId == null) {
            return view.getTag();
        } else {
            return view.getTag(tagId);
        }
    }
复制代码

上面已经能够明显看出,只要这个 View 传参入过 glide ,就会被 setTag,进行 request 的绑定。如今再来看看 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;
}

复制代码

全文终

相关文章
相关标签/搜索