本文基于Glide 4.16.0版本,总结其中内存使用比较优秀的设计,包括对象池复用、内存缓存策略、Bitmap内存控制等方面。
BitmapPool — Bitmap对象池复用
在图片加载中,Bitmap的创建和销毁是内存开销最大的部分。Glide通过BitmapPool实现了Bitmap对象的复用,避免了频繁创建和GC。
1
2
3
4
5
|
public interface BitmapPool {
void put(Bitmap bitmap);
Bitmap get(int width, int height, Bitmap.Config config);
Bitmap getDirty(int width, int height, Bitmap.Config config);
}
|
默认实现是LruBitmapPool,它内部使用LRU策略管理Bitmap对象。当解码一张新图片时,不是直接new一个Bitmap,而是先从BitmapPool中获取一个尺寸和配置匹配的Bitmap来复用:
1
2
3
4
5
6
7
8
9
10
11
|
public class Downsampler {
private Resource<Bitmap> decodeFromWrappedStreams(InputStream is,
BitmapFactory.Options options, ...) {
//从bitmapPool中获取一个可复用的bitmap
if (expectedWidth > 0 && expectedHeight > 0) {
options.inBitmap = bitmapPool.getDirty(expectedWidth, expectedHeight, expectedConfig);
}
Bitmap downsampled = BitmapFactory.decodeStream(is, null, options);
//...
}
}
|
此处通过设置options.inBitmap告诉BitmapFactory复用已有的Bitmap内存,而不是重新分配。getDirty方法获取的Bitmap不会清空像素数据(因为解码会覆盖),比get方法更高效。
BitmapPool的大小计算
BitmapPool的最大缓存大小是在GlideBuilder中通过MemorySizeCalculator计算的:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class MemorySizeCalculator {
//屏幕一帧所需的字节数
private final int screenSize = widthPixels * heightPixels * BYTES_PER_ARGB_8888_PIXEL;
int getBitmapPoolSize() {
//低内存设备
if (isLowMemoryDevice) {
return screenSize * LOW_MEMORY_BYTE_ARRAY_POOL_DIVISOR;
}
//非低内存设备取屏幕大小的4倍
return Math.round(screenSize * BITMAP_POOL_TARGET_SCREENS);
}
}
|
其中BITMAP_POOL_TARGET_SCREENS默认是4,也就是BitmapPool最大缓存的内存大小等于4个屏幕的bitmap所占大小。这个设计的思路是,一般一个列表页面中可见的图片不会超过4屏,所以4倍屏幕大小可以满足大部分场景的Bitmap复用需求。如果是低内存设备(ActivityManager.isLowRamDevice()),会降低到更小的值。
LruBitmapPool内部策略
LruBitmapPool内部使用SizeConfigStrategy来管理Bitmap:
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
|
public class LruBitmapPool implements BitmapPool {
private final LruPoolStrategy strategy;
private long currentSize;
private final long maxSize;
@Override
public void put(Bitmap bitmap) {
if (bitmap.getAllocationByteCount() > maxSize) {
//如果单个bitmap大于池子的最大值,直接回收
bitmap.recycle();
return;
}
strategy.put(bitmap);
currentSize += bitmap.getAllocationByteCount();
//超出最大值就淘汰
trimToSize(maxSize);
}
@Override
public Bitmap getDirty(int width, int height, Bitmap.Config config) {
Bitmap result = strategy.get(width, height, config);
if (result == null) {
//没有可复用的,创建新的
result = createBitmap(width, height, config);
}
return result;
}
}
|
SizeConfigStrategy按照Bitmap的字节大小和Config组合作为key进行匹配。在get的时候,它不要求宽高完全匹配,只要求getAllocationByteCount()大于等于所需大小即可。找到后通过bitmap.reconfigure(width, height, config)重新配置尺寸,这样就可以最大程度复用Bitmap内存:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class SizeConfigStrategy implements LruPoolStrategy {
private final KeyPool keyPool = new KeyPool();
private final GroupedLinkedMap<Key, Bitmap> groupedMap = new GroupedLinkedMap<>();
//按照大小排序的TreeMap,用于快速查找大于等于目标大小的bitmap
private final Map<Bitmap.Config, NavigableMap<Integer, Integer>> sortedSizes = new HashMap<>();
@Override
public Bitmap get(int width, int height, Bitmap.Config config) {
int size = Util.getBitmapByteSize(width, height, config);
//从TreeMap中找到大于等于目标大小的最小key
Key bestKey = findBestKey(size, config);
Bitmap result = groupedMap.get(bestKey);
if (result != null) {
result.reconfigure(width, height, config);
}
return result;
}
}
|
这里使用了NavigableMap(TreeMap)这个数据结构,利用其ceilingKey方法可以以O(logN)的时间复杂度找到大于等于目标大小的最小可用Bitmap,避免了线性遍历。
ArrayPool — 字节数组池复用
除了Bitmap的复用之外,Glide还通过ArrayPool对byte[]和int[]数组进行池化复用,减少了解码、编码过程中频繁创建临时数组的GC开销:
1
2
3
4
5
6
|
public interface ArrayPool {
int STANDARD_BUFFER_SIZE_BYTES = 64 * 1024;
<T> T get(int size, Class<T> arrayClass);
<T> void put(T array);
}
|
默认实现是LruArrayPool。在前面文章中介绍过的StreamEncoder保存原始图片到磁盘的时候就用到了ArrayPool:
1
2
3
4
5
6
7
8
|
public class StreamEncoder implements Encoder<InputStream> {
public boolean encode(@NonNull InputStream data, @NonNull File file, @NonNull Options options) {
byte[] buffer = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
//...读写操作
byteArrayPool.put(buffer);
return success;
}
}
|
64KB的缓冲区如果每次都new一个,在大量图片加载场景下会产生频繁的GC。通过ArrayPool复用,避免了这个问题。
ArrayPool的大小计算
ArrayPool的大小也是通过MemorySizeCalculator计算的:
1
2
3
4
5
6
7
8
|
class MemorySizeCalculator {
int getArrayPoolSizeInBytes() {
if (isLowMemoryDevice) {
return LOW_MEMORY_BYTE_ARRAY_POOL_DIVISOR * ArrayPool.STANDARD_BUFFER_SIZE_BYTES;
}
return ARRAY_POOL_SIZE_BYTES; //4MB
}
}
|
默认分配4MB大小给ArrayPool,低内存设备会减半。4MB足以覆盖多个并发图片解码的缓冲区需求。
MemoryCache — LRU内存缓存大小控制
在前面缓存流程文章中介绍过内存缓存分为活跃缓存和LRU缓存。LRU缓存的最大大小也是通过MemorySizeCalculator计算:
1
2
3
4
5
6
|
class MemorySizeCalculator {
int getMemoryCacheSize() {
//非低内存设备:2个屏幕大小
return Math.round(screenSize * MEMORY_CACHE_TARGET_SCREENS);
}
}
|
MEMORY_CACHE_TARGET_SCREENS默认是2,即LRU内存缓存最大能存储2屏的Bitmap数据。这个值和BitmapPool的4倍一起确保了总内存占用在可控范围内。
内存总量计算与上限控制
MemorySizeCalculator最终会检查BitmapPool + MemoryCache + ArrayPool的总和是否超过了可用内存的上限:
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
|
class MemorySizeCalculator {
MemorySizeCalculator(MemorySizeCalculator.Builder builder) {
//获取应用可用内存
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
activityManager.getMemoryInfo(memoryInfo);
int maxSize = getMaxSize(activityManager, builder.maxSizeMultiplier, builder.lowMemoryMaxSizeMultiplier);
int arrayPoolSize = /* ... */;
int memoryCacheSize = /* ... */;
int bitmapPoolSize = /* ... */;
//如果总和超过maxSize,按比例缩小
int totalSize = memoryCacheSize + bitmapPoolSize;
if (totalSize > maxSize) {
float toRemove = totalSize - maxSize;
memoryCacheSize = Math.round(memoryCacheSize * (1 - toRemove / totalSize));
bitmapPoolSize = Math.round(bitmapPoolSize * (1 - toRemove / totalSize));
}
}
private int getMaxSize(ActivityManager activityManager, float maxSizeMultiplier, float lowMemoryMaxSizeMultiplier) {
final float multiplier = isLowMemoryDevice ? lowMemoryMaxSizeMultiplier : maxSizeMultiplier;
//应用可用最大堆内存 * 系数
int memoryClassBytes = activityManager.getMemoryClass() * 1024 * 1024;
return Math.round(memoryClassBytes * multiplier);
}
}
|
其中maxSizeMultiplier默认是0.4,lowMemoryMaxSizeMultiplier默认是0.33。也就是说,正常设备Glide的内存缓存(BitmapPool + MemoryCache)总量不会超过应用堆内存的40%,低内存设备不超过33%。如果按屏幕计算的值超过了这个上限,会按比例缩小,保证不会导致OOM。
Bitmap采样率下采样 — 从源头减少内存
Glide在解码图片时会根据目标View的尺寸计算采样率,避免加载过大的Bitmap到内存中:
1
2
3
4
5
6
7
8
9
10
11
12
|
public final class Downsampler {
private void calculateScaling(...) {
//计算采样率,使解码出来的bitmap尽量接近目标尺寸
int sampleSize = Math.min(sourceHeight / targetHeight, sourceWidth / targetWidth);
options.inSampleSize = Integer.highestOneBit(sampleSize);
//通过inTargetDensity和inDensity进行更精确的缩放
if (isScaling(options)) {
adjustTargetDensity(options, sourceWidth, sourceHeight, targetWidth, targetHeight);
}
}
}
|
假如原图是4000x3000像素,而目标ImageView只有400x300,如果不做下采样,Bitmap占用内存为4000*3000*4=45.7MB。通过inSampleSize=8下采样后,解码出500x375的Bitmap,只占用500*375*4=0.73MB,内存降低了约62倍。这是Glide内存优化中效果最显著的一环。
Resource包装与引用计数回收
Glide中所有资源都通过Resource接口进行包装,在前面缓存流程中讲到的EngineResource就使用了引用计数来管理资源的生命周期:
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
|
class EngineResource<Z> implements Resource<Z> {
private int acquired;
private final boolean isRecyclable;
private final Resource<Z> toWrap;
synchronized void acquire() {
++acquired;
}
void release() {
boolean release = false;
synchronized (this) {
if (--acquired == 0) {
release = true;
}
}
if (release) {
listener.onResourceReleased(key, this);
}
}
@Override
public void recycle() {
if (isRecyclable) {
toWrap.recycle();
}
}
}
|
通过acquired引用计数,确保同一张图片被多个View共享时不会被提前回收。当acquired降到0时说明没有View在使用该资源了,此时从活跃缓存移到LRU缓存中。只有当LRU缓存中也被淘汰时,才会真正调用recycle归还到BitmapPool中。这个分层回收机制确保了Bitmap不会被过早销毁,也不会在不使用时一直占据活跃内存。
内存裁剪 — 响应系统内存压力
Glide注册了ComponentCallbacks2来响应系统的内存裁剪事件,在内存紧张时主动释放缓存:
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
|
public class Glide implements ComponentCallbacks2 {
@Override
public void onTrimMemory(int level) {
trimMemory(level);
}
public void trimMemory(int level) {
Util.assertMainThread();
memoryCache.trimMemory(level);
bitmapPool.trimMemory(level);
arrayPool.trimMemory(level);
}
@Override
public void onLowMemory() {
clearMemory();
}
public void clearMemory() {
Util.assertMainThread();
memoryCache.clearMemory();
bitmapPool.clearMemory();
arrayPool.clearMemory();
}
}
|
当系统回调onTrimMemory时,Glide会根据level级别来裁剪内存缓存。在LruBitmapPool中的处理:
1
2
3
4
5
6
7
8
9
10
11
|
public class LruBitmapPool implements BitmapPool {
@Override
public void trimMemory(int level) {
if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
clearMemory();
} else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
|| level == android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE) {
trimToSize(getMaxSize() / 2);
}
}
}
|
TRIM_MEMORY_BACKGROUND:应用进入后台,清空所有缓存
TRIM_MEMORY_UI_HIDDEN或TRIM_MEMORY_RUNNING_MODERATE:裁剪到最大值的一半
这样在系统内存紧张时,Glide能及时让出内存资源,减少被系统杀死的风险。
在前面编解码文章中提到的InputStreamRewinder中使用了RecyclableBufferedInputStream,它是Glide对BufferedInputStream的改造版:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class RecyclableBufferedInputStream extends FilterInputStream {
private volatile byte[] buf;
public RecyclableBufferedInputStream(@NonNull InputStream in, @NonNull ArrayPool arrayPool) {
super(in);
buf = arrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
}
public void fixMarkLimit() {
marklimit = buf.length;
}
public void release() {
//归还缓冲区到ArrayPool
byte[] localBuf = buf;
if (localBuf != null) {
arrayPool.put(localBuf);
}
buf = null;
}
}
|
标准的BufferedInputStream每次创建都会new一个内部缓冲区byte[]。RecyclableBufferedInputStream的缓冲区是从ArrayPool中获取的,用完后归还给ArrayPool,避免了频繁创建和回收byte数组的开销。在大量图片并发加载的场景下,这个优化可以显著减少GC压力。
总结
Glide在内存管理上的核心思路可以概括为以下几点:
- 对象池复用:通过BitmapPool复用Bitmap对象,通过ArrayPool复用byte[]数组,减少对象创建和GC开销。
- 按需解码:通过inSampleSize和density缩放,只解码目标尺寸的Bitmap,从源头减少内存占用。
- 分层缓存:活跃缓存(弱引用)+ LRU缓存(强引用)的两层内存缓存设计,既保证正在使用的图片不被回收,也能在不使用时快速释放。
- 引用计数:通过EngineResource的acquired计数,精确跟踪资源使用情况,实现自动的缓存层级迁移。
- 自适应内存:根据设备内存情况动态计算各缓存池大小,并设置总量上限(不超过堆内存的40%),防止OOM。
- 响应系统压力:通过ComponentCallbacks2监听系统内存压力事件,在后台或内存紧张时主动释放缓存。