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

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容