android的Bitmap缓存策略全面剖析第二篇(之Glide缓存策略)

上一篇咱们讲了ImageLoader的缓存策略,那么Glide的缓存策略相较于ImageLoader缓存有什么优势吗?它们之间有什么样的差别,或者说性能方面,那一个更好,注意咱们这里所介绍的缓存是不包括sd卡存储的。

先来看一看Glide用了哪些缓存策略模式,如下:


是不是少了点,没错和缓存有关的就这两个类,MemoryCache是缓存的接口,MemoryCacheAdapter和LruResourceCache是实现类

public class MemoryCacheAdapter implements MemoryCache {

  private ResourceRemovedListener listener;

  @Override
  public int getCurrentSize() {
    return 0;
  }

  @Override
  public int getMaxSize() {
    return 0;
  }

  @Override
  public void setSizeMultiplier(float multiplier) {
    // Do nothing.
  }

  @Override
  public Resource remove(Key key) {
    return null;
  }

  @Override
  public Resource put(Key key, Resource resource) {
    listener.onResourceRemoved(resource);
    return null;
  }

  @Override
  public void setResourceRemovedListener(ResourceRemovedListener listener) {
    this.listener = listener;
  }

  @Override
  public void clearMemory() {
    // Do nothing.
  }

  @Override
  public void trimMemory(int level) {
    // Do nothing.
  }
}

但是MemoryCacheAdapter的实现方法里面并没有实现具体的业务,看名字是实现了适配器模式,目前这个类没用,是为了以后做扩展用的,先不管它。除了它就只剩LruResourceCache这个缓存策略类了,也是用了最少使用算法(经常被使用的对象在总的对象存储的内存超过阈值时被回收的概率低)。它也是和ImageLoader一样用了下面这个集合类,在此不再深究。

LinkedHashMap cache = new LinkedHashMap<>(100, 0.75f, true);

就这一个缓存策略,这样就完了吗,不不不,远没有想象的那么简单,Glide在缓存的Bitmap所占内存的总大小超过阈值的去除最少使用的Bitmap的时候,总是会回调下面这个方法进行Bitmap的回收保存,如下:

 protected void onItemEvicted(Key key, Resource item) {
    if (listener != null) {
      listener.onResourceRemoved(item);
    }
  }

为什么要保存一部分Bitmap?而不是直接放弃它,让它回收,这样的做法的好处就是避免不必要的内存申请和回收的时间上的浪费,如果内存申请和回收太频繁的话就会引起内存抖动,内存抖动在用户看来那就是界面已经在视觉上可以看出卡顿了有木有,这个是无法忍受的,这也是我们常说的以内存换速度的方式,合理的利用内存不是内存浪费,这也是Glide强于ImageLoader的一个点了。ok,来具体看一下它是怎么重利用Bitmap内存的。

这里注意一下,Glide缓存的所有资源都是Resource的实现类,Bitmap被它封装在内部的变量中,也是说Glide没有直接操作Bitmap,而是用了Resource这个包装类来操作Bitmap。所以说看回收的话,我们直接看com.bumptech.glide.load.resource.bitmap下的BitmapResource这个类就好了,回收的方法如下:

 public void recycle() {
    bitmapPool.put(bitmap);
  }
最终用的 com.bumptech.glide.load.engine.bitmap_recycle包下的 LruBitmapPool来进行回收的,继续看
 public synchronized void put(Bitmap bitmap) {
    if (bitmap == null) {
      throw new NullPointerException("Bitmap must not be null");
    }
    if (bitmap.isRecycled()) {
      throw new IllegalStateException("Cannot pool recycled bitmap");
    }
    if (!bitmap.isMutable() || strategy.getSize(bitmap) > maxSize
        || !allowedConfigs.contains(bitmap.getConfig())) {
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Reject bitmap from pool"
                + ", bitmap: " + strategy.logBitmap(bitmap)
                + ", is mutable: " + bitmap.isMutable()
                + ", is allowed config: " + allowedConfigs.contains(bitmap.getConfig()));
      }
      bitmap.recycle();
      return;
    }

    final int size = strategy.getSize(bitmap);
    strategy.put(bitmap);
    tracker.add(bitmap);

    puts++;
    currentSize += size;

    if (Log.isLoggable(TAG, Log.VERBOSE)) {
      Log.v(TAG, "Put bitmap in pool=" + strategy.logBitmap(bitmap));
    }
    dump();

    evict();
  }

