BitmapPool 了解吗?Glide 是如何实现 Bitmap 复用的?

Glide实现原理解析 一.Glide缓存机制 Glide采取的多级缓存机制,能够较为友好地实现图片、动图的加载。其主要有 内存缓存+磁盘缓存 ,当然他们的作用也有不同,其中内存缓存主要用于防止将重复的图读入内存中,磁盘缓存则用于防止从网络或者其他地方将重复下载和数据读取。 默认情况下,Glide 会在开始一个新的图片请求之前检查以下多级的缓存: 活动资源 (Active Resources) 内存缓存 (Memor... 阅读详情

highlight: agate

这个问题实际上是我前几天面试时遇到的一个问题,虽然我之前分析过 Glide 的源码,但是老实说,如果不是面试遇到这类问题,我根本不会留意 Glide 的 Bitmap 复用这块…不管怎么说,遇到了这个问题,我们就来看下 Glide 是如何实现 Bitmap 复用的吧~

1、“池化”以及对象复用

其实,说起“池化”以及对象复用,在 Android 中例子还是有这么几个的。典型的比如 Handler 中的 Message. 当我们使用 Message 的 obtain 获取消息的受,实际上是从 Message 池中获取的。Handler 中的 Message 是通过链表维护的数据结构,以此来构成一个 “Message 池”。这个池的最大数量由 MAX_POOL_SIZE 这个参数指定,即为 50.

那么,“池化”以及对象复用有什么好处呢?

这是因为对于 Message 这类频繁使用的对象,如果每次使用的时候直接创建一个对象,那么可能会因频繁创建和销毁导致虚拟机 GC,从而造成页面卡顿现象,尤其是在低端设备上面。“池化”之后每次从池子中获取已经创建的对象进行复用,从而避免了虚拟机频繁 GC.

对于 Bitmap 这类对象和图片相关、占用内存较大的对象,如果频繁创建和销毁,对虚拟机的影响可能比 Message 要大得多,因此 Bitmap 复用显得非常重要。

2、从 Bitmap 的回收说起

先看下 Bitmap 是如何进行回收的吧。

根据官方的建议,在 Android 2.3 及以下的版本中建议使用 recycle() 回收内存,防止 OOM. 但是,使用这个方法的前提是需要确保这个位图不再被使用,否则回收之后再使用将会导致运行时错误。所以,官方的建议是通过引用计数的方式统计位图的引用,只有当位图不再被引用的时候再真正调用该方法进行回收。

官方文档参考:https://developer.android.com/topic/performance/graphics/manage-memory

在 Android 3.0 上面引入了 BitmapFactory.Options.inBitmap 字段。如果设置了此选项,那么采用 Options 对象的解码方法会在加载内容时尝试重复使用现有位图。这样可以复用现有的 Bitmap,减少对象创建,从而减少发生 GC 的概率。不过,inBitmap 的使用方式存在某些限制。特别是在 Android 4.4(API 级别 19)之前,系统仅支持大小相同的位图。在 Android 4.4 之后的版本,只要内存大小不小于需求的 Bitmap 都可以复用。

所以,当我们需要在 Android 中使用 Bitmap 的时候,应该考虑进行 Bitmap 复用以提升应用性能。但是,这些复杂的逻辑要如何封装呢?官方的建议是使用比较成熟的图片加载框架,比如 Glide. 所以,接下来我们来分析下 Glide 是如何实现 Bitmap 复用的。

3、Glide 的 BitmapPool

我们直接从 Glide 的 BitmapPool 开始分析。BitmapPool 是一个接口,定义如下:

public interface BitmapPool {
  long getMaxSize();
  void setSizeMultiplier(float sizeMultiplier);
  // 往 pool 中插入 bitmap 以备复用
  void put(Bitmap bitmap);
  // 从 pool 中获取 bitmap 以复用
  @NonNull Bitmap get(int width, int height, Bitmap.Config config);
  @NonNull Bitmap getDirty(int width, int height, Bitmap.Config config);
  void clearMemory();
  void trimMemory(int level);
}

BitmapPool 通过定义一个 Pool 来让用户复用 Bitmap 对象。在 Glide 中,BitmapPool 有一个默认的实现 LruBitmapPool. 顾名思义,也是基于 LRU 的理念设计的。

前面我们提到过 inBitmap 以 Android 4.4 为分水岭,之前和之后的版本在使用上存在版本差异,那么 BitmapPool 是如何处理这个差异的呢?答案是策略模式。Glide 定义了 LruPoolStrategy 接口,该接口内部定义了增删相关操作。真实的 Bitmap 数据根据尺寸和颜色等映射关系存储到 LruPoolStrategy 中。BitmapPool 的 get 和 put 也是通过 LruPoolStrategy 的 get 和 put 完成的。

interface LruPoolStrategy {
  void put(Bitmap bitmap);
  @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);
}

