美文网首页
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