这个方法的意思就是首先判断需要回收的Bitmap是否已经被回收了,它所占内存总大小是否超过最大值(这里的最大值是默认屏幕width*屏幕height*Argb_8888(也就是4)*4),也就是说最大值是你当前手机满屏幕像素所占内存大小的4倍,是否包括这个图片配置属性(默认包括android的Bitmap的所有的属性),如果这些都不满足的话将Bitmap缓存到回收集合中,如果满足一项,回收Bitmap所占内存,接下来判断所有在回收集合中的Bitmap总占内存是否超过阈值,如果超过的话就移除最少使用的回收,实现如下:

private synchronized void trimToSize(int size) {
    while (currentSize > size) {
      final Bitmap removed = strategy.removeLast();
      // TODO: This shouldn't ever happen, see #331.
      if (removed == null) {
        if (Log.isLoggable(TAG, Log.WARN)) {
          Log.w(TAG, "Size mismatch, resetting");
          dumpUnchecked();
        }
        currentSize = 0;
        return;
      }
      tracker.remove(removed);
      currentSize -= strategy.getSize(removed);
      evictions++;
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "Evicting bitmap=" + strategy.logBitmap(removed));
      }
      dump();
      removed.recycle();
    }
  }

接下来看一下获取的方法:

 public Bitmap get(int width, int height, Bitmap.Config config) {
    Bitmap result = getDirtyOrNull(width, height, config);
    if (result != null) {
      // Bitmaps in the pool contain random data that in some cases must be cleared for an image
      // to be rendered correctly. we shouldn't force all consumers to independently erase the
      // contents individually, so we do so here. See issue #131.
      result.eraseColor(Color.TRANSPARENT);
    } else {
      result = Bitmap.createBitmap(width, height, config);
    }

    return result;
  }
private synchronized Bitmap getDirtyOrNull(int width, int height, Bitmap.Config config) {
    // Config will be null for non public config types, which can lead to transformations naively
    // passing in null as the requested config here. See issue #194.
    final Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG);
    if (result == null) {
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "Missing bitmap=" + strategy.logBitmap(width, height, config));
      }
      misses++;
    } else {
      hits++;
      currentSize -= strategy.getSize(result);
      tracker.remove(result);
      normalize(result);
    }
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
      Log.v(TAG, "Get bitmap=" + strategy.logBitmap(width, height, config));
    }
    dump();

    return result;
  }

这两个方法的意思就是首先从回收池中获取,如果获取到Bitmap,则复用这个Bitmap,并把当前的bitmap的颜色值清空,如果没有的话,直接创建一个新的Bitmap返回,下面这个方法是用来判断这个回收池中有没有符合这个Bitmap大小的Bitmap的内存

 public Bitmap get(int width, int height, Bitmap.Config config) {
    final int size = Util.getBitmapByteSize(width, height, config);
    Key key = keyPool.get(size);

    Integer possibleSize = sortedSizes.ceilingKey(size);
    if (possibleSize != null && possibleSize != size && possibleSize <= size * MAX_SIZE_MULTIPLE) {
      keyPool.offer(key);
      key = keyPool.get(possibleSize);
    }

    // Do a get even if we know we don't have a bitmap so that the key moves to the front in the
    // lru pool
    final Bitmap result = groupedMap.get(key);
    if (result != null) {
      result.reconfigure(width, height, config);
      decrementBitmapOfSize(possibleSize);
    }

    return result;
  }

为什么要传宽和高还有config,因为你在复用Bitmap的时候,你必须要满足你当前要创建的Bitmap所占内存必须小于等于你缓冲池中的BitMap大小,并且格式也要保持一致,这样才能保证准确的复用。下面来看一下复用的方法

private static void applyMatrix(@NonNull Bitmap inBitmap, @NonNull Bitmap targetBitmap,
      Matrix matrix) {
    BITMAP_DRAWABLE_LOCK.lock();
    try {
      Canvas canvas = new Canvas(targetBitmap);
      canvas.drawBitmap(inBitmap, matrix, DEFAULT_PAINT);
      clear(canvas);
    } finally {
      BITMAP_DRAWABLE_LOCK.unlock();
    }
  }

