Bitmap内存复用,你要的细节全在这里

⏰ : 全文字数:10492+

🥅 : 内容关键字:Glide,Bitmap复用,内存优化

🤙 : 公_众_号:七郎的小院

🥅 : 更多文章,博客:blog.softgeek.cn

Android Bitmap 内存存储的演变过程

Android 随着版本的变化,它的内存分配一直在变化,具体变化如下:

  • 在 Android 2.3.3(API 级别 10)及更低版本上,Bitmap 的像素数据存储在 Native 内存中。它与存储在 Dalvik 堆中的 Bitmap 本身是分开的。Native 内存中的像素数据并不以可预测的方式释放,可能会导致应用短暂超出其内存限制并崩溃。
  • 从 Android 3.0(API 级别 11)到 Android 7.1(API 级别 25),像素数据会与关联的 Bitmap 一起存储在 Dalvik 堆上。
  • 在 Android 8.0(API 级别 26)及更高版本中,Bitmap 像素数据存储在 Native 堆中。

Bitmap 复用原理

Android 3.0(API 级别 11)引入了 BitmapFactory.Options.inBitmap 字段。如果设置了此选项,那么采用 Options 对象的解码方法会在加载内容时,尝试重复使用现有 Bitmap。这意味着 Bitmap 的内存得到了重复使用,从而提高了性能,同时避免了内存分配和取消分配。但是呢,因为 Android 版本碎片化的原因,复用的条件在不同的版本不一样。根据官网的解释有两种区别:分别是 Android 4.4 之前和 Android 4.4 之后

Android 4.4 之前

Build.VERSION_CODES.KITKAT 之前,适用其他约束:

  • 正在解码的图像(无论是作为资源还是作为流)必须是 jpeg 或 png 格式。
  • 仅支持相同大小的位图
  • 并将 inSampleSize 设置为 1。
  • 重用位图的 configuration 将覆盖 inPreferredConfig 的设置(如果设置)。

Android 4.4 之后

Build.VERSION_CODES.KITKAT 开始, BitmapFactory 可以重用任何可变 Bitmap 来解码任何其他 Bitmap,只要解码 Bitmap 的内存大小 byte count 小于或等于到复用 Bitmap 的 allocated byte count 。这可能是因为固有尺寸较小,或者缩放后的尺寸(对于密度/样本大小)较小。

我们看下官网中关于这块的代码示例:

java 复制代码
  if ((options.inSampleSize == 1 || isKitKatOrGreater) && shouldUsePool(imageType)) {
    .......
  }
  private boolean shouldUsePool(ImageType imageType) {
    // On KitKat+, any bitmap (of a given config) can be used to decode any other bitmap
    // (with the same config).
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
      return true;
    }
    return TYPES_THAT_USE_POOL_PRE_KITKAT.contains(imageType);
  }
  private static final Set<ImageHeaderParser.ImageType> TYPES_THAT_USE_POOL_PRE_KITKAT =
      Collections.unmodifiableSet(
          EnumSet.of(
              ImageHeaderParser.ImageType.JPEG,
              ImageHeaderParser.ImageType.PNG_A,
              ImageHeaderParser.ImageType.PNG));

所以根据上面的两种不同策略,可以设计一套 Bitmap 的内存缓存池,使得 Bitmap 的内存可以重复使用,从而提高了性能,同时避免了内存分配和取消分配。但是怎么设计呢?

这里大家可以停顿几秒钟,想想如果让自己设计,该如何设计这一套缓存..............

好了,我们一起看下在 Glide 库是如何设计的。

Glide 缓存池实现

Glide 在构建 Bitmap 缓存池的时候,就对这两种思路进行了实现。它使用策略模式,对这两种模式实现了两种策略,根据版本的不同,来得到对应的策略,具体可以看LruBitmapPool#getDefaultStrategy的实现

java 复制代码
  private static LruPoolStrategy getDefaultStrategy() {
    final LruPoolStrategy strategy;
    // 版本在4.4以上,使用SizeConfigStrategy策略
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
      strategy = new SizeConfigStrategy();
    } else {
      // 版本在4.4以下,使用AttributeStrategy策略
      strategy = new AttributeStrategy();
    }
    return strategy;
  }

