您的位置:首页 > 移动开发

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

2018-03-06 15:35 651 查看

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

掘金: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
内存地址总是同一个。简单修改以下,打破上面两点任一一点,就能验证,例如下面的代码,我们不采用继承于
ViewTarger
Target
。而使用
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(...)

    方法的时候,总能找到上一次请求的
    Request
    Request
    Glide
    源码里面的一个接口,这里的缓存保存是保存的都是它的实现类。

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

源码简析:

Glide
into
方法,位于
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.java
clear()

@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 给缓存起来了。
}

当我们不使用

ViewTarget
Target
的时候,就不会有上面的流程,因为
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;
}

全文终

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: