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 的内存
其中最重要的角色是 LruPoolStrategy
,是 Bitmap 内存复用策略的接口,有三个实现类 SizeConfigStrategy
、SizeStrategy
和 AttributeStrategy
在分析这个具体的复用策略前,先介绍一下其中实现 LRU 的数据结构,我们都知道 SDK 提供的 LRUCache
类可以帮助实现 LRU 功能,且内部的数据结构是 LinkedHashMap
。但 Glide
并没有使用现成的,而是定义了一个 GroupedLinkedMap
,其类似 LinkedHashMap
,其实体 LinedEntry
可以用于实现双向链表,并通过 GroupedLinkedMap#makeTail
/GroupedLinkedMap#makeHead
方法来改变链表头尾位置,使得具有 LRU 功能,且 LinedEntry
的 Value
为一个 List
集合,因为可能有多个相同 key
(例如:Bitmap Size)的 Bitmap
集合可以复用,而且可以控制移除队列最后一个对象,而不是整个 List
集合,这应该是不使用 LRUCache
的原因
class GroupedLinkedMap {
private final LinkedEntry head = new LinkedEntry();
private final Map> keyToEntry = new HashMap>();
public void put(K key, V value) {
LinkedEntry entry = keyToEntry.get(key);
if (entry == null) {
entry = new LinkedEntry(key);
makeTail(entry); //移到表尾
keyToEntry.put(key, entry);
} else {
key.offer(); //如果 key 相同,那么可以把现在的回收到对象池
}
entry.add(value);
}
public V get(K key) {
LinkedEntry entry = keyToEntry.get(key);
if (entry == null) {
entry = new LinkedEntry(key);
keyToEntry.put(key, entry);
} else {
key.offer();
}
makeHead(entry); //移到表头
return entry.removeLast();
}
public V removeLast() {
LinkedEntry 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 entry) {
removeEntry(entry);
entry.prev = head;
entry.next = head.next;
updateEntry(entry);
}
// Make the entry the least recently used item.
private void makeTail(LinkedEntry entry) {
removeEntry(entry);
entry.prev = head.prev;
entry.next = head;
updateEntry(entry);
}
private static void updateEntry(LinkedEntry entry) {
entry.next.prev = entry;
entry.prev.next = entry;
}
private static void removeEntry(LinkedEntry entry) {
entry.prev.next = entry.next;
entry.next.prev = entry.prev;
}
private static class LinkedEntry {
private final K key;
private List values;
LinkedEntry next;
LinkedEntry 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();
}
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 groupedMap = new GroupedLinkedMap();
// 使用 TreeMap 是为了能够按照 key(这里实际上是 Bitmap 的 size) 的大小进行排序,找到大于或等于目标 Bitmap 内存的复用内存空间
private final TreeMap sortedSizes = new PrettyPrintTreeMap();
@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 {
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();
}
参考
- GlideBitmapPool
- Glide
- Managing Bitmap Memory
- Android性能优化(五)之细说Bitmap