从上面可以看到,版本在 4.4 以上,使用SizeConfigStrategy策略, 版本在 4.4 以下,使用AttributeStrategy策略。

GroupedLinkedMap

因为这里涉及到一个存储 key-value 的结构 GroupedLinkedMap,需要提前看下Glide 内存优化之 GroupedLinkedMap这篇文章,了解下的原理。

AttributeStrategy(4.4 之前)

put 缓存 Bitmap

我们看下 4.4 以下的策略,是如何缓存 Bitmap 的

java 复制代码
  public void put(Bitmap bitmap) {
    final Key key = keyPool.get(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig());

    groupedMap.put(key, bitmap);
  }

这个逻辑比较简单:

  • 利用 Bitmap 的 width,height,config 构建 Key
  • 然后把 Bitmap 存放到 GroupedLinkedMap

这里的 Key 又是啥呢?我们看下它的实现:

java 复制代码
  static class Key implements Poolable {
    ......

    public void init(int width, int height, Bitmap.Config config) {
      this.width = width;
      this.height = height;
      this.config = config;
    }

    @Override
    public boolean equals(Object o) {
      if (o instanceof Key) {
        Key other = (Key) o;
        // 可以看到只有当withd,height,config都相等,key才相等
        return width == other.width && height == other.height && config == other.config;
      }
      return false;
    }
  }

Keyequals 方法可以看到,只有当 width,height,config 都相等的情况,Key 才相等。这里响应了 Android 4.4 Bitmap 可缓存的条件。

get 获取可复用的 Bitmap

java 复制代码
  @Override
  public Bitmap get(int width, int height, Bitmap.Config config) {
    // 根据宽度和高度,config,获取一个key
    final Key key = keyPool.get(width, height, config);
    // 根据Key找到对应的对象
    return groupedMap.get(key);
  }

获取的逻辑也比较简单,主要步骤

  • 根据要创建的 Bitmap 的 with,height,config,组建 key
  • 根据 key 到缓存池中获取是否有可复用的 Bitmap

总结:在 Android 4.4 的版本之前,复用的整体逻辑比较简单,就是比较宽度,高度,config 是否相等,相等就复用,否则返回 null。

SizeConfigStrategy(4.4 以后)

在上面说了,在 4.4 版本之后,如果要复用,只要解码 Bitmap 的内存大小 byte count 小于或等于到复用 Bitmap 的 allocated byte count 。虽然这个复用的条件比较简单,但是实现的好,就比较复杂了。这里考虑一个问题:

如果一个 Bitmap 缓存池中,有多个内存大于目标 Bitmap 的图片,选哪个才能最高效,最节省内存?比如有内存中存在 2 个 Bitmap 对象,分别是 5M,100M,目标 Bitmap 的大小是 4M,那么取那一张作为复用?很明显是取 5M 的那张最优。所以这里涉及针对对象内存大小择优的问题,要如何设计?

在 Glide 中,使用了多个数据结构来解决这些问题:

java 复制代码
public class SizeConfigStrategy implements LruPoolStrategy {
  ......
  private final GroupedLinkedMap<Key, Bitmap> groupedMap = new GroupedLinkedMap<>();
  private final Map<Bitmap.Config, NavigableMap<Integer, Integer>> sortedSizes = new HashMap<>();
  ......
}

从上面可以看到:

  • 内部使用 groupedMap 结构存储 key-Bitmap 的数据,关于GroupedLinkedMap可以看我之前写的一篇文章Glide 内存优化之 GroupedLinkedMap
  • 使用 sortedSizes 存放同一个 Bitmap.Config 下,各个图片信息。这些信息包含图片的内存大小,可能有很多大小一样的图,所以又记录了每个大小在缓存池中的个数。使用了 NavigableMap 也就是 TreeMap 保存了图片大小和这个大小的图片有几个(数量)。当数据插入时,会按照大小排序。

记录了这些数据后,就能在查找过程中,通过比较大小、配置来查找合适的图。

Key

因为采用了 Key-Bitmap 的方式,所以这里涉及到如何设计 Key。我们看下:

java 复制代码
 static final class Key implements Poolable {
    .....

    @VisibleForTesting
    Key(KeyPool pool, int size, Bitmap.Config config) {
      this(pool);
      init(size, config);
    }
    .....
    @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;
    }
 }

