拆 Glide 系列之 - Bitmap 复用

Bitmap 内存管理

Google 官方教程 Managing Bitmap Memory 是这样说的

  • Android2.2(API 8)一下的时候,当 GC 工作时,应用的线程会暂停工作,同步的 GC 会影响性能。而 Android2.3 之后,GC 变成了并发的,意味着 Bitmap 没有引用的时候其占有的内存会很快被回收
  • 在Android2.3.3(API10)之前,Bitmap 的像素数据存放在 Native 内存,而 Bitmap 对象本身则存放在 Dalvik Heap 中。Native 内存中的像素数据以不可预测的方式进行同步回收,有可能会导致内存升高甚至 OOM Crash。而在 Android3.0 之后,Bitmap 的像素数据也被放在了 Dalvik Heap 中

所以根据不同的 Android 版本,对于 Bitmap 的内存处理上,也需要以不同的方式来对待

  • Android2.3.3 以下

推荐使用 Bitmap#recycle 方法进行 Bitmap 内存回收

  • Android3.0 以上

推荐的是 Bitmap 内存复用,为此引入了一个 BitmapFactory.Options.inBitmap 字段来设置打算复用的 Bitmap,这个字段设置之后 Bitmap 解码的时候会尝试复用这一张存在的 Bitmap。内存复用减少内存的分配操作可以避免内存抖动带来的潜在的卡顿,Glide 内部的 Bitmap 复用模块正是今天的主角

Bitmap 复用

几点限制:

  • 被复用的 Bitmap 必须为 Mutable(通过 BitmapFactory.Options 设置)
  • 4.4 之前,将要解码的图像(无论是资源还是流)必须是 jpeg 或 png 格式且和被复用的 Bitmap 大小一样,其中BitmapFactory.Options#inSampleSize 字段必须设置为 1,要求比较严苛
  • 4.4 以后,将要解码的图像的内存需要大于等于要复用的 Bitmap 的内存

《拆 Glide 系列之 - Bitmap 复用》 Glide Bitmap 复用模块

其中最重要的角色是 LruPoolStrategy ,是 Bitmap 内存复用策略的接口,有三个实现类 SizeConfigStrategySizeStrategyAttributeStrategy

在分析这个具体的复用策略前,先介绍一下其中实现 LRU 的数据结构,我们都知道 SDK 提供的 LRUCache 类可以帮助实现 LRU 功能,且内部的数据结构是 LinkedHashMap。但 Glide 并没有使用现成的,而是定义了一个 GroupedLinkedMap ,其类似 LinkedHashMap ,其实体 LinedEntry 可以用于实现双向链表,并通过 GroupedLinkedMap#makeTail/GroupedLinkedMap#makeHead 方法来改变链表头尾位置,使得具有 LRU 功能,且 LinedEntryValue 为一个 List 集合,因为可能有多个相同 key(例如:Bitmap Size)的 Bitmap 集合可以复用,而且可以控制移除队列最后一个对象,而不是整个 List 集合,这应该是不使用 LRUCache 的原因

class GroupedLinkedMap<K extends Poolable, V> {
    private final LinkedEntry<K, V> head = new LinkedEntry<K, V>();
    private final Map<K, LinkedEntry<K, V>> keyToEntry = new HashMap<K, LinkedEntry<K, V>>();

    public void put(K key, V value) {
        LinkedEntry<K, V> entry = keyToEntry.get(key);

        if (entry == null) {
            entry = new LinkedEntry<K, V>(key);
            makeTail(entry);   //移到表尾
            keyToEntry.put(key, entry);
        } else {
            key.offer(); //如果 key 相同,那么可以把现在的回收到对象池
        }

        entry.add(value);
    }

    public V get(K key) {
        LinkedEntry<K, V> entry = keyToEntry.get(key);
        if (entry == null) {
            entry = new LinkedEntry<K, V>(key);
            keyToEntry.put(key, entry);
        } else {
            key.offer();
        }
        makeHead(entry);    //移到表头
        return entry.removeLast();
    }

    public V removeLast() {
        LinkedEntry<K, V> last = head.prev;

        while (!last.equals(head)) {
            V removed = last.removeLast();
            if (removed != null) {
                return removed;
            } else {
                removeEntry(last);  //清理已经空的 LinkedEntry
                keyToEntry.remove(last.key);
                last.key.offer();
            }

            last = last.prev;
        }

        return null;
    }

    // Make the entry the most recently used item.
    private void makeHead(LinkedEntry<K, V> entry) {
        removeEntry(entry);
        entry.prev = head;
        entry.next = head.next;
        updateEntry(entry);
    }

    // Make the entry the least recently used item.
    private void makeTail(LinkedEntry<K, V> entry) {
        removeEntry(entry);
        entry.prev = head.prev;
        entry.next = head;
        updateEntry(entry);
    }