LruPoolStrategy 默认提供了三个实现,分别是 AttributeStrategy、SizeConfigStrategy 和 SizeStrategy. 其中,AttributeStrategy 适用于 Android 4.4 以下的版本,SizeConfigStrategy 和 SizeStrategy 适用于 Android 4.4 及以上的版本。

AttributeStrategy 通过 Bitmap 的 width(图片宽度)、height(图片高度) 和 config(图片颜色空间,比如 ARGB_8888 等) 三个参数作为 Bitmap 的唯一标识。当获取 Bitmap 的时候只有这三个条件完全匹配才行。而 SizeConfigStrategy 使用 size(图片的像素总数) 和 config 作为唯一标识。当获取的时候会先找出 cofig 匹配的 Bitmap(一般就是 config 相同),然后保证该 Bitmap 的 size 大于我们期望的 size 并且小于期望 size 的 8 倍即可复用(可能是为了节省内存空间)。

所谓的 LRU 就是 BitmapPool 通过 LruPoolStrategy 实现的,具体操作是,在往 BitmapPool 中 put 数据之后会执行下面的操作调整空间大小:

private synchronized void trimToSize(long size) {
    while (currentSize > size) {
        // 移除尾部的
        final Bitmap removed = strategy.removeLast();
        if (removed == null) {
            currentSize = 0;
            return;
        }
        currentSize -= strategy.getSize(removed);
        // ...
        // 回收
        removed.recycle();
    }
}

4、Bitmap 加载和复用

下面我们来复习下一般的 Bitmap 加载的步骤。常规的图片加载过程如下,

// 设置 inJustDecodeBounds 为 true 来获取图片尺寸
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);

// 设置 inJustDecodeBounds 为 false 来真正加载
options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight);
options.inJustDecodeBounds = false;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);

也就是说,首先通过设置 options.inJustDecodeBounds 为 true 来获取图片真实的尺寸,以便设置采样率。因为我们一般不会直接加载图片的所有的像素,而是采样之后再按需加载,以减少图片的内存占用。当真正需要加载的时候,设置 options.inJustDecodeBounds 为 false,再调用 decode 相关的方法即可。

那么 Bitmap 复用是如何使用的呢?很简单,只需要在加载的时候通过 options 的 inBitmap 参数指定一个 Bitmap 对象再 decode 即可:

options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);

5、Glide 是如何加载 Bitmap 的