从上面可以看到,逻辑比较简单,通过 Keyequals 方法可以看到,只有当 size相等,config相等或者都为 null 的情况,Key 才相等。

put 方法

同样,先看他的 put 方法,

java 复制代码
  @Override
  public void put(Bitmap bitmap) {
    int size = Util.getBitmapByteSize(bitmap);
    Key key = keyPool.get(size, bitmap.getConfig());

    groupedMap.put(key, bitmap);

    NavigableMap<Integer, Integer> sizes = getSizesForConfig(bitmap.getConfig());
    Integer current = sizes.get(key.size);
    // 记录对应Bitmap大小的记录加1
    sizes.put(key.size, current == null ? 1 : current + 1);
  }
  • 先获取要缓存的 Bitmap 的内存大小
  • 然后用内存大小 size 和配置 config,构建一个 key
  • 然后保存到 GroupedLinkedMap 结构中,也就是缓存到内存中保存

继续往下看,它在缓存完 Bitmap 之后,下面还有一个逻辑:

java 复制代码
    NavigableMap<Integer, Integer> sizes = getSizesForConfig(bitmap.getConfig());
    Integer current = sizes.get(key.size);
    // 记录对应Bitmap大小的记录加1
    sizes.put(key.size, current == null ? 1 : current + 1);

这个是干嘛的呢?这个就是我们上面说的,按照 Bitmap 的 config,size 两个维度记录图片的信息,因为我们在查找复用条件的时候,就需要根据 config 和 size 的值来判断是否可以缓存。主要特性:

  • 使用上面说的Map<Bitmap.Config, NavigableMap<Integer, Integer>>数据结构,把 Bitmap 的 Config 作为 key,Bitmap 的内存大小作为 value 保存下来。因为同一个配置可能对应多个 不同大小的大小的图片,所以这里使用一个TreeMap来保存,这个TreeMap会按照大小进行排序。
  • 如果相同大小和 Config 的图片存在,则数量加 1,否则数量就是 -1(注意这里不会替换)

get 获取复用 Bitmap

java 复制代码
public class SizeConfigStrategy implements LruPoolStrategy {
  ......
  @Override
  @Nullable
  public Bitmap get(int width, int height, Bitmap.Config config) {
    int size = Util.getBitmapByteSize(width, height, config);
    Key bestKey = findBestKey(size, config);
    // 从对应的大小中使用Bitmap
    Bitmap result = groupedMap.get(bestKey);
    if (result != null) {
      // Decrement must be called before reconfigure.
      decrementBitmapOfSize(bestKey.size, result);
      result.reconfigure(width, height, config);
    }
    return result;
  }
// 如果Bitmap复用了,那么需要删除掉对应Bitmap
  private void decrementBitmapOfSize(Integer size, Bitmap removed) {
    Bitmap.Config config = removed.getConfig();
    // 找到对应config存储的大小的Map
    NavigableMap<Integer, Integer> sizes = getSizesForConfig(config);
    // 找到这个大小的元素
    Integer current = sizes.get(size);
    // 如果只剩一个了,就表示已有对应config和大小的元素,可以删除了
    if (current == 1) {
      sizes.remove(size);
    } else {
      // 减去1
      sizes.put(size, current - 1);
    }
  }

  // 根据config得到对应的缓存中不同大小的Bitmap,是一个TreeMap的结构,可以按照大小进行排序
  private NavigableMap<Integer, Integer> getSizesForConfig(Bitmap.Config config) {
    NavigableMap<Integer, Integer> sizes = sortedSizes.get(config);
    if (sizes == null) {
      sizes = new TreeMap<>();
      sortedSizes.put(config, sizes);
    }
    return sizes;
  }

上面的主要逻辑是:

