Glide中内存统计总结

本文基于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_HIDDENTRIM_MEMORY_RUNNING_MODERATE:裁剪到最大值的一半

这样在系统内存紧张时,Glide能及时让出内存资源,减少被系统杀死的风险。

RecyclableBufferedInputStream — 可复用的缓冲流

在前面编解码文章中提到的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监听系统内存压力事件,在后台或内存紧张时主动释放缓存。
Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计