浅谈 Glide - BitmapPool 的存储时机 & 解答 ViewTarget 在同一View显示不同的图片时,总用同一个 Bitmap 引用的原因
作者:林冠宏 / 指尖下的幽灵
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
的源码简析。
-
ViewTarget
内部使用View.setTag
做了Request
的缓存保存。导致同一个View
多次传入into(...)
方法的时候,总能找到上一次请求的Request
。Request
是Glide
源码里面的一个接口,这里的缓存保存是保存的都是它的实现类。 -
glide
默认的加载形式中Target
都继承了ViewTarget
-
SimpleTarget
没有继承ViewTarget
-
glide
在每次请求开始的时候会去调用target.getRequest()
,如果获取的request
不为null
,那么它就会去释放上一个请求的一些资源,最后会调用到BitmapPool.put(Bitmap)
把上一次的Bitmap
缓存起来。如果request
获取的是 null,那么就不会缓存上一次加载成功的Bitmap
。 -
最后在加载图片并解码完成后,在从
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;
}
全文终
我的“区块链”技术书籍:《区块链以太坊DApp开发实战》
、支付宝收款码 https://www.cnblogs.com/linguanh/gallery/825997.html
微信:https://www.cnblogs.com/linguanh/gallery/image/321906.html
银行卡:6217007200076746554 , 林冠宏