之前分析 Glide 的源码的时候,注重的是整个流程,对于很多细节没用照顾到,这里我简化下逻辑。首先,Glide 的 Bitmap 加载流程位于 Downsampler 类中。当从其他渠道,比如网络或者磁盘中获取到一个输入流 InputStream 之后就可以进行图片加载了。下面是 Downsampler 的 decodeFromWrappedStreams 方法,这里是执行图片加载的流程,主要代码的逻辑和功能已经备注到了注释上面:

  private Bitmap decodeFromWrappedStreams(InputStream is,
      BitmapFactory.Options options, DownsampleStrategy downsampleStrategy,
      DecodeFormat decodeFormat, ...) throws IOException {
    long startTime = LogTime.getLogTime();
    // 通过设置 inJustDecodeBounds 读取图片的原始尺寸信息
    int[] sourceDimensions = getDimensions(is, options, callbacks, bitmapPool);
    int sourceWidth = sourceDimensions[0];
    int sourceHeight = sourceDimensions[1];
    String sourceMimeType = options.outMimeType;

    // ...

    // 读取图片的 exif 信息,如果需要的话,先对图片进行旋转
    int orientation = ImageHeaderParserUtils.getOrientation(parsers, is, byteArrayPool);
    int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation);
    boolean isExifOrientationRequired = TransformationUtils.isExifOrientationRequired(orientation);
    int targetWidth = requestedWidth == Target.SIZE_ORIGINAL ? sourceWidth : requestedWidth;
    int targetHeight = requestedHeight == Target.SIZE_ORIGINAL ? sourceHeight : requestedHeight;

    ImageType imageType = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool);

    // 根据要求计算需要记载的图片大小和 config,计算结果直接设置给 options 即可
    calculateScaling(imageType, is, ..., options);
    calculateConfig(is, ..., options, targetWidth, targetHeight);

    boolean isKitKatOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
    if ((options.inSampleSize == 1 || isKitKatOrGreater) && shouldUsePool(imageType)) {
      // ...
      // 根据图片的期望尺寸到 BitmapPool 中获取一个 Bitmap 以复用
      if (expectedWidth > 0 && expectedHeight > 0) {
        setInBitmap(options, bitmapPool, expectedWidth, expectedHeight);
      }
    }
    // 开始执行 decode 逻辑
    Bitmap downsampled = decodeStream(is, options, callbacks, bitmapPool);
    callbacks.onDecodeComplete(bitmapPool, downsampled);

    // ... 图片旋转等后续逻辑

    return rotated;
  }

上述代码中的 setInBitmap 方法中即调用了 BitmapPool 的 get 方法用来获取复用的 Bitmap 对象,其代码如下:

  private static void setInBitmap(
      BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height) {
    @Nullable Bitmap.Config expectedConfig = null;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      if (options.inPreferredConfig == Config.HARDWARE) {
        return;
      }
      expectedConfig = options.outConfig;
    }
    if (expectedConfig == null) {
      expectedConfig = options.inPreferredConfig;
    }
    // 调用了 inBitmap
    options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
  }

另外,通过查看 Bitmap 的 inBitmap 文档注释,我们可以看到可能存在一些情况导致 inBitmap 过程中出现异常,那么 Glide 会不会因为复用 Bitmap 而导致加载过程异常?Glide 又是如何进行处理的呢?参考上述代码,我们可以看到加载图片调用到了名为 decodeStream 方法。该方法经过我的简化之后大致如下:

  private static Bitmap decodeStream(InputStream is, BitmapFactory.Options options,
      DecodeCallbacks callbacks, BitmapPool bitmapPool) throws IOException {
    // ...
    final Bitmap result;
    TransformationUtils.getBitmapDrawableLock().lock();
    try {
      // 数据加载
      result = BitmapFactory.decodeStream(is, null, options);
    } catch (IllegalArgumentException e) {
      // ...
      if (options.inBitmap != null) {
        try {
          // 输入流重置
          is.reset();
          bitmapPool.put(options.inBitmap);
          // 清理掉 inBitmap 并进行第二次加载
          options.inBitmap = null;
          // 再次调用进行加载
          return decodeStream(is, options, callbacks, bitmapPool);
        } catch (IOException resetException) {
          throw bitmapAssertionException;
        }
      }
      throw bitmapAssertionException;
    } finally {
      TransformationUtils.getBitmapDrawableLock().unlock();
    }
    if (options.inJustDecodeBounds) {
      is.reset();
    }
    return result;
  }

也就是说,Glide 首先会通过设置 inBitmap 复用的方式加载图片。如果这个过程中出现了异常,因为此时 inBitmap 不为空,所以将会进入异常处理流程,此时会清理掉 inBitmap,再次调用 decodeStream 方法二次加载,这个时候就不是 Bitmap 复用的了。所以,Glide 内部会通过错误重试机制进行 Bitmap 复用,当复用并出现错误的时候,会降级为非复用的方式第二次进行加载。

