Glide的内存缓存失效的原因之一

在Glide开启内存缓存和Disk缓存的时候,加载同一个图片url,在第一次加载成功的前提下,第二次可能会从Disk中取,无法从内存缓存中取,按经验这个情况不可能发生,但实际上在项目中出现了,列表一滑动,就会出现闪跳,分析是因为开了一个线程从硬盘读取并解码来获取图片.但为什么不是从内存缓存中读取.需要深入Glide的缓存原理来分析.

Glide 内部的加载入口在Engile.load()方法

 public <R> LoadStatus load(
      GlideContext glideContext,
      Object model,
      Key signature,
      int width,
      int height,
      Class<?> resourceClass,
      Class<R> transcodeClass,
      Priority priority,
      DiskCacheStrategy diskCacheStrategy,
      Map<Class<?>, Transformation<?>> transformations,
      boolean isTransformationRequired,
      boolean isScaleOnlyOrNoTransform,
      Options options,
      boolean isMemoryCacheable,
      boolean useUnlimitedSourceExecutorPool,
      boolean useAnimationPool,
      boolean onlyRetrieveFromCache,
      ResourceCallback cb,
      Executor callbackExecutor) {
    long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;

    EngineKey key =                        //EngineKey 貌似是一个缓存key
        keyFactory.buildKey(
            model,
            signature,
            width,
            height,
            transformations,
            resourceClass,
            transcodeClass,
            options);

    EngineResource<?> memoryResource;
    synchronized (this) {
      memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);  //从缓存加载的入口方法

      if (memoryResource == null) {
        return waitForExistingOrStartNewJob(
            glideContext,
            model,
            signature,
            width,
            height,
            resourceClass,
            transcodeClass,
            priority,
            diskCacheStrategy,
            transformations,
            isTransformationRequired,
            isScaleOnlyOrNoTransform,
            options,
            isMemoryCacheable,
            useUnlimitedSourceExecutorPool,
            useAnimationPool,
            onlyRetrieveFromCache,
            cb,
            callbackExecutor,
            key,
            startTime);
      }
    }

重点是EngineKey ,这是一个缓存的key对象,暂不分析.这个结合后面的流程来分析

断点进入loadFromMemory()->loadFromCache()->getEngineResourceFromCache()

 private EngineResource<?> getEngineResourceFromCache(Key key) {
    Resource<?> cached = cache.remove(key);

    final EngineResource<?> result;
    if (cached == null) {
      result = null;
    } else if (cached instanceof EngineResource) {
      // Save an object allocation if we've cached an EngineResource (the typical case).
      result = (EngineResource<?>) cached;
    } else {
      result =
          new EngineResource<>(
              cached, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ true, key, /*listener=*/ this);
    }
    return result;
  }

cache 的类型LruResourceCache,继承于LruCache,LruResourceCache并没有remove()方法,实际调用的是LruCache类的方法

public class LruCache<T, Y> {
  private final Map<T, Y> cache = new LinkedHashMap<>(100, 0.75f, true);
  private final long initialMaxSize;
  private long maxSize;
  private long currentSize;

..... 

  public synchronized Y remove(@NonNull T key) {
    final Y value = cache.remove(key);
    if (value != null) {
      currentSize -= getSize(value);
    }
    return value;
  }
}

在remove方法中的cache类是LinkedHashMap,断点得知,T key,这个T的类型正好是EngineKey,我们知道LinkedHashMap或者HashMap寻址返回元素是和key的hashCode以及equals()方法有关,两个key的hashcode一样,equals返回true,才会认定是同一个key,并返回已经存在的value,那就接着看下EngineKey的hashCode()和equals()方法是怎么定义的.

class EngineKey implements Key {
  private final Object model;
  private final int width;
  private final int height;
  private final Class<?> resourceClass;
  private final Class<?> transcodeClass;
  private final Key signature;
  private final Map<Class<?>, Transformation<?>> transformations;
  private final Options options;
  private int hashCode;

  EngineKey(
      Object model,
      Key signature,
      int width,
      int height,
      Map<Class<?>, Transformation<?>> transformations,
      Class<?> resourceClass,
      Class<?> transcodeClass,
      Options options) {
    this.model = Preconditions.checkNotNull(model);
    this.signature = Preconditions.checkNotNull(signature, "Signature must not be null");
    this.width = width;
    this.height = height;
    this.transformations = Preconditions.checkNotNull(transformations);
    this.resourceClass =
        Preconditions.checkNotNull(resourceClass, "Resource class must not be null");
    this.transcodeClass =
        Preconditions.checkNotNull(transcodeClass, "Transcode class must not be null");
    this.options = Preconditions.checkNotNull(options);
  }

