从内存优化视角再看 Glide 图片加载库

Glide 使用和部分原理参考:bumptech.github.io/glide/doc/d...

前置背景

Glide 作为常用的图片加载框架,框架层面已经对内存方面有不少优化,但作为一个图片框架,确保正确性一定是第一位的,因此在应用层还可以在适当的场景做一些额外的优化,当然你需要了解优化设置可能产生的问题。另外框架设计复杂性是很高的,但暴露给上层的 API 又非常简单,这导致我们忽略一些基本的工作原理,从而有意无意错误的使用框架。

这不是一篇 Glide 源码分析类的文章,如果对 Glide 使用还不够熟悉,不建议直接阅读。 以下文档结论基于 Glide v4+ 版本。

Glide 的一些特性

  1. 在不经其他额外配置的情况下 Glide 加载到内存中的 bitmap 大小不会超过视图大小本身,因此不会存在内存浪费。例如,网络图片原图大小为 200x200,但显示在页面中的 ImageView 控件大小为 100x100,Glide 内部最终解码的图片大小为 100x100。
  2. 若网络图片大小 > 控件宽或高,则默认情况下会按比例放大,返回一个满足宽高比例的位图。
  3. Glide v4+ 开始图片加载回归 ARGB_8888 格式作为默认 DecodeFormat。
  4. 如果加载图片时使用 RGB_565 格式,则实际运行不一定能够生效,主要是受两方面影响:1是如果图片本身包含透明度则仍会使用 ARGB_8888 格式,2是包含一些圆角或关于Alpha 的 Transform 变换,也会保持 ARGB_8888 格式。
  5. Glide 占用系统哪部分的内存?从 Android 8.0 开始,Bitmap 内存分配在 Native 堆,在未开启硬件位图前,还会在硬件层复制一份位图的拷贝,因此是双倍内存。硬件位图特性从 Android O 版本开始,在不了解硬件位图弊端前,应该谨慎使用,详情参考:了解硬件位图。由此,应用不会因为图片内存占用过高导致 OOM。

Glide 使用的一些细节点

手动设置 override(width, height) 时,要确保不要使用大于控件实际尺寸的 size。

手动调用 override 的方式,对于解码图片的大小优先级最高,会默认跳过采样策略计算,如果你明确知道显示区域需要多大的图片可以使用此方法,避免图片放大(第5条)。如果想使用原图大小,可以使用 override(Target.SIZE_ORIGINAL)。

使用 preload 预加载图片默认会 decode 原图大小

preload 相关API 是不需要指定 Target 的,Glide 内部会默认加载原图,如果想明确 UI 上需要的尺寸,需调用重载的方法 preload(int width, int height)定制图片尺寸。

ImageView 在布局文件中,避免设置不明确的 match_parent 作为宽高

Glide 解码图片时是不会考虑 ImageView 的 scale_type 属性的,来看这个例子:

ini 复制代码
<ImageView
    android:id="@+id/iv_cover_bg"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:scaleType="centerCrop" />

由于图片设置了 centerCrop 属性,在直观看图片会按等比居中缩放,但由于 decode 发生在 scaleType 生效之前,因此使用 Glide 加载图片时会以 ImageView 的实际尺寸(即全屏) 的宽高来解码图片,由此产生了不必要的内存开销。

解决方案比较多,常规的可以提前通过原始图和容器宽高比,计算出实际需要显示的图片尺寸,并使用 override(width, height) 方法强制指定解码图片的大小。亦或前提知道图片的比例,可使用 layout_constraintDimensionRatio 约束 ImageView 控件的大小。

如果封面图有圆角需求,使用 CardView 的圆角属性会比 Glide rounderCorner 内存开销低。

原因是 Glide rounderCorner 发生在图片decode 之后,经一轮变换会产生一个新的位图(官方描述为所有基于BitmapTransformation 变换产生的位图会被存储在 BitmapPool 中,并最终按 LRU 算法释放),而 CardView 是通过canvas.drawRoundRect(mBoundsF, mRadius, mRadius, paint),drawRoundRect 会转化成一系列绘图指令到 GPU,不会在堆内存产生额外的内存开销。