这里的第一个参数是你需要显示的Bitmap,而第二参数是回收池里的Bitmap,很显然这个方法是把你需要显示的Bitmap通过回收池的方法将已有内存交给它,通俗的将就是将显示的Bitmap画在回收池里面Bitmap的内存上。这里可能你会问这不是还是创建了一个Bitmap吗?只是它创建了然后马上销毁了罢了。好,接下来重点来了,看一看它是怎么创建要显示的Bitmap的。

要显示的Bitmap的创建是在解码器里面创建的,这里只看流式解码器,位于com.bumptech.glide.load.resource.bitmap包下的StreamBitmapDecoder类decode方法,如下:

public Resource decode(InputStream source, int width, int height, Options options)
      throws IOException {

    // Use to fix the mark limit to avoid allocating buffers that fit entire images.
    final RecyclableBufferedInputStream bufferedStream;
    final boolean ownsBufferedStream;
    if (source instanceof RecyclableBufferedInputStream) {
      bufferedStream = (RecyclableBufferedInputStream) source;
      ownsBufferedStream = false;
    } else {
      bufferedStream = new RecyclableBufferedInputStream(source, byteArrayPool);
      ownsBufferedStream = true;
    }

    // Use to retrieve exceptions thrown while reading.
    // TODO(#126): when the framework no longer returns partially decoded Bitmaps or provides a
    // way to determine if a Bitmap is partially decoded, consider removing.
    ExceptionCatchingInputStream exceptionStream =
        ExceptionCatchingInputStream.obtain(bufferedStream);

    // Use to read data.
    // Ensures that we can always reset after reading an image header so that we can still
    // attempt to decode the full image even when the header decode fails and/or overflows our read
    // buffer. See #283.
    MarkEnforcingInputStream invalidatingStream = new MarkEnforcingInputStream(exceptionStream);
    UntrustedCallbacks callbacks = new UntrustedCallbacks(bufferedStream, exceptionStream);
    try {
      return downsampler.decode(invalidatingStream, width, height, options, callbacks);
    } finally {
      exceptionStream.release();
      if (ownsBufferedStream) {
        bufferedStream.release();
      }
    }
  }

然后最终是通过Downsampler这个类来完成创建显示的Bitmap的,如下方法:

public Resource decode(InputStream is, int requestedWidth, int requestedHeight,
      Options options, DecodeCallbacks callbacks) throws IOException {
    Preconditions.checkArgument(is.markSupported(), "You must provide an InputStream that supports"
        + " mark()");

    byte[] bytesForOptions = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
    BitmapFactory.Options bitmapFactoryOptions = getDefaultOptions();
    bitmapFactoryOptions.inTempStorage = bytesForOptions;

    DecodeFormat decodeFormat = options.get(DECODE_FORMAT);
    DownsampleStrategy downsampleStrategy = options.get(DOWNSAMPLE_STRATEGY);
    boolean fixBitmapToRequestedDimensions = options.get(FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS);

    try {
      Bitmap result = decodeFromWrappedStreams(is, bitmapFactoryOptions,
          downsampleStrategy, decodeFormat, requestedWidth, requestedHeight,
          fixBitmapToRequestedDimensions, callbacks);
      return BitmapResource.obtain(result, bitmapPool);
    } finally {
      releaseOptions(bitmapFactoryOptions);
      byteArrayPool.put(bytesForOptions, byte[].class);
    }
  }

这里注意 BitmapFactory.Options的inTempStorage 属性的用法,这个属性设置的话Bitmap的内存占用将保存在这个字节数组中,而看这句代码byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class),又是从回收池里获取的有木有,那么就算创建显示的Bitmap也是用了原来的内存占用,这样就少了很多的内存重新申请和销毁所损耗的时间,也就减少了卡顿所发生的概率。

ok,这就是Glide强于ImageLoader的缓存的策略了,有没有更好的策略呢,当然你可以把Bitmap占用内存改到native中,

Fresco就是这么搞的,但是android O又将Bitmap内存占用改到native中了,这的确比较蛋疼。






你可能感兴趣的:(Bitmap缓存策略全面剖析)