Glide(四):强大的图片缓存池和复用机制详解手机开发

Glide有一个很强大的Glide缓存策略和Glide复用机制,之前在里面栽过跟头,借此好好整理总结。


Glide缓存策略

Glide使用了类似三级缓存策略,分别是弱引用缓存、LruCache缓存、DiskLruCache缓存和网络加载。

默认情况下,Glide 会在开始一个新的图片请求之前检查以下多级的缓存:
活动资源 (Active Resources) – 现在是否有另一个 View 正在展示这张图片?
内存缓存 (Memory cache) – 该图片是否最近被加载过并仍存在于内存中?
资源类型(Resource) – 该图片是否之前曾被解码、转换并写入过磁盘缓存?
数据来源 (Data) – 构建这个图片的资源是否之前曾被写入过文件缓存?
前两步检查图片是否在内存中,如果是则直接返回图片。后两步则检查图片是否在磁盘上,以便快速但异步地返回图片。
如果四个步骤都未能找到图片,则Glide会返回到原始资源以取回数据(原始文件,Uri, Url等)。


Glide复用机制

除去了Glide的缓存机制以外,还有一个很强大的复用机制

1. 缓存和复用机制的区别和作用

简单来说,缓存是将数据存储起来,下次需要时就不用重新加载数据,直接拿来即用,作用是加快加载速度、避免相同的数据占用空间,降低内存占用;

复用的意思是重新使用,将已经不需要使用的数据空间重新拿来使用,他的作用是避免频繁申请内存,避免OOM,因为在短时间内快速申请释放内存,因为GC不及时,可能在短时间内来不及回收到足够的空间,导致OOM。所以复用的作用是减少内存抖动。

2. 原理是什么

(1)内存占用

在讲解glide如何复用bitmap之前,先来了解bitmap的内存占用问题。

在Android3.0之前,Bitmap的内存分配分为两部分,一部分是分配在Dalvik的VM堆中,而像素数据的内存是分配在Native堆中。

而到了Android3.0之后,Bitmap的内存则已经全部分配在VM堆上。意味着我们不需要手动释放bitmap内存,gc内存管理机制会帮忙管理。

(2)inMutable

inMutable是glide能够复用的基石,是bitmapFactory提供的一个参数,表示该bitmap是可变的,支持复用的。

Bitmap是android中最容易造成OOM的元凶之一,在Bitmap的解析参数,BitmapFactory.Options中提供了两个属性:inMutable、inBitmap

详情参考之前的文章《bitmap疑惑》

当你进行Bitmap的复用,需要设置inMutable为true,inBitmap设置想已经存在的Bitmap。

所谓复用的意思,就是将废弃不用准备recyle的bitmap,重新拿过来使用。

在使用inBitmap参数前,每创建一个Bitmap对象都会分配一块内存,而使用了inBitmap后, Bitmap的内存是被重新利用的。

(3)Bitmap复用使用条件

在Android 4.4之前,仅支持相同大小的bitmap,inSampleSize必须为1,而且必须采用jpeg或png格式。

在Android 4.4之后只有一个限制,就是被复用的bitmap尺寸要大于 新的bitmap,简单来说就是大图可以给小图复用。

(4)inMutable、inBitmap使用

BitmapFactory.Options largeOption = new BitmapFactory.Options(); 
largeOption.inMutable = true; // 设置inMutable 
Bitmap largeBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.large, largeOption) 
 
BitmapFactory.Options smallOption = new BitmapFactory.Options(); 
smallOption.inBitmap = largeBitmap; // 设置inBitmap被复用的Bitmap 
Bitmap smallBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.small, smallOption); 

(5)Lru算法

虽然说,将不再使用的bitmap保存起来能够避免频繁申请内存,减少内存抖动,但是同时也会耗用内存存储这些已经不再使用的bitmap,因此Glide使用LRU算法,类似LruCache,有一个最大空间限制。

3.源码分析