总结

以上就是 Glide 中 Bitmap 复用的原理,希望本文对你有所帮助!

GlideBitmapPool, Glide位图池是用于重用位图内存的内存管理库.zip GlideBitmapPool, Glide位图池是用于重用位图内存的内存管理库 Glide位图池 Bitmap位图池Glide位图池是用于重用位图内存的内存管理库。 因为它重用位图内存,所以不再反复调用 GC,因此运行流畅的应用程序。 它使用inBitmap在支持的安卓版本上解码位图。 立即下载

相关推荐

大模型-高通平台性能测试示例-2

在QNN中,可以使用(genis-t2t-run)和这两个工具来进行推理的性能分析。支持在QNX操作系统或Android操作系统上进行性能分析。:提供基本的性能数据,如运行时间、资源消耗等。:提供更详细的性能数据,可能包括各个操作的执行时间、资源使用情况等。:是指在性能分析的同时进行代码质量检查,以发现潜在的问题。:是指跟踪和分析神经网络中各个操作的性能,以优化网络结构或参数。

点滴的岁月 1491

Android Glide 3.7.0 源码解析(四), BitmapPool作用及原理

Android 中图片显示的实体其实是一个 Bitmap 对象, 每次图片显示时, 都会构建一个 Bitmap 对象, 不用时再销毁 假设: 一个长列表每项都有个图片需要显示, 我们在快速滑动长列表的时候, 会产生什么? Bitmap 对象被频繁的创建和释放, 导致 GC 频繁 如何解决上述问题? BitmapPool , 一个 Bitmap 的对象池, 让一个新的图片资源复用在旧的 Bitmap对象上, 假设, 长列表一页有 20 个图片资源, BitmapPool 大小也刚好是 20 , 那么当滑动列表

πππ 817

保姆级教程:在RK3588开发板上用MPP库实现H.264硬编码(附完整代码)

本文详细介绍了在RK3588开发板上使用MPP库实现H.264硬编码的全流程,包括环境准备、MPP库编译、编码器初始化与配置、数据输入与编码流程以及高级性能优化技巧。通过实战案例和完整代码,帮助开发者充分利用RK3588的VPU硬件加速能力,提升视频编码效率和质量。

weixin_28716769的博客 414

BitmapPool 了解吗?Glide 是如何实现 Bitmap 复用的?(1)

其实,说起“池化”以及对象复用,在 Android 中例子还是有这么几个的。典型的比如 Handler 中的 Message. 当我们使用 Message 的 obtain 获取消息的受,实际上是从 Message 池中获取的。Handler 中的 Message 是通过链表维护的数据结构,以此来构成一个 “Message 池”。这个池的最大数量由 这个参数指定,即为 50.那么,“池化”以及对象复用有什么好处呢?这是因为对于 Message 这类频繁使用的对象,如果每次使用的时候直接创建一个对象,那么可

2401_84149256的博客 821

使用STC15F104W产生PWM脉冲

最近的小项目中使用到了STC15F104W产生PWM脉冲

yeats1989的专栏 1万+

一生一芯 预学习阶段 NEMU代码学习(2)

如果 nemu_state.state == NEMU_ABORT:说明程序处于 "中止" 状态,输出 "ABORT",并且使用 ANSI_FG_RED 红色格式化显示。如果 halt_ret == 0,表示程序正常终止(触发了 "GOOD TRAP"),输出 "HIT GOOD TRAP",并使用 ANSI_FG_GREEN 绿色显示。如果要执行的指令数量 n 小于预定义的常量 MAX_INST_TO_PRINT,则将 g_print_step 置为 true,表示会逐步打印指令执行的过程。

weixin_45041054的博客 826

BitmapPool 了解吗?Glide 是如何实现 Bitmap 复用的?,移动开发者升职加薪的8项技能

