前言
Bitmap加载不当导致内存占用过大或者内存抖动,可能是每个Android工程师无法逃避的问题,如何合理的管理Bitmap内存,几乎成了各位必备的功课;好在Android官方早已经为我们提供了解决思路;
本章主要内容:
Bitmap
内存管理和复用的简介LruBitmapPool
的基本分析AttributeStrategy
关键逻辑分析SizeConfigStrategy
关键逻辑分析GroupedLinkedMap
源码细致分析MemorySizeCalculator
对size的基本计算
Bitmap内存管理和复用
Android对Bitmap的内存管理
在Android2.2或更低的版本中,当发生垃圾回收时,线程都会暂停执行。这会导致延迟,降低程序性能。Android2.3增加了并发的垃圾回收机制,这意味着当图片对象不再被引用时所占用的内存空间立即就会被回收。
在Android2.3.3或更低版本中,Bitmap的像素数据是存储在Native内存中,它和Bitmap对象本身是分开来的,Bitmap对象本身是存储在Java虚拟机堆内存中。存储在本地内存中的像素数据的释放是不可预知的,这样就有可能导致应用短暂的超过其内存限制而崩溃。Android3.0之后,Bitmap像素数据和它本身都存储在Java虚拟机的堆内存中。到了Android8.0及其以后,Bitmap有重新存储到Native堆中。
在Android2.3.3或更低版本中,调用recycle()
方法来尝试对Bitmap进行回收;
在Android3.0或者更高的版本中,使用BitmapFactory.Options.inBitmap
来尝试对Bitmap进行复用,但是复用Bitmap是有条件限制的;
复用Bitmap的限制
参考官方文档
- 首先Bitmap必须是mutable,可以通过
BitmapFactory.Options
设置; - 在Android4.4版本之前,仅支持jpg、png图片,且size大小一样,且
inSampleSize=1
的Bitmap才可以复用,inPreferredConfig需要设置成与目标Config一致; - 在Android4.4之后的版本,只要内存大小不小于需求的Bitmap都可以复用.
- Bitmap.Config.HARDWARE不需要复用;
既然想复用Bitmap,就需要有集合来存储这些Bitmap,在Glide中,BitmapPool
就是干这事的。
Glide中的BitmapPool
BitmapPool
是Glide中对Bitmap复用进行统一管理的接口,原则上所有需要创建Bitmap的操作,都要经过它来进行获取,BitmapPool的类关系图如下:
(缺个图)
BitmapPool
是一个接口,实现类有BitmapPoolAdapter
和LruBitmapPool
这两个;BitmapPoolAdapter
是一个空壳子,根本没有做实际意义上的缓存操作;LruBitmapPool
采用策略模式,它自身不处理具体逻辑,真正的逻辑在LruPoolStrategy
中;
LruBitmapPool
LruBitmapPool是策略的执行者,也是缓存大小的控制者;
LruBitmapPool.java
1 | public class LruBitmapPool implements BitmapPool{ |
LruBitmapPool
主要方法:
构造方法
:需要传入maxSize
,这个是控制缓存大小的必要参数;
put()
:将Bitmap进行缓存,如果不满足缓存条件,执行回收;
getDirtyOrNull()
:从缓存中获取Bitmap,可能返回空;
trimToSize()
:对内存重新整理,防止超出目标size;
get()
:获取一个全透明像素的bitmap,不为空;
getDirty()
:直接获取,如果取自缓存,可能包含脏数据,不为空;
其中操作缓存的核心方法在strategy
中,LruPoolStrategy
也是一个抽象的策略接口,真正策略的实现类是SizeConfigStrategy
和AttributeStrategy
;
LruPoolStrategy
LruPoolStrategy.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17interface LruPoolStrategy {
//put操作
void put(Bitmap bitmap);
//get操作
@Nullable
Bitmap get(int width, int height, Bitmap.Config config);
@Nullable
Bitmap removeLast();
String logBitmap(Bitmap bitmap);
String logBitmap(int width, int height, Bitmap.Config config);
int getSize(Bitmap bitmap);
}
通过调用getDefaultStrategy()
方法获得LruPoolStrategy实例:
getDefaultStrategy()1
2
3
4
5
6
7
8
9private static LruPoolStrategy getDefaultStrategy() {
final LruPoolStrategy strategy;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
strategy = new SizeConfigStrategy();
} else {
strategy = new AttributeStrategy();
}
return strategy;
}
SizeConfigStrategy
是针对Android4.4及其以上版本的策略,AttributeStrategy
则是低版本的策略;
首先来看低版本的策略:
AttributeStrategy
AttributeStrategy.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class AttributeStrategy implements LruPoolStrategy {
private final KeyPool keyPool = new KeyPool();//KeyPool
private final GroupedLinkedMap<Key, Bitmap> groupedMap = new GroupedLinkedMap<>();//真正的LRU CACHE
@Override
public void put(Bitmap bitmap) {
//获取Key
final Key key = keyPool.get(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig());
//保存
groupedMap.put(key, bitmap);
}
@Override
public Bitmap get(int width, int height, Bitmap.Config config) {
//获取Key
final Key key = keyPool.get(width, height, config);
//取出
return groupedMap.get(key);
}
//移除末尾的元素
@Override
public Bitmap removeLast() {
return groupedMap.removeLast();
}
}
AttributeStrategy
重写接口定义的方法,其中GroupedLinkedMap
是真正实现Lru逻辑的集合,值得注意的是Key
的获取在KeyPool
中,Key
作为对入参int width, int height, Bitmap.Config config
的封装,也是Lru缓存的键;
AttributeStrategy.Key重写equals()
方法和hashCode()
方法,其中hashCode()
是用来识别Lru内部LinkedHashMap
中的bucket
,equal()
是真正的对比;
AttributeStrategy.Key
1 | { |
AttributeStrategy
这个策略的核心目的,就是在缓存的Key
上面做对比,只有缓存中的Bitmap同时满足width
、height
、config
相等才能命中;
SizeConfigStrategy
上面说了AttributeStrategy
是面向低于Android4.4版本的Bitmap缓存策略,SizeConfigStrategy
则是面向高版本的,从文章开头的部分我们知道,高版本的inBitmap
限制没有这么严格,至少在尺寸这一块是放开了,只有内存大小不小于需求就行;下面看看代码怎么实现的:
SizeConfigStrategy.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70public class SizeConfigStrategy implements LruPoolStrategy {
private static final int MAX_SIZE_MULTIPLE = 8;
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) {
//获取bitmap的像素占用字节数
int size = Util.getBitmapByteSize(bitmap);
//获取key
Key key = keyPool.get(size, bitmap.getConfig());
//保存到LRU
groupedMap.put(key, bitmap);
//获取sizeConfig Map
NavigableMap<Integer, Integer> sizes = getSizesForConfig(bitmap.getConfig());
//保存键值对,键是字节数大小,值是总共有多少个
Integer current = sizes.get(key.size);
sizes.put(key.size, current == null ? 1 : current + 1);
}
public Bitmap get(int width, int height, Bitmap.Config config) {
//获取字节数
int size = Util.getBitmapByteSize(width, height, config);
//获取最优的key
Key bestKey = findBestKey(size, config);
//从LRU中获取
Bitmap result = groupedMap.get(bestKey);
if (result != null) {
//操作sizeConfig集合,做减1操作或者移除
decrementBitmapOfSize(bestKey.size, result);
//重新计算Bitmap宽高和config
result.reconfigure(width, height,
result.getConfig() != null ? result.getConfig() : Bitmap.Config.ARGB_8888);
}
return result;
}
//获取SizesForConfig Map
private NavigableMap<Integer, Integer> getSizesForConfig(Bitmap.Config config) {
NavigableMap<Integer, Integer> sizes = sortedSizes.get(config);
if (sizes == null) {
//treeMap
sizes = new TreeMap<>();
sortedSizes.put(config, sizes);
}
return sizes;
}
//Key的组成
static final class Key{
@Override
public boolean equals(Object o) {
if (o instanceof Key) {
Key other = (Key) o;
return size == other.size
&& Util.bothNullOrEqual(config, other.config);
}
return false;
}
@Override
public int hashCode() {
int result = size;
result = 31 * result + (config != null ? config.hashCode() : 0);
return result;
}
}
}
}
SizeConfigStrategy
和AttributeStrategy
有很多相似之处,但是复杂的多,相同的是都是用GroupedLinkedMap
作为Lru存储,不同之处是对于Key
的获取以及多出一个辅助集合NavigableMap
;Key
的获取已经不依赖Width
和Height
了,而是size
,它是Bitmap占用的字节数,Key
的hashCode()
和equals()
依赖的是size
和config
;
SizeConfigStrategy
最关键的方法是getBestKey()
,它的作用是获取最合适的Key;
SizeConfigStrategy.findBestKey()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71//获取最适合的Key
private Key findBestKey(int size, Bitmap.Config config) {
//从pool里取出,肯定不为空
Key result = keyPool.get(size, config);
//获取匹配的Config,一般只有一个匹配
for (Bitmap.Config possibleConfig : getInConfigs(config)) {
//获取sizesForConfig
NavigableMap<Integer, Integer> sizesForPossibleConfig = getSizesForConfig(possibleConfig);
//获取不比size小的可能缓存的size,ceiling方法相当于是数学上的进一法
Integer possibleSize = sizesForPossibleConfig.ceilingKey(size);
//命中的size不能大于目标size的8倍,可能是担心浪费内存;
if (possibleSize != null && possibleSize <= size * MAX_SIZE_MULTIPLE) {
//`size`不相等或者`config`不相等,此处的判断等于是判断了`!Key.equals()`逻辑,这时候才降低维度获取相近的key
if (possibleSize != size
|| (possibleConfig == null ? config != null : !possibleConfig.equals(config))) {
//接受相近的缓存key,第一步创建的key放入队列
keyPool.offer(result);
//命中的key,他的size和目标相近但是肯定不完全一样
result = keyPool.get(possibleSize, possibleConfig);
}
break;
}
}
return result;
}
//获取能匹配上的config
private static Bitmap.Config[] getInConfigs(Bitmap.Config requested) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (Bitmap.Config.RGBA_F16.equals(requested)) {
return RGBA_F16_IN_CONFIGS;
}
}
switch (requested) {
case ARGB_8888:
return ARGB_8888_IN_CONFIGS;
case RGB_565:
return RGB_565_IN_CONFIGS;
case ARGB_4444:
return ARGB_4444_IN_CONFIGS;
case ALPHA_8:
return ALPHA_8_IN_CONFIGS;
default:
return new Bitmap.Config[] { requested };
}
}
//8888能匹配8888,大于等于Android O 能匹配RGBA_F16
private static final Bitmap.Config[] ARGB_8888_IN_CONFIGS;
static {
Bitmap.Config[] result =
new Bitmap.Config[] {
Bitmap.Config.ARGB_8888,
null,
};
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
result = Arrays.copyOf(result, result.length + 1);
result[result.length - 1] = Config.RGBA_F16;
}
ARGB_8888_IN_CONFIGS = result;
}
//RGBA_F16_IN_CONFIGS和ARGB_8888_IN_CONFIGS一样
private static final Bitmap.Config[] RGBA_F16_IN_CONFIGS = ARGB_8888_IN_CONFIGS;
//565匹配565
private static final Bitmap.Config[] RGB_565_IN_CONFIGS =
new Bitmap.Config[] { Bitmap.Config.RGB_565 };
//4444匹配4444
private static final Bitmap.Config[] ARGB_4444_IN_CONFIGS =
new Bitmap.Config[] { Bitmap.Config.ARGB_4444 };
//ALPHA_8匹配ALPHA_8
private static final Bitmap.Config[] ALPHA_8_IN_CONFIGS =
new Bitmap.Config[] { Bitmap.Config.ALPHA_8 };
getBestKey()
主要是通过getInConfigs()
拿到能匹配到的sizesForPossibleConfig
,通过辅助集合NavigableMap
拿到size相近的possibleSize
;
能匹配的第一个条件是possibleSize要小于等于
size * MAX_SIZE_MULTIPLE
,MAX_SIZE_MULTIPLE
默认是8;如果大于8对内存的利用率很低,没有必要强制匹配缓存;如果
sizesForPossibleConfig
和possibleSize
有一个不和目标相等,就可以复用,否则说明两者的key肯定相等(参考Key.equals()
方法),两者相等没有必须再进行经纬度的匹配,直接返回就行;
辅助Map
在回头看看这个NavigableMap
,通过调用getSizesForConfig()
得到一个TreeMap
,这个Map保存了每个缓存的Bitmap的size和相同size的count,在getBestKey()
方法中调用ceilingKey(size)
方法,TreeMap
默认会对key进行自然排序,ceilingKey(size)
函数的意义是返回一个和size最接近的不小于size的key,正好符合内存复用的价值;而
疑问:为啥要用Map来保存size和该size对应的count,count有何用?
SizeConfigStrategy中有这么一个方法:decrementBitmapOfSize()
;1
2
3
4
5
6
7
8
9
10
11
12
13
14private 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) {
}
if (current == 1) {
//移除掉该条数据
sizes.remove(size);
} else {
//减1
sizes.put(size, current - 1);
}
}
该方法调用时机是当Bitmap
从是缓存池中取出或者移除时,执行内容:操作该map,被移除的Bitmap对应的size减1或者把当前key移除,只有移除掉,在getBestKey()
调用ceilingKey(size)
时才知道该size在缓存中是否存在;
GroupedLinkedMap
BitmapPool真正实现LruCache功能的是GroupedLinkedMap
,这个类的功能跟LinkedHashMap
很相似但又不同,相同的是都是利用链表来记住数据访问顺序,不同的是该类把相同key的value保存到一个数组中;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122class GroupedLinkedMap<K extends Poolable, V> {
//LinkedEntry是存入Map的节点,同时是一个双向链表,同时还是持有一个数组
private static class LinkedEntry<K, V> {
@Synthetic final K key;//key
private List<V> values;//value数组
LinkedEntry<K, V> next;//链表下一个节点
LinkedEntry<K, V> prev;//链表上一个节点
//构造
LinkedEntry() {
this(null);
}
//构造
LinkedEntry(K key) {
next = prev = this;
this.key = key;
}
//移除数组的最后一个元素
@Nullable
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);
}
}
//头节点
private final LinkedEntry<K, V> head = new LinkedEntry<>();
//存储key和entry的HashMap
private final Map<K, LinkedEntry<K, V>> keyToEntry = new HashMap<>();
//放入
public void put(K key, V value) {
LinkedEntry<K, V> entry = keyToEntry.get(key);
if (entry == null) {
//创建结点
entry = new LinkedEntry<>(key);
//放到链表尾部
makeTail(entry);
//放到hashMap中
keyToEntry.put(key, entry);
} else {
key.offer();//keyPool的操作
}
//放入entry数组中
entry.add(value);
}
//获取操作
public V get(K key) {
//从HashMap中查找
LinkedEntry<K, V> entry = keyToEntry.get(key);
if (entry == null) {
//如果不存在,创建结点,放到hashMap中
entry = new LinkedEntry<>(key);
keyToEntry.put(key, entry);
} else {
key.offer();//keyPool的操作
}
//放到链表头部
makeHead(entry);
return entry.removeLast();//返回数组的最后一个
}
//设成链表头(其实就是head的下一个)
private void makeHead(LinkedEntry<K, V> entry) {
removeEntry(entry);
entry.prev = head;
entry.next = head.next;
updateEntry(entry);
}
//设成链表尾(其实就是head的上一个)
private void makeTail(LinkedEntry<K, V> entry) {
//把自己从链表中移除
removeEntry(entry);
//绑定自身的关系
entry.prev = head.prev;
entry.next = head;
//绑定自身前后节点与自己的关系
updateEntry(entry);
}
//更新节点,把当前节点的上一个的next指向自己,下一个的perv指向自己,完成双向链表
private static <K, V> void updateEntry(LinkedEntry<K, V> entry) {
entry.next.prev = entry;
entry.prev.next = entry;
}
//删除当前节点,把自己上一个的next指向下一个,把自己下一个的prev指向上一个
private static <K, V> void removeEntry(LinkedEntry<K, V> entry) {
entry.prev.next = entry.next;
entry.next.prev = entry.prev;
}
//移除队尾的元素
public V removeLast() {
//获取队尾节点
LinkedEntry<K, V> last = head.prev;
//这一块的whild循环有意思
while (!last.equals(head)) {
//移除改节点数组的最后一个
V removed = last.removeLast();
if (removed != null) {//如果不为空直接返回
return removed;
} else {
//如果走到这里,说明last节点底下的数组为空,所以根本没有移除掉数据,第一件事就是干掉这个节点
removeEntry(last);
keyToEntry.remove(last.key);
last.key.offer();
}
//走到这一步还是因为last节点底下的数组为空,继续探寻它的上一个节点,直到能return出去为止
last = last.prev;
}
return null;
}
}
GroupedLinkedMap
的代码量并不大,我在代码里做了比较详细的注释,如果有解释不当之处,还请留言交流;
BitmapPool缓存大小的计算
首先,BitmapPool
相对Glide对象是单例,在GlideBuilder.build()
中创建,构造方法中需要传maxSize
,maxSize
的计算规则是从MemorySizeCalculator.getBitmapPoolSize()
获得;
GlideBuilder1
2
3
4
5
6
7
8
9
10//BitmapPool的创建
if (bitmapPool == null) {
//通过memorySizeCalculator获取bitmapPoolSize
int size = memorySizeCalculator.getBitmapPoolSize();
if (size > 0) {
bitmapPool = new LruBitmapPool(size);
} else {
bitmapPool = new BitmapPoolAdapter();
}
}
通过memorySizeCalculator获取size
,
如果size
等于0时,创建BitmapPoolAdapter
,否则创建LruBitmapPool
,什么时候情况下size
等于0?我们还是看一些memorySizeCalculator
的定义;
MemorySizeCalculator
1 | //构造方法,Builder模式 |
首先,MemorySizeCalculator
是Builder模式,主要的参数是在MemorySizeCalculator.Builder
中生成,在MemorySizeCalculator
构造方法中对Glide
所有内存缓存的计算,这包括arrayPool
缓存的大小,bitmapPool
缓存的大小,memoryCache
缓存的大小。
我们主要讨论BitmapPool的size计算,在构造方法中,targetBitmapPoolSize
的计算规则是屏幕尺寸的像素大小 * builder.bitmapPoolScreens
;
其次MemorySizeCalculator
还会根据builder
的配置得到最大的缓存容量maxSize
;
最后,会重新计算targetBitmapPoolSize
,使其不超出最大容量;
接下来看一下MemorySizeCalculator.Builder
中对bitmapPoolScreens
的计算:
MemorySizeCalculator.Builder1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public static final class Builder {
static final int BITMAP_POOL_TARGET_SCREENS =
Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 4 : 1;
float bitmapPoolScreens = BITMAP_POOL_TARGET_SCREENS;
public Builder(Context context) {
this.context = context;
activityManager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
screenDimensions =
new DisplayMetricsScreenDimensions(context.getResources().getDisplayMetrics());
// On Android O+ Bitmaps are allocated natively, ART is much more efficient at managing
// garbage and we rely heavily on HARDWARE Bitmaps, making Bitmap re-use much less important.
// We prefer to preserve RAM on these devices and take the small performance hit of not
// re-using Bitmaps and textures when loading very small images or generating thumbnails.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isLowMemoryDevice(activityManager)) {
//在Android O上面,低内存的手机bitmapPoolScreens设置为0
bitmapPoolScreens = 0;
}
}
}
bitmapPoolScreens
的值在这三种情况:
- 如果设备小于Android O,取值4;
- 如果设备大于等于Android O,低内存手机,取值0;
- 如果设备大于等于Android O,非低内存手机,取值1;
至于为啥要取值0,Glide的解释是Android O上面Bitmap内存的申请在native,ART虚拟机对垃圾回收非常高效,而且我们可以用设置BitmapConfig.HARDWARE
,所以对于Bitmap的缓存不是那么的重要。
总结
随着Android各个版本对Bitmap的不断进化,Glide也在不断的适应新的特性,高版本对Bitmap的复用也在不断地放松,或许有一天,我们不再为Bitmap
内存问题所困扰,是不是就可以放弃这令人头大的Pool;