真正解析的是DownSampler类

DownSampler

// DownSampler 
public Resource<Bitmap> decode(InputStream is, int requestedWidth, int requestedHeight, 
      Options options, DecodeCallbacks callbacks) throws IOException {
    
 
    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); 
    boolean isHardwareConfigAllowed = 
      options.get(ALLOW_HARDWARE_CONFIG) != null && options.get(ALLOW_HARDWARE_CONFIG); 
    if (decodeFormat == DecodeFormat.PREFER_ARGB_8888_DISALLOW_HARDWARE) {
    
      isHardwareConfigAllowed = false; 
    } 
 
    try {
    
      Bitmap result = decodeFromWrappedStreams(is, bitmapFactoryOptions, 
          downsampleStrategy, decodeFormat, isHardwareConfigAllowed, requestedWidth, 
          requestedHeight, fixBitmapToRequestedDimensions, callbacks); 
      return BitmapResource.obtain(result, bitmapPool); 
    } finally {
    
      releaseOptions(bitmapFactoryOptions); 
      byteArrayPool.put(bytesForOptions); 
    } 
  } 

DownSampler 负责真正的解析加载图片,过载过程,

  1. inTempStorage

inTempStorage是一个bitmap解析的参数,带入一个buffer,创建临时文件,将图片存储的中间缓存空间

 BitmapFactory.Options bitmapFactoryOptions = getDefaultOptions(); 
 
 bitmapFactoryOptions.inTempStorage = bytesForOptions; 
    
 byteArrayPool.put(bytesForOptions); 
  1. getDefaultOptions()

Glide哪里将inMutable设置为true,答应就在getDefaultOptions()中

获取默认的bitmap解析参数,可以看到默认的设置里面inMutable是设为true的。

  private static synchronized BitmapFactory.Options getDefaultOptions() {
    
    BitmapFactory.Options decodeBitmapOptions; 
    synchronized (OPTIONS_QUEUE) {
    
      decodeBitmapOptions = OPTIONS_QUEUE.poll(); 
    } 
    if (decodeBitmapOptions == null) {
    
      decodeBitmapOptions = new BitmapFactory.Options(); 
      resetOptions(decodeBitmapOptions); 
    } 
 
    return decodeBitmapOptions; 
  } 
   
  private static void resetOptions(BitmapFactory.Options decodeBitmapOptions) {
    
    decodeBitmapOptions.inTempStorage = null; 
    decodeBitmapOptions.inDither = false; 
    decodeBitmapOptions.inScaled = false; 
    decodeBitmapOptions.inSampleSize = 1; 
    decodeBitmapOptions.inPreferredConfig = null; 
    decodeBitmapOptions.inJustDecodeBounds = false; 
    decodeBitmapOptions.inDensity = 0; 
    decodeBitmapOptions.inTargetDensity = 0; 
    decodeBitmapOptions.outWidth = 0; 
    decodeBitmapOptions.outHeight = 0; 
    decodeBitmapOptions.outMimeType = null; 
    decodeBitmapOptions.inBitmap = null; 
    decodeBitmapOptions.inMutable = true; 
  } 

LruBitmapPool

Glide复用bitmap,总需要一个地方来缓存废弃不用的bitmap,而LruBitmapPool 就是Glide提供Bitmap缓存复用的池。

下面的代码逻辑比较简单,内部其实也是LruCache的思想,真正的实现类是LruPoolStrategy

 
public class LruBitmapPool implements BitmapPool {
    
 
private final LruPoolStrategy strategy; 
 
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())) {
    
      bitmap.recycle(); 
      return; 
    } 
 
    final int size = strategy.getSize(bitmap); 
    strategy.put(bitmap); 
    tracker.add(bitmap); 
 
    puts++; 
    currentSize += size; 
    dump(); 
    evict(); 
  } 
 
  public Bitmap get(int width, int height, Bitmap.Config config) {
    
    Bitmap result = getDirtyOrNull(width, height, config); 
    if (result != null) {
    
      result.eraseColor(Color.TRANSPARENT); 
    } else {
    
      result = createBitmap(width, height, config); 
    } 
 
    return result; 
    } 
} 