以下是实验数据,在三种情况下做内存数据对比,前置条件是加载一张 750x750 大小的不带透明度的 jpeg 格式的网络图片。按常规来说,如果使用 ARGB_8888 格式解码,产生的内存开销为 750x750x4 = 2197 kb,如果使用 RGB_565 则能减少一半的内存开销。

场景 Native Heap(KB) Graphics(KB)
RGB_565 & 默认直角 12730 5410
RGB_565/ARGB_8888 & 圆角 Transform 17326 6564
RGB_565 & CardView 圆角 13633 5417

可以看到是 NativeHeap 有明显的降低,推测与RounderCorner 额外变换导致的位图临时开销有关,Graphics 降低 1M 左右与预期相符。

关注图片放大场景

在背景知识中提到如果原图比控件容器大,则会缩小图片,并不会导致内存浪费。但如果原图比容器小会发生什么呢?例如:原图为 100x100,但容器为 200x200,那么默认情况下 decode 出一个 200x200 的位图。事实上,Glide 整体的图片缩放机制是通过内部的 DownsampleStrategy 完成的,其内部定义了与 ScaleType 类似的采样策略,默认的采样策略为 CENTER_OUTSIDE: 当原图尺寸 < 控件尺寸时会成比例放大至控件大小。另外还有其他采样策略,使用 AT_LEAST、AT_MOST、CENTER_INSIDE 模式,当明确知道原图尺寸 < 控件尺寸时均不会放大原图;反之,当原图尺寸 > 控件尺寸是可能会出现解码出大于控件尺寸图的情况。

使用示例:

scss 复制代码
Glide.with(this)
    .asBitmap()
    .load(url)  
    .apply(RequestOptions.downsampleOf(DownsampleStrategy.AT_LEAST))
    .into(imageView)

RGB_565 模式不生效与 Alpha Transform 变换冲突问题。

有效降低内存,对于多图文或 Feed 列表的 App,开启该配置可降低 10-20M 内存。示例全局开启 RGB_565 解码配置。

kotlin 复制代码
@GlideModule
class GlideConfig : AppGlideModule() {

    override fun applyOptions(context: Context, builder: GlideBuilder) {
        super.applyOptions(context, builder)
        builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
    }
    ...
}

但与此同时你需要了解在一些情况下 RGB_565 可能不生效,甚至产生奇怪的显示效果。

  1. 若原图包含 alpha 透明度,则配置失效。
  2. RGB_565 配置与 RoundedCorners/CircleCrop 变换同时使用时,配置失效。
  3. RGB_565 与一些包含硬件绘制指令的变换同时使用时,配置失效或显示异常。

1/2 比较好理解,对于3,常见的就是高斯模糊效果(毛玻璃),该效果目前是通过 RenderScript 相关API,此 API 要求应用视图必须开启硬件加速。在此情况下,同时开启 RGB_565 配置会导致出现异常,可能会返回一个空的位图,在部分设备上可能会返回一个异常的模糊效果。

关于 RGB-565 失效原因的官方 issue 讨论 github.com/bumptech/gl...

//官方5年前对 inPreferredConfig 的修改。 github.com/bumptech/gl...

图片降质与内存优化没有必然关联

根据以上 Glide 对图片的缩放处理可知,默认情况下一张图片占用的最大内存只与控件宽/高和解码格式有关。

在整个图片处理的过程中,图片降质只是减少图片的下载速度,对内存占用并无直接影响。

相关推荐
Estar.Lee29 分钟前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
温辉_xh1 小时前
uiautomator案例
android
工业甲酰苯胺2 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
少说多做3432 小时前
Android 不同情况下使用 runOnUiThread
android·java
Estar.Lee4 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯4 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey5 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!7 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟8 小时前
Android音频采集
android·音视频
小白也想学C9 小时前
Android 功耗分析(底层篇)
android·功耗