    private static <K, V> void updateEntry(LinkedEntry<K, V> entry) {
        entry.next.prev = entry;
        entry.prev.next = entry;
    }

    private static <K, V> void removeEntry(LinkedEntry<K, V> entry) {
        entry.prev.next = entry.next;
        entry.next.prev = entry.prev;
    }

    private static class LinkedEntry<K, V> {
        private final K key;
        private List<V> values;
        LinkedEntry<K, V> next;
        LinkedEntry<K, V> prev;

        // Used only for the first item in the list which we will treat specially and which will not contain a value.
        public LinkedEntry() {
            this(null);
        }

        public LinkedEntry(K key) {
            next = prev = this;
            this.key = key;
        }
        //清理集合的最后一个元素
        public V removeLast() {
            final int valueSize = size();
            return valueSize > 0 ? values.remove(valueSize - 1) : null;
        }

        public int size() {
            return values != null ? values.size() : 0;
        }

        public void add(V value) {
            if (values == null) {
                values = new ArrayList<V>();
            }
            values.add(value);
        }
    }
}

SizeStrategy

下面以 SizeStrategy 来分析,这是 4.4 以上的复用策略,只要被复用的内存比需要申请的内存小

@TargetApi(Build.VERSION_CODES.KITKAT)
class SizeStrategy implements LruPoolStrategy {
    //被复用的Bitmap的内存必须大于需要申请内存的Bitmap的内存,但也要避免不要过大
    //例如分配 1M 空间的 Bitmap,确实没必要去复用以前分配的 100M 的可用 Bitmap 空间,所以这里的规定是 8 倍
    private static final int MAX_SIZE_MULTIPLE = 8;
    private final KeyPool keyPool = new KeyPool();
    private final GroupedLinkedMap<Key, Bitmap> groupedMap = new GroupedLinkedMap<Key, Bitmap>();
    // 使用 TreeMap 是为了能够按照 key(这里实际上是 Bitmap 的 size) 的大小进行排序,找到大于或等于目标 Bitmap 内存的复用内存空间
    private final TreeMap<Integer, Integer> sortedSizes = new PrettyPrintTreeMap<Integer, Integer>();

    @Override
    public void put(Bitmap bitmap) {
        int size = Util.getBitmapByteSize(bitmap);
        final Key key = keyPool.get(size);

        groupedMap.put(key, bitmap);

        Integer current = sortedSizes.get(key.size);
        sortedSizes.put(key.size, current == null ? 1 : current + 1);
    }

    @Override
    public Bitmap get(int width, int height, Bitmap.Config config) {
        final int size = Util.getBitmapByteSize(width, height, config); //目标 bitmap 大小,
        Key key = keyPool.get(size);

        Integer possibleSize = sortedSizes.ceilingKey(size);  //获取一个大于等于当前 size 的 Key
        if (possibleSize != null && possibleSize != size && possibleSize <= size * MAX_SIZE_MULTIPLE) {
            keyPool.offer(key); //这个 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;
    }

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

    private void decrementBitmapOfSize(Integer size) {
        Integer current = sortedSizes.get(size);
        if (current == 1) {
            sortedSizes.remove(size);
        } else {
            sortedSizes.put(size, current - 1);
        }
    }
    //..

    @Override
    public int getSize(Bitmap bitmap) {
        return Util.getBitmapByteSize(bitmap);
    }
    //..

    // 对象池,存储以 size 为标识的池对象
    static class KeyPool extends BaseKeyPool<Key> {

        public Key get(int size) {
            Key result = get();
            result.init(size);
            return result;
        }

        @Override
        protected Key create() {
            return new Key(this);
        }
    }

    // 以 size 为标识的池对象
    static final class Key implements Poolable {
        private final KeyPool pool;
        private int size;

        Key(KeyPool pool) {
            this.pool = pool;
        }

        public void init(int size) {
            this.size = size;
        }

        @Override
        public boolean equals(Object o) {
            if (o instanceof Key) {
                Key other = (Key) o;
                return size == other.size;
            }
            return false;
        }

        @Override
        public int hashCode() {
            return size;
        }

        @Override
        public String toString() {
            return getBitmapString(size);
        }

        @Override
        public void offer() {
            pool.offer(this);
        }
    }
}

Bitmap 大小的计算

/**
 * Returns the in memory size of the given {@link Bitmap} in bytes.
 */
@TargetApi(Build.VERSION_CODES.KITKAT)
public static int getBitmapByteSize(Bitmap bitmap) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        // Workaround for KitKat initial release NPE in Bitmap, fixed in MR1\. See issue #148.
        try {
            return bitmap.getAllocationByteCount();
        } catch (NullPointerException e) {
            // Do nothing.
        }
    }
    return bitmap.getHeight() * bitmap.getRowBytes();
}

参考

    原文作者:HelloCsl
    原文地址: https://www.jianshu.com/p/d6cae68175f2
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