SizeConfigStrategy

SizeConfigStrategy 内部代码逻辑也比较简单,是执行真正实现存取bitmap的策略类。

public class SizeConfigStrategy implements LruPoolStrategy {
    
  private static final int MAX_SIZE_MULTIPLE = 8; 
  private final KeyPool keyPool = new KeyPool(); 
  private final GroupedLinkedMap<Key, Bitmap> groupedMap = new GroupedLinkedMap<>(); 
  private final Map<Bitmap.Config, NavigableMap<Integer, Integer>> sortedSizes = new HashMap<>(); 
   
  // 存入 
  public void put(Bitmap bitmap) {
    
    int size = Util.getBitmapByteSize(bitmap); 
    Key key = keyPool.get(size, bitmap.getConfig()); 
 
    groupedMap.put(key, bitmap); 
 
    NavigableMap<Integer, Integer> sizes = getSizesForConfig(bitmap.getConfig()); 
    Integer current = sizes.get(key.size); 
    sizes.put(key.size, current == null ? 1 : current + 1); 
  } 
 
  // 取出 
  public Bitmap get(int width, int height, Bitmap.Config config) {
    
    int size = Util.getBitmapByteSize(width, height, config); 
    // 查找出最合适的bitmap 
    Key bestKey = findBestKey(size, config); 
    // 取出 
    Bitmap result = groupedMap.get(bestKey); 
    if (result != null) {
    
      // Decrement must be called before reconfigure. 
      decrementBitmapOfSize(bestKey.size, result); 
      result.reconfigure(width, height, 
          result.getConfig() != null ? result.getConfig() : Bitmap.Config.ARGB_8888); 
    } 
    return result; 
  } 
   
} 

findBestKey()方法就是查找出最合适的key,通过大小匹配

// SizeCofingStrategy 
  private Key findBestKey(int size, Bitmap.Config config) {
    
    Key result = keyPool.get(size, config); 
    for (Bitmap.Config possibleConfig : getInConfigs(config)) {
    
      NavigableMap<Integer, Integer> sizesForPossibleConfig = getSizesForConfig(possibleConfig); 
      Integer possibleSize = sizesForPossibleConfig.ceilingKey(size); 
      if (possibleSize != null && possibleSize <= size * MAX_SIZE_MULTIPLE) {
    
        if (possibleSize != size 
            || (possibleConfig == null ? config != null : !possibleConfig.equals(config))) {
    
          keyPool.offer(result); 
          result = keyPool.get(possibleSize, possibleConfig); 
        } 
        break; 
      } 
    } 
    return result; 
  } 

在这里有个疑问

从缓存池里获取一张Bitmap了,但是我们如何修改他的Config设置,例如颜色格式、或者更改默认的Config.ARGB_8888格式为ARGB_444格式?

答案就是:

  1. 如果是首次加载,可以直接在Option指定;

  2. 如果是已经加载过的bitmap,bitmap提供了一个reconfigure()方法进行设置。

    public void reconfigure(int width, int height, Config config) {
    
        checkRecycled("Can't call reconfigure() on a recycled bitmap"); 
        if (width <= 0 || height <= 0) {
    
            throw new IllegalArgumentException("width and height must be > 0"); 
        } 
        if (!isMutable()) {
    
            throw new IllegalStateException("only mutable bitmaps may be reconfigured"); 
        } 
 
        nativeReconfigure(mNativePtr, width, height, config.nativeInt, mRequestPremultiplied); 
        mWidth = width; 
        mHeight = height; 
        mColorSpace = null; 
    } 

什么时候加入复用池?

BitmapPool提供了缓存bitmap的地方,那什么时候将bitmap加入BitmapPool呢?

