美文网首页
Glide最新源码解析(六)-缓存策略-复用池

Glide最新源码解析(六)-缓存策略-复用池

作者: 烧伤的火柴 | 来源:发表于2019-10-08 11:27 被阅读0次

介绍

Glide在解码Bitmap的时候采用了BitmapPool复用池的方式,达到高效利用内存,减少创建内存的开销。复用效果如下:


复用前.png 内存复用.png

未使用复用的情况下,每次解码都会申请一块新的内存,如果使用复用bitmap对象,解码的时候会去池子中找出合适大小的bitmap,使用这个bitmap对象的内存。
bitmap复用并不会减少内存大小,而是减少了内存分配和回收带来的内存抖动导致页面卡顿,以及内存溢出问题。Google的开发者文档中有如下提示:


复用介绍.png

android4.4以上被复用的bitmap内存大小必须大于等于要解码的bitmap的内存大小,才可以复用bitmap
4.4以下3.0以上要解码的bitmap必须是jpeg或者png格式,而且和复用的bitmap大小一样,inSampleSize=1,另外复用的bitmap必须要设置inPreferredConfig

分析

bitmapPool接口

** An interface for a pool that allows users to reuse {@link android.graphics.Bitmap} objects. */
public interface BitmapPool {

  /** Returns the current maximum size of the pool in bytes. */
  long getMaxSize();

  /**
   * Multiplies the initial size of the pool by the given multiplier to dynamically and
   * synchronously allow users to adjust the size of the pool.
   *
   * <p>If the current total size of the pool is larger than the max size after the given multiplier
   * is applied, {@link Bitmap}s should be evicted until the pool is smaller than the new max size.
   *
   * @param sizeMultiplier The size multiplier to apply between 0 and 1.
   */
  void setSizeMultiplier(float sizeMultiplier);

  /**
   * Adds the given {@link android.graphics.Bitmap} if it is eligible to be re-used and the pool can
   * fit it, or calls {@link Bitmap#recycle()} on the Bitmap and discards it.
   *
   * <p>Callers must <em>not</em> continue to use the Bitmap after calling this method.
   *
   * @param bitmap The {@link android.graphics.Bitmap} to attempt to add.
   * @see android.graphics.Bitmap#isMutable()
   * @see android.graphics.Bitmap#recycle()
   */
  void put(Bitmap bitmap);

  /**
   * Returns a {@link android.graphics.Bitmap} of exactly the given width, height, and
   * configuration, and containing only transparent pixels.
   *
   * <p>If no Bitmap with the requested attributes is present in the pool, a new one will be
   * allocated.
   *
   * <p>Because this method erases all pixels in the {@link Bitmap}, this method is slightly slower
   * than {@link #getDirty(int, int, android.graphics.Bitmap.Config)}. If the {@link
   * android.graphics.Bitmap} is being obtained to be used in {@link android.graphics.BitmapFactory}
   * or in any other case where every pixel in the {@link android.graphics.Bitmap} will always be
   * overwritten or cleared, {@link #getDirty(int, int, android.graphics.Bitmap.Config)} will be
   * faster. When in doubt, use this method to ensure correctness.
   *
   * <pre>
   *     Implementations can should clear out every returned Bitmap using the following:
   *
   * {@code
   * bitmap.eraseColor(Color.TRANSPARENT);
   * }
   * </pre>
   *
   * @param width The width in pixels of the desired {@link android.graphics.Bitmap}.
   * @param height The height in pixels of the desired {@link android.graphics.Bitmap}.
   * @param config The {@link android.graphics.Bitmap.Config} of the desired {@link
   *     android.graphics.Bitmap}.
   * @see #getDirty(int, int, android.graphics.Bitmap.Config)
   */
  @NonNull
  Bitmap get(int width, int height, Bitmap.Config config);

  /**
   * Identical to {@link #get(int, int, android.graphics.Bitmap.Config)} except that any returned
   * {@link android.graphics.Bitmap} may <em>not</em> have been erased and may contain random data.
   *
   * <p>If no Bitmap with the requested attributes is present in the pool, a new one will be
   * allocated.
   *
   * <p>Although this method is slightly more efficient than {@link #get(int, int,
   * android.graphics.Bitmap.Config)} it should be used with caution and only when the caller is
   * sure that they are going to erase the {@link android.graphics.Bitmap} entirely before writing
   * new data to it.
   *
   * @param width The width in pixels of the desired {@link android.graphics.Bitmap}.
   * @param height The height in pixels of the desired {@link android.graphics.Bitmap}.
   * @param config The {@link android.graphics.Bitmap.Config} of the desired {@link
   *     android.graphics.Bitmap}.
   * @return A {@link android.graphics.Bitmap} with exactly the given width, height, and config
   *     potentially containing random image data.
   * @see #get(int, int, android.graphics.Bitmap.Config)
   */
  @NonNull
  Bitmap getDirty(int width, int height, Bitmap.Config config);

  /** Removes all {@link android.graphics.Bitmap}s from the pool. */
  void clearMemory();

  /**
   * Reduces the size of the cache by evicting items based on the given level.
   *
   * @param level The level from {@link android.content.ComponentCallbacks2} to use to determine how
   *     many {@link android.graphics.Bitmap}s to evict.
   * @see android.content.ComponentCallbacks2
   */
  void trimMemory(int level);
}