我见过很多技术leader在面试的时候,遇到处于迷茫期的大龄程序员,比面试官年龄都大。这些人有一些共同特征:可能工作了5、6年,还是每天重复给业务部门写代码,工作内容的重复性比较高,没有什么技术含量的工作。问到这些人的职业规划时,他们也没有太多想法。其实30岁到40岁是一个人职业发展的黄金阶段,一定要在业务范围内的扩张,技术广度和深度提升上有自己的计划,才有助于在职业发展上有持续的发展路径,而不至于停滞不前。不断奔跑,你就知道学习的意义所在!

2401_84150165的博客 394

Emotion-LLaMA实战指南:从零构建多模态情感识别系统(附Python代码)

本文提供了Emotion-LLaMA多模态情感识别系统的实战指南。通过详细的步骤,指导读者从零搭建开发环境、运行本地Demo、理解模型架构,并完成数据处理与模型训练。该创新模型融合视觉、听觉与文本信息,实现更精准、接近人类理解的情感分析与推理,并附有完整的Python代码实现。

weixin_29094775的博客 415

Glide(Android)之Bitmap对象池

对象池,我们知道是存了对象的,将来可以复用,这样避免频繁地创建和回收常用对象,造成内存抖动,降低app的性能。 对于普通的对象来说,其本身占用内存很小,同时在放到对象池的时候,会对对象重置的,所以对象池不会关心对象个体的内存大小,只关心对象的数量。基本上有两种实现方案,数组和单链表,Android官方都有实现使用,Pools采用了数组,大小可以自己指定,Message采用了单链表,可以存50个m...

“点点”专栏 1379

Nova — GPU/vGPU 虚拟机

目录 文章目录目录云主机使用显卡的实现方式虚拟显卡物理显卡直通物理显卡虚拟化 云主机使用显卡的实现方式 采用虚拟显卡 采用物理显卡直通 采用物理显卡虚拟化 虚拟显卡 虚拟显卡技术,主要用于虚拟机 Remote Console 领域,如:VNC(Virtual Network Computing,虚拟网络计算机),它能将完整的窗口界面通过网络,传输到另一台计算机的屏幕上。 VNC 主要由 VNC server 和 VNC viewer 两部分组成。用户需先将 VNC server 安装在被远程操控的计算

烟云的计算 2422

基于深度学习的生活垃圾检测与分类系统:YOLOv5、YOLOv6、YOLOv7与YOLOv8实现

本项目的目标是开发一个基于YOLO系列模型的生活垃圾检测与分类系统。数据集准备与标注:收集并标注生活垃圾图像数据集。模型训练:使用YOLOv5、YOLOv6、YOLOv7、YOLOv8等深度学习模型训练垃圾分类系统。UI界面设计:设计一个简洁的用户界面,支持图像或视频的上传与垃圾分类结果展示。实时垃圾检测与分类:通过摄像头进行实时垃圾检测和分类。结果展示与保存:展示检测结果,支持用户保存分类后的图像或视频。

YOLO 705

Glide4.8源码拆解(五)BitmapPool从入门到放弃

前言 Bitmap加载不当导致内存占用过大或者内存抖动,可能是每个Android工程师无法逃避的问题,如何合理的管理Bitmap内存,几乎成了各位必备的功课;好在Android官方早已经为我们提供了解决思路; 本章主要内容: Bitmap内存管理和复用的简介 LruBitmapPool的基本分析 AttributeStrategy关键逻辑分析 SizeConfigStrategy关键逻辑分析 G...

weixin_33824363的博客 2295

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

Glide手写实现之复用池

为什么要用复用池 Bitmap属于大对象,在频繁创建对象和回收对象时,会有内存抖动问题。为了解决这个问题,Glide中使用了复用池。在开辟新的Bitmap对象空间之前,从复用池中找寻是否有合适的内存空间可以直接使用。复用池仍然使用了LRU算法。 BitmapPool 复用池的标准接口 // 复用池 标准 public interface BitmapPool { /** * 存...

吓成一坨兔子 770

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

上一篇: 建议收藏,从 jCenter 迁移到 MavenCentral 完整方案
下一篇: 异步、非阻塞式 Android 启动任务调度库
开发者如是说
博客等级 码龄10年 67粉丝 163原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值