  @Override
  public boolean equals(Object o) {
    if (o instanceof EngineKey) {
      EngineKey other = (EngineKey) o;
      return model.equals(other.model)
          && signature.equals(other.signature)
          && height == other.height
          && width == other.width
          && transformations.equals(other.transformations)
          && resourceClass.equals(other.resourceClass)
          && transcodeClass.equals(other.transcodeClass)
          && options.equals(other.options);
    }
    return false;
  }

  @Override
  public int hashCode() {
    if (hashCode == 0) {
      hashCode = model.hashCode();//1
      hashCode = 31 * hashCode + signature.hashCode();//2
      hashCode = 31 * hashCode + width;//3
      hashCode = 31 * hashCode + height;//4
      hashCode = 31 * hashCode + transformations.hashCode();//5
      hashCode = 31 * hashCode + resourceClass.hashCode();//6
      hashCode = 31 * hashCode + transcodeClass.hashCode();//7
      hashCode = 31 * hashCode + options.hashCode();//8
    }
    return hashCode;
  }

  @Override
  public String toString() {
    return "EngineKey{"
        + "model="
        + model
        + ", width="
        + width
        + ", height="
        + height
        + ", resourceClass="
        + resourceClass
        + ", transcodeClass="
        + transcodeClass
        + ", signature="
        + signature
        + ", hashCode="
        + hashCode
        + ", transformations="
        + transformations
        + ", options="
        + options
        + '}';
  }

  @Override
  public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
    throw new UnsupportedOperationException();
  }

看到这里就大概明白了,之所以没有命中缓存,估计因为EngineKey返回的hashCode在即使url一样的情况下,最终计算得到的值也不一样.让我惊讶的是,影响EnigneKey的hashCode计算这么复杂,竟然有8个因素,url只是其中一个因子.有一个不一样,缓存就无法命中了.这只是推测,实际调试先记录第一次加载每一步的hashCode值.然后断点调试

发现在第二次执行

hashCode = 31 * hashCode + transformations.hashCode()

这句代码之后,hashCode变的和第一次的不一样,那问题就出在了transformations.hashCode()的计算上.后面几步的计算就不用看了,肯定不一样。transformations是什么对象呢,8个因子分别是什么呢.一一分下下,这8个因子分别是什么.

1.model 这个对象的来源是Glide.load(obj)传入的obj,可以是String类型,可以是Url类型,也可以是GlideUrl或者继承GlideUrl的类.

2.signature这个对象的来源是BaseRequestionOptions,RequestOptions的父类,默认类型是Emptysignature,这个加载图片来说不常设置,一般默认就是Emptysignature

3.width:view的宽度

4.height:view的高度

5.transformations的实际类型是:CachedHashCodeArrayMap,也是来自BaseRequestionOptions,存储的是transformation对象,比如圆角transformation或者圆形的transformation,这个来源路径的伪码如下:

Glide.with(activity).apply(RequestOptions().transform(GlideCircleTransformation()))
 RequestOptins.transform(@NonNull Transformation<Bitmap> transformation, boolean isRequired)
@NonNull
  T transform(@NonNull Transformation<Bitmap> transformation, boolean isRequired) {
    if (isAutoCloneEnabled) {
      return clone().transform(transformation, isRequired);
    }

   //可以看到外部传入的transformation,会被包装成DrawableTransformation,GifDrawableTransformation等.

    DrawableTransformation drawableTransformation =
        new DrawableTransformation(transformation, isRequired);

   //之后transform()其实就是相当于往transformations这个对象put数据,一共4个,
   //1 key:Bitmap value:transformation
   //2 key:Drawable.class value:drawableTransformation
   //3 key:BitmapDrawable value:drawableTransformation.asBitmapDrawable()
   //4 key:GifDrawable value:GifDrawableTransformation()

    transform(Bitmap.class, transformation, isRequired);
    transform(Drawable.class, drawableTransformation, isRequired);
    // TODO: remove BitmapDrawable decoder and this transformation.
    // Registering as BitmapDrawable is simply an optimization to avoid some iteration and
    // isAssignableFrom checks when obtaining the transformation later on. It can be removed without
    // affecting the functionality.
    transform(BitmapDrawable.class, drawableTransformation.asBitmapDrawable(), isRequired);
    transform(GifDrawable.class, new GifDrawableTransformation(transformation), isRequired);


    return selfOrThrowIfLocked();
  }

  @NonNull
  <Y> T transform(
      @NonNull Class<Y> resourceClass,
      @NonNull Transformation<Y> transformation,
      boolean isRequired) {
    if (isAutoCloneEnabled) {
      return clone().transform(resourceClass, transformation, isRequired);
    }

    Preconditions.checkNotNull(resourceClass);
    Preconditions.checkNotNull(transformation);
    transformations.put(resourceClass, transformation); 
    fields |= TRANSFORMATION;
    isTransformationAllowed = true;
    fields |= TRANSFORMATION_ALLOWED;
    // Always set to false here. Known scale only transformations will call this method and then
    // set isScaleOnlyOrNoTransform to true immediately after.
    isScaleOnlyOrNoTransform = false;
    if (isRequired) {
      fields |= TRANSFORMATION_REQUIRED;
      isTransformationRequired = true;
    }
    return selfOrThrowIfLocked();
  }

  transformations这个map的数据,一共4个,
   //1 key:Bitmap.class value:transformation(我们传入的transformation)
   //2 key:Drawable.class value:drawableTransformation(包装了transformation)
   //3 key:BitmapDrawable.class value:drawableTransformation.asBitmapDrawable()
   //4 key:GifDrawable.class value:GifDrawableTransformation() (包装了transformation)