  • 先获取 Bitmap 的内存大小
  • 然后通过 size 和 config 查找最优解对应 besetKey,这个后面细讲
  • 从内存池中,根据 bestKey 获取对应 Bitmap
  • 如果有缓存,通过 decrementBitmapOfSize 更新 Bitmap 的数量和内存的大小
  • 如果有缓存,需要调用 reconfigure 方法重置 Bitmap 的配置

查找 key 的最优解

那么是如何根据内存大小 size 和 config,查找最优 key 呢(这里如果找到了最优的 key,也就找到了最优复用的 Bitmap )?我们看下 findBestKey 方法的实现

java 复制代码
private Key findBestKey(int size, Bitmap.Config config) {
  Key result = keyPool.get(size, config);
  // 根据传入的config,选择合适和使用的config,可能一种config合适复用多种config
  // 不过从上面的定义来看,只有RGBA_F16可以有多个
  for (Bitmap.Config possibleConfig : getInConfigs(config)) {
    // 根据config,获取对应config所有大小的TreeMap
    NavigableMap<Integer, Integer> sizesForPossibleConfig = getSizesForConfig(possibleConfig);
    // 得到大小大于或者等于指定复用的size的最小值
    Integer possibleSize = sizesForPossibleConfig.ceilingKey(size);
    if (possibleSize != null && possibleSize <= size * MAX_SIZE_MULTIPLE) {
      if (possibleSize != size
          || (possibleConfig == null ? config != null : !possibleConfig.equals(config))) {
        // 把上面不合适的key放入到key缓存池中
        keyPool.offer(result);
        // 重新用最优的size和config获取对应的最优key
        result = keyPool.get(possibleSize, possibleConfig);
      }
      break;
    }
  }
  return result;
}
......
}

根据上面的逻辑,匹配的逻辑是:

  • 先根据目标 size 和 Config,直接从 key 的缓存池中获取对应 key,这个 key 就是目标 Bitmap 对应的 key,但是不一定是最优的 key
  • 根据 Bitmap 的 Config,去获取对应的 Config 下所有缓存池中 Bitmap 对应的 size 和数量,也就是上面的 TreeMap
  • 然后根据目标 size,取出大于 size 的最小值,这个值就是最优解
  • 如果取出的 possibleSize 和目标 size 不相等,说明找到了最优解,则说明上面的 result 对应的 key 不是最优解,先把它放到 key 缓存池中,然后用最优的 possibleSizepossibleConfig 重新从 key 缓存池中生成或者获取一个 key
  • 如果取出的 possibleSize 和目标 size 相等,说明上面目标 key(resul)就可能是最优的,则把当前的配置和大小更新 key

总结

从上面的代码逻辑中,可以了解到,Glide 本质上还是利用了 Android 中的 Bimtap 的复用特性进行封装设计的,不同的版本使用不同的缓存策略。但是不同的是 Glide 的设计更加完善,更加合理,个人觉得主要体现在:

  • 使用了合理的数据结构,比如GroupedLinkedMap,不会覆盖相同 key 的图片,能够增加复用命中的概率
  • 大量使用了对象缓存池的思想,防止内粗的抖动。比如 Key,KeyPool
  • Android 4.4 以后的策略,考虑了最优解,找到最合适的 Bitmap 的内存最小值,防止内存复用的浪费。比如一个 10*10 的图片,用了一个 200*200 的 Bitmap

最后,我们可以感受到这些大量使用的三方库,内部是有很多的东西直到我们学习和研究的,不论是思想还是代码质量都写得非常好,这也是正是我现在写深入学习系列的初衷,希望大家持续关注。

相关推荐
2401_8979078614 分钟前
10天学会flutter DAY2 玩转dart 类
android·flutter
m0_7482336441 分钟前
【PHP】部署和发布PHP网站到IIS服务器
android·服务器·php
Yeats_Liao2 小时前
Spring 定时任务:@Scheduled 注解四大参数解析
android·java·spring
xidianjiapei0013 小时前
为何应将微服务从Java迁移到Kotlin:经验与见解【来自DZone】
java·微服务·kotlin
雾里看山4 小时前
【MySQL】 库的操作
android·数据库·笔记·mysql
水瓶丫头站住12 小时前
安卓APP如何适配不同的手机分辨率
android·智能手机
xvch13 小时前
Kotlin 2.1.0 入门教程(五)
android·kotlin
xvch16 小时前
Kotlin 2.1.0 入门教程(七)
android·kotlin
望风的懒蜗牛17 小时前
编译Android平台使用的FFmpeg库
android
浩宇软件开发17 小时前
Android开发,待办事项提醒App的设计与实现(个人中心页)
android·android studio·android开发