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

本文探讨了Android中对象复用,特别是通过Handler的Message池和Bitmap对象的复用来提高性能,重点介绍了Glide的BitmapPool机制,以及如何在不同Android版本间适配Bitmap回收策略。
摘要由CSDN通过智能技术生成

其实,说起“池化”以及对象复用,在 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. 但是,使用这个方法的前提是需要确保这个位图不再被使用,否则回收之后再使用将会导致运行时错误。所以,官方的建议是通过引用计数的方式统计位图的引用,只有当位图不再被引用的时候再真正调用该方法进行回收。

官方文档参考:developer.android.com/topic/perfo…

在 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 默认提供了三个实现,分别是 AttributeStrategySizeConfigStrategySizeStrategy. 其中,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;
}

如何做好面试突击,规划学习方向?

面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。

学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。

我搜集整理过这几年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节

img

在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 20
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值