接口中的方法名字望文生义也应该知道是干什么,而且注释也很清楚,我们具体看一下实现类,它的实现类也有一个BitmapPoolAdapter适配器适配不使用复用的时候,另外一个就是LruBitmapPool,采用LRU算法管理复用池。
首先看一下LruBitmapPool的结构

  public class LruBitmapPool implements BitmapPool {
  private static final String TAG = "LruBitmapPool";
  private static final Bitmap.Config DEFAULT_CONFIG = Bitmap.Config.ARGB_8888;//默认格式

  private final LruPoolStrategy strategy;//LruPool策略
  private final Set<Bitmap.Config> allowedConfigs;//允许复用的格式,在Android的O版本以上移除HARDWARE格式
  private final long initialMaxSize;//默认大小
  private final BitmapTracker tracker;//bitmap跟踪记录,默认是null实现

  private long maxSize;
  private long currentSize;//已用大小
  private int hits;
    private int misses;
  private int puts;
  private int evictions;
  ...
  public LruBitmapPool(long maxSize) {
    this(maxSize, getDefaultStrategy(), getDefaultAllowedConfigs());
  }

 ...
  private static LruPoolStrategy getDefaultStrategy() {
    final LruPoolStrategy strategy;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
      strategy = new SizeConfigStrategy();
    } else {
      strategy = new AttributeStrategy();
    }
    return strategy;
  }
  ...
 }

构造方法中根据不同的sdk版本采用不同的策略,现在大部分的应用都是4.4以上版本,所以主要分析SizeConfigStrategy策略即可。SizeConfigStrategy这个类中采用Lru算法,但是会根据不同的bitmap config缓存不同的复用池

  • put方法
public synchronized void put(Bitmap bitmap) {
    //安全校验
   ...
  //bitmap不支持复用,bitmap大小超出了最大容量,配置表中不支持此格式,这些情况bitmap都回收,不能复用
    if (!bitmap.isMutable()
        || strategy.getSize(bitmap) > maxSize
        || !allowedConfigs.contains(bitmap.getConfig())) {
      bitmap.recycle();
      return;
    }

    final int size = strategy.getSize(bitmap);
    strategy.put(bitmap);
    ...
    evict();
  }

strategy.getSize(bitmap);得到bitmap的内存大小,重要的是strategy.put(bitmap);看一下SizeConfigStrategy#put方法

  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<>();

@Override
  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);
  }

groupedMap的key是根据bitmap的config和size生成的,这样groupedMap可以存储不同格式相同内存大小的bitmap。

  private NavigableMap<Integer, Integer> getSizesForConfig(Bitmap.Config config) {
    NavigableMap<Integer, Integer> sizes = sortedSizes.get(config);
    if (sizes == null) {
      sizes = new TreeMap<>();
      sortedSizes.put(config, sizes);
    }
    return sizes;
  }

根据bitmap的格式从sortedSizes中取出NavigableMap对象,这个对象中的key是bitmap 的内存大小,value是一个引用计数

简单介绍一下NavigableMap:
NavigableMap扩展了 SortedMap,具有了针对给定搜索目标返回最接近匹配项的导航方法。方法 lowerEntry、floorEntry、ceilingEntry 和 higherEntry 分别返回与小于、小于等于、大于等于、大于给定键的键关联的 Map.Entry 对象,如果不存在这样的键,则返回 null。类似地,方法 lowerKey、floorKey、ceilingKey 和 higherKey 只返回关联的键。

  • LruBitmapPool#get方法
  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;
  }

@NonNull
  private static Bitmap createBitmap(int width, int height, @Nullable Bitmap.Config config) {
    return Bitmap.createBitmap(width, height, config != null ? config : DEFAULT_CONFIG);
  }

 @Nullable
  private synchronized Bitmap getDirtyOrNull(
      int width, int height, @Nullable Bitmap.Config config) {
   ...
    final Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG);
   ...
    return result;
  }

get先从复用池中筛选合适的内存大小的Bitmap,如果没有就使用Bitmap.createBitmap创建一个
重点分析一下strategy中的get方法

  @Override
  @Nullable
  public Bitmap get(int width, int height, Bitmap.Config config) {
    int size = Util.getBitmapByteSize(width, height, config);
    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, config);
    }
    return result;
  }
//第一步根据bitmap内存大小和格式找出key
 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;
  }

根据config找出NavigableMap,然后使用ceilingKey找出大等于size 的key值possibleSize,possibleSize如果大于size的8倍就造成内存浪费,possibleSize 和size不相等或者配置不同就更新key。get方法根据这个key从groupedMap中取出要复用的bitmap。然后是decrementBitmapOfSize 方法减少bitmap的大小

private void decrementBitmapOfSize(Integer size, Bitmap removed) {
    Bitmap.Config config = removed.getConfig();
    NavigableMap<Integer, Integer> sizes = getSizesForConfig(config);
    Integer current = sizes.get(size);
    ...
    if (current == 1) {
      sizes.remove(size);
    } else {
      sizes.put(size, current - 1);
    }
  }

这个方法会根据config找出复用管理的NavigableMap然后将引用计数-1;
至此bitmapPool的get和put方法都分析完了,这两个方法都是委托给LruPoolStrategy实现的。
回到put方法中在最后会调用evict();方法有调用trimToSize(maxSize);方法

  private synchronized void trimToSize(long size) {
    while (currentSize > size) {
      final Bitmap removed = strategy.removeLast();
     ...
      tracker.remove(removed);
      currentSize -= strategy.getSize(removed);
      ...
      removed.recycle();
    }
  }

这个方法会一直通过strategy移除最少使用的Bitmap并且回收掉内存,以达到需要的内存大小。

相关文章

网友评论

      本文标题:Glide最新源码解析(六)-缓存策略-复用池

      本文链接:https://www.haomeiwen.com/subject/eibiyctx.html