事实上是当各个外部资源recycle的时候,就会加入对应的BitmapPool,而BitmapPool是可配置的,这里只是分析LruBitmapPool

// BitmapDrawableResource 
  public void recycle() {
    
    bitmapPool.put(drawable.getBitmap()); 
  } 

什么时候从复用池取出?

同理,那什么时候从BitmapPool中取出来使用的,来看一个例子就是在DownSamper的decodeFromWrappedStreams,内部有一个取出逻辑

// DownSampler 
  private Bitmap decodeFromWrappedStreams(InputStream is, 
      BitmapFactory.Options options, DownsampleStrategy downsampleStrategy, 
      DecodeFormat decodeFormat, boolean isHardwareConfigAllowed, int requestedWidth, 
      int requestedHeight, boolean fixBitmapToRequestedDimensions, 
      DecodeCallbacks callbacks) throws IOException {
    
     //计算原始大小 
    int[] sourceDimensions = getDimensions(is, options, callbacks, bitmapPool); 
    int sourceWidth = sourceDimensions[0]; 
    int sourceHeight = sourceDimensions[1]; 
    String sourceMimeType = options.outMimeType; 
    // 计算方向 
    int orientation = ImageHeaderParserUtils.getOrientation(parsers, is, byteArrayPool); 
    int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation); 
    boolean isExifOrientationRequired = TransformationUtils.isExifOrientationRequired(orientation); 
    // 计算目标大小 
    int targetWidth = requestedWidth == Target.SIZE_ORIGINAL ? sourceWidth : requestedWidth; 
    int targetHeight = requestedHeight == Target.SIZE_ORIGINAL ? sourceHeight : requestedHeight; 
    // 计算类型 
    ImageType imageType = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool); 
    ... 
    // 使用复用池 
    if ((options.inSampleSize == 1 || isKitKatOrGreater) && shouldUsePool(imageType)) {
    
      ... 
      // If this isn't an image, or BitmapFactory was unable to parse the size, width and height 
      // will be -1 here. 
      if (expectedWidth > 0 && expectedHeight > 0) {
    
        setInBitmap(options, bitmapPool, expectedWidth, expectedHeight); 
      } 
    } 
    Bitmap downsampled = decodeStream(is, options, callbacks, bitmapPool); 
    callbacks.onDecodeComplete(bitmapPool, downsampled); 
    ... 
    return rotated; 
  } 
   
private static void setInBitmap( 
      BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height) {
    
    @Nullable Bitmap.Config expectedConfig = null; 
	... 
    // BitmapFactory will clear out the Bitmap before writing to it, so getDirty is safe. 
    options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig); 
  } 

getDirty()方法,表示在图片加载中,如果可以使用bitmap池,就会调用bitmapPool的getDirty(),最后赋值给inBitmap

// LruBitmapPool 
  public Bitmap getDirty(int width, int height, Bitmap.Config config) {
    
    // 优先获取 
    Bitmap result = getDirtyOrNull(width, height, config); 
    if (result == null) {
    // 若没有,新建一个bitmap 
      result = createBitmap(width, height, config); 
    } 
    return result; 
  } 

什么时候从复用池删除?

LruBitmapPool内部利用了Lru算法,每次操作都自动检测是否删除多余的缓存

//LruBitmapPool 
  private void evict() {
    
    trimToSize(maxSize); 
  } 
 
  private synchronized void trimToSize(long 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(); 
    } 
  } 

总结

  1. Glide缓存:
    活动资源 (Active Resources)
    内存缓存 (Memory cache)
    资源类型(Resource
    数据来源 (Data)

  2. Glide复用:
    BitmapPool(LruBitmapPool)

参考

https://muyangmin.github.io/glide-docs-cn/doc/caching.html

https://blog.csdn.net/u011035622/article/details/50277565

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/6262.html

(0)
上一篇 2021年7月17日
下一篇 2021年7月17日

相关推荐

发表回复

登录后才能评论