美文网首页Glide学习
Glide中bitmap对象池实现学习

Glide中bitmap对象池实现学习

作者: vb12 | 来源:发表于2018-07-10 18:13 被阅读6次

    bitmap对象池基础代码学习

    GroupedLinkedMap.java的逻辑跟java.util.LinkedHashMap类似, 也是通过LRU算法来记录最近最少使用的数据. 只不过这里的数据指的是尺寸信息, 而不是具体的某个bitmap对象. 同时, GroupedLinkedMap把每次get()请求都认为是一次访问, 而忽略addremove操作.

    代码逻辑

    对于其中的双向链表的增长逻辑, 如下图:

    首先有一个初始节点Head :


    image.png

    当遇到get(Key)操作时, 会新生成一个节点 并调用makeHead(entry); 如下图: 新生成一个节点B


    image.png

    在makeHead()方法的最后会更新该新节点B. 最后变成如下:


    image.png

    于是一次get调用结束后, 两个节点的关系就如上图

    当再发生一次get操作: 会新生成一个节点C , 同样不管命中与否,都要调用makeHead()


    image.png

    makeHead()--> updateEntry()


    image.png

    其实是把新节点插入到head和B节点之间了, 变换一下上图, 最后结果如下:

    image.png

    为了更清晰的分析其逻辑, 再通过get(key)操作, 插入一个新节点, 最终图:

    image.png

    可以看到head的next始终指向最近被访问的那个节点. 而head的prev指向最后被访问的那个节点

    于是我们知道, 如果要删除最后的那个节点, 就可以通过head立马找到.

    这个列表结构, 应该也算是一个典型的解决方案. 需要记住.

    具体分析其removeLast的代码:

    
     @Nullable
      public V removeLast() {
        LinkedEntry<K, V> last = head.prev;
    
        while (!last.equals(head)) {
          V removed = last.removeLast();
          if (removed != null) {
            return removed;
          } else {
            // We will clean up empty lru entries since they are likely to have been one off or
            // unusual sizes and
            // are not likely to be requested again so the gc thrash should be minimal. Doing so will
            // speed up our
            // removeLast operation in the future and prevent our linked list from growing to
            // arbitrarily large
            // sizes.
            removeEntry(last);
            keyToEntry.remove(last.key);
            last.key.offer();
          }
    
          last = last.prev;
        }
    
        return null;
      }
    

    如果不看else分支中的特殊逻辑, 那么这个方法是非常简单的, 就是通过head节点的prev找到最不经常被访问的节点, 并返回.

    需要注意的是, GroupedLinkedMap中的数据除去双向链表的结构, 还有一个map结构, value值是LinkedEntry. 而这个自定义的LinkedEntry, 是持有一个列表来保存数据.

    问题:

    为什么put操作, 会把新创建的节点放到队尾, 也就是最近不不经常使用的位置上. 这是为了什么呢? 难道它的意思是说, 新创建的节点, 一次都没有被访问过(get操作) 所以应该放在最后的位置上.

    --------put操作只会把新建的节点放在队尾, 因为这个节点从来没有被访问过, 只要这种类型的数据被访问了, 更具体来说就是被从对象池中拿去使用了, 那就会把该节点前移, 所以如果一直没人用, 那么这种类型的数据在以后遇到内存不足时, 会被优先清理掉, 就是因为没人用.

    问题:

    BitmapPool中存放的bitmap, 或者更具体的说, GroupedLinkedMap中存放的bitmap, 到底是正在被使用的? 还是没有被使用的?

    --------回答这个问题, 可以通过查看BitmapPool.java这个接口的方法注释来解答, put()操作就是把bitmap放回缓冲池, 所以其中的bitap是不在用的.

    可以尝试跟踪一下, 对于略懂应用, 在请求图片时, 都是哪些尺寸的图片请求最多, 如果说图片的分类比较集中, 那么可以据此进一步优化bitmap对象池.

    通过为RequestOptions设置下面的标志, 能够使得所有的对象池图片尺寸都按照控件请求的尺寸, 也就是说如果控件要求一个200x300的图片, 那么不管后台给的是200x301还是200x400 ,那么都会只裁剪成200x300, 这样会大大减轻bitmap对象池的占用量和可能导致的回收.

    为什么会影响回收? 是因为如果对象池内的key, 也就是图片的分辨率种类太多, 那么对象池的空间必然会很快的超过最大限制, 那么进而会触发一次对象池回收, 回收掉最不常用的一种key , 也就是把其对应的列表中的bitmap都进行一次recycle回收, 这是很耗时的. 而且都在主线程中完成.

    DEFAULT_REQUESTOPTIONS.getOptions().set(FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS, true);

    SizeConfigStrategy.java

    groupedMap变量定义:

    private final GroupedLinkedMap<Key, Bitmap> groupedMap = new GroupedLinkedMap<>();

    他的key信息包括尺寸和config.

    sortedSizes变量, 定义:

    private final Map<Bitmap.Config, NavigableMap<Integer, Integer>> sortedSizes = new HashMap<>();

    key时Bitmap.Config, value是一个map, 首先这个map的定义:

    public interface NavigableMap<K,V> extends SortedMap<K,V> {

    所以这个map是排过序的, 当然是按key排序的. 它的key是图片尺寸, 类型为integer, value类型也是integer, 记录的是图片尺寸对应的数量.

    sortedSizes到底是什么意义? 理解不了呢!!!!

    sortedSizes这个map的value值虽然声明为NavigableMap, 但实际类型为TreeMap

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

    需要搞清楚这个bitmap.getConfig()值, 是每个bitmap各部相同吗? 还是系统内都是一样的, 类似Bitmap.Config.ARGB_8888 ????

    -------回答: Bitmap.Config是一个emun类型, 当然整个系统内只有有限的几种, 具体到我们的应用内, 只有Bitmap.Config.ARGB_8888 一种. 所以sortedSizes结构就可以解构一层, 只需要关注其中的value类型:NavigableMap<Integer, Integer> sizes

    那么这个类型的sizes有序散列表, 代表了什么含义呢/?>????

    这个有序散列表的key是图片的大小, 比如1m, 2m, 或者多少K 是一个数值.

    而这个有序散列表的vaue是这种大小的数量. 比如目前整个bitmap对象池内有1M的图片3张, 那么value就是3.

    在图片归还bitmap对象池时, 需要增加这个value值, 如果不存在就置为1

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

    当从bitmap对象池内请求图片后, 需要对应的减少value的计数

    
    @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,
              result.getConfig() != null ? result.getConfig() : Bitmap.Config.ARGB_8888);
        }
        return result;
      }
    
    

    计数减一的操作都在decrementBitmapOfSize方法中:

    
     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 == null) {
          throw new NullPointerException("Tried to decrement empty size"
              + ", size: " + size
              + ", removed: " + logBitmap(removed)
              + ", this: " + this);
        }
    
        if (current == 1) {
          sizes.remove(size);
        } else {
          sizes.put(size, current - 1);
        }
      }
    

    如果当前只有1个, 再减1就是0, 那么就直接从sizes散列表中删除这一项.

    否则就是单纯的减1.

    这个逻辑就是比较简单易懂了.

    同理, 当bitmap对象池, 由于占用空间超过最大限制, 而必须删除一项时, 也需要把sizes中的计数减1

      @Override
      @Nullable
      public Bitmap removeLast() {
        Bitmap removed = groupedMap.removeLast();
        if (removed != null) {
          int removedSize = Util.getBitmapByteSize(removed);
          decrementBitmapOfSize(removedSize, removed);
        }
        return removed;
      }
    
    

    剩下最后一个难懂的方法findBestKey, 如下图:

    
      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;
      }
    
    

    为什么要使用NavigableMap, 是因为它有一个celling方法, 对于根据size进行的查询可以返回, 一个大于等于size的bitmap, 这样仍然可以重用. 而不必强制要求size在map中存在.

    说白一点: 能找到一个相同尺寸的bitmap拿来重用当然好, 但是如果找不到, 就找一个稍微大一点的也行. 也能重用.

    当然, ceilingKey(key) 返回的是大于等于key的, 最接近key的key, 所以只要有等于key的值, 肯定还是优先使用key 这个尺寸本身.

    AttributeStrategy.java

    在K版本之前的版本上作为LruBitmapPool的具体实现, 逻辑比较简单了, K版本之前对bitmap的复用, 要求比较高, 只有尺寸完全一致的bitmap才能被复用, 所以其内部实现很简单, 就是通过GroupedLinkedMap来保存bitmap, 可以是bitmap 对应的key(size, config).

    LruBitmapPool.java

    根据版本来选择对应的缓存策略实现类:

    
      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;
      }
    
    

    dump()

    dumpUnchecked()

    getDefaultAllowedConfigs()

    ThrowingBitmapTracker

    NullBitmapTracker

    ---------------这几个都可以先不看, 都没用.

    唯一要关注的是put() 和 get()方法.

    实际上对bitmap的缓存都是通过private final LruPoolStrategy strategy; 来委托实现的.

    最复杂的方法trimToSize() 从优化的经历来看这里面的 removed.recycle()操作非常耗时. 能在非UI线程recycle吗?

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

    我们可以想办法减少trimToSize()的执行, 从而避免在ui线程回收bitmap导致的耗时.

    bitmapPool等缓存池的大小的设置, 是通过MemorySizeCalculator来设置的.

    可以看到对于O版本以上的系统, 设置的bitmappool尺寸会比较小.

    private final int bitmapPoolSize;

    int screenSize = widthPixels * heightPixels * BYTES_PER_ARGB_8888_PIXEL;

    int targetBitmapPoolSize = Math.round(screenSize * builder.bitmapPoolScreens);


    image.png

    如果是O版本之前的系统, 就是4屏幕的图片大小空间作为缓存, 而从O版本开始就只有1屏幕了. 原因看上面的注释.

    但是还有一点,如果是高于O版本的低内存设备, 那么不会设置图片bitmap缓存, 会创建一个BitmapPoolAdapter实例作为一个虚缓存, 其实每次取bitmap时都是create操作.

    
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isLowMemoryDevice(activityManager)) {
    
        bitmapPoolScreens = 0;
    
    }
    
    

    相关文章

      网友评论

        本文标题:Glide中bitmap对象池实现学习

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