然后transformations.hashCode()的方法如下

//CachedHashCodeArrayMap
 @Override
  public int hashCode() {
    if (hashCode == 0) {
      hashCode = super.hashCode();
    }
    return hashCode;
  }


//SimpleArrayMap
  @Override
    public int hashCode() {
        final int[] hashes = mHashes;
        final Object[] array = mArray;
        int result = 0;
        for (int i = 0, v = 1, s = mSize; i < s; i++, v+=2) {
            Object value = array[v];
            result += hashes[i] ^ (value == null ? 0 : value.hashCode());
        }
        return result;
    }

计算方法遍历map,将key的hash和value的hash进行按位与得出的值累加(为什么hashes数组的元素是key的hash值,这个可以去看ArrayMap的原理),得到最终的hashcode,我们分析了这个map有4条数据,key其实是Class类型的对象,所以每个类型在内存中有唯一值,影响map的hashcode的值是value的hash,分析下这4个value

1. value:transformation,我们传入的transformation,以GlideCircleTransformation为例,就是GlideCircleTransformation对象的hashCode,如果没重写,则返回系统分配的hashcode值

2.value:drawableTransformation,

@Override
public int hashCode() {
  return wrapped.hashCode();
}

wapped就是我们的transformation,返回的也是transformation的hashcode值

3 value:drawableTransformation.asBitmapDrawable()

public Transformation<BitmapDrawable> asBitmapDrawable() {
  return (Transformation<BitmapDrawable>) (Transformation<?>) this;
}

this指代的还是drawableTransformation本身,所以返回的hashcode也是transformation的hashcode值

4  value:GifDrawableTransformation(),

.

@Override
public int hashCode() {
  return wrapped.hashCode();
}

wapped也是我们传入的transformation,返回的也是transformation的hashcode值

综上分析,所以transformations这个map的hashcode取决于我们传入的transformation的hashcode值.

6.分析了第5个因子,接着分析第6个因子reourceClass,它的来源也是BaseRequestionOptions,默认是Object.class

7.transcodeClass也是个Class的对象,来自于Glide.as***方法,可以是Bitmap,Drawable,File等类型

8.options来自于BaseRequestionOptions的,需要调用set方法设置.

Glide.with(activity).asDrawable().apply(RequestOptions().set("EXTRA", extra)
//BaseRequestOptions
​​​​​​​@NonNull
@CheckResult
public <Y> T set(@NonNull Option<Y> option, @NonNull Y value) {
  if (isAutoCloneEnabled) {
    return clone().set(option, value);
  }

  Preconditions.checkNotNull(option);
  Preconditions.checkNotNull(value);
  options.set(option, value);
  return selfOrThrowIfLocked();
}

options的类型Options,这个对象的hashcode值取决于value的hashcode,按照给出的案例就是取决于extra对象的hashcode值

@Override
public int hashCode() {
  return values.hashCode();
}

分析了8个因子,我们总结一下可变因子是7个分别是model,widht,height,transformation,transcodeClass,options,.另外一个signature,可以忽略.

如果对同一个url,而且对Glide加载代码一样的的话,影响的就只有transformation,options.

所以主要对同一个url加载,而且Glide加载代码一致,就是关注transformation,options,这两个因子都是和对应对象返回的hashCode方法有关。那么有两种情况.其中一种假如我们设置transformation,options的value,并没有实现hashcode方法,那么系统就根据对象分配的hashcode来指定.每次加载因为分配的对象是重新构造的,所以EngineKey的hashcode的值不一样,导致一个现象:加载同一个url,EngineKey返回的hashCode并不一致,缓存无法命中,只能从硬盘缓存中取,硬盘根据图片的地址来缓存,没有那么复杂的key的计算.另外一种,如果我们实现了hashcode方法,保证每次加载两个因子的hashcode值一样,那么同一个url,EngineKey返回的hashCode也一样,缓存就能命中了.这样就回答了开头提出的问题.解决方案就是重写transformation,options.value各自类的hashCode方法.比如

GlideRoundTransformation{  
 private static final String ID = "com.videogo.glide.transformation.GlideRoundTransformation";
...
 @Override
    public boolean equals(Object o) {
       return o instanceof GlideRoundTransformation;
    }

    public int hashCode() {
        return ID.hashCode();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值