一、前言
笔者最近致力于 vivo 游戏核心稳定性保护,在分析线上异样时,发现有相当一部分是由 OutOfMemory 引起。谈及 OOM,咱们个别都会想到内存透露,其实,往往还有另外一个因素——图片,如果对图片使用不当的话,很容易吃掉大量内存,从而导致异样。
尤其是游戏核心在 2020 末~2021 初的几个重要版本,上线了很多内容相干的 feature,引入大量图片、视频列表,从而导致线上 OOM 占比回升。
在这篇文章中,笔者将解说一张看似一般的 Bitmap 对内存的占用,介绍 Android Studio 中帮忙咱们剖析图片占用内存的工具,举例说明风行的两大图片加载框架:Glide、Picasso 在加载图片时应用内存的不同形式,接着剖析不同 drawable 目录下图片的显示策略,最初基于手机内存、版本,提出一种优化内存调配的计划。
二、查看图片内存占用
一张图片在内存占用的空间到底有多少,普遍存在的一个误会是,图片自身在磁盘上 / 从网络下载下来是多大,就会占用多少的内存。这种说法是不正确的,图片占用内存的大小不取决于它自身的大小,而取决于图片库所采纳的展现形式所申请的内存。
拿钢铁侠这张图片举例,它的尺寸是 350*350,能够看到在电脑磁盘上,它只占 36KB 的空间。
咱们创立一个简略的 Demo,页面正地方是一个 ImageView,用于显示这张钢铁侠图片。
通过 Android Studio 进行 heap dump,从而看图片所占用的内存。首先咱们将显示图片时的内存快照保留下来。操作门路为 Profiler -> Memory -> Heap Dump,这会生成一个 dump 文件,在其中能够看到以后堆的应用状况。
在上面这张图能够看到,程序运行时,“钢铁侠”这张图片占用的内存(Retained Size)是 2560000bytes,约等于 2.4MB 内存。与它在磁盘上 36KB 的大小,相差了整整 70 倍!
小技巧:如何查看 dump 文件中的图片
在调试时,如果咱们手头只有一个 dump 文件,往往须要还原图片内容,以帮忙定位问题。有两种形式能够从 dump 文件里提取原图片。
形式一:通过 Android Studio 间接查看
如果 dump 文件起源自 Android 版本为 7.1.1(Android N,API=25)及以下的设施,能够应用这种办法。选中 Bitmap 对象,间接在窗口的 Bitmap Preview 中查看图片内容(如上图),十分不便。
形式二:通过 MAT+GIMP 查看
这种办法实用于全副 Android 版本的设施,首先用 MAT 关上 dump 文件,有时会产生下图的谬误:
起因是 Android Studio 的 Profiler 生成的 dump 文件不是规范格局,咱们能够应用位于门路 SDK/platform-tools/hprof-conv.exe 的工具将其转换为规范格局,转换命令为:
hprof-conv.exe <in-file> <out-file>
将转换后的 dump 文件通过 MAT 关上,在其中找到 Bitmap 对象的 byte[]属性,将其复制为 image01.data 文件。
Tip: 能够看到这里 image01.data 文件的尺寸是 2.44MB,也正是在运行时图片所占用的内存。
而后用 GIMP 工具关上该文件,在格局那里抉择 RGBA(大部分 Bitmap 都应用这种格局),宽与高能够在 MAT 中看到,笔者这里是 800 * 800。设置好格局和宽高后,就能够看到图片的实在面目了。
二、图片内存占用计算公式
在上一章节咱们晓得一个通过网络下载的 36KB 图片,在被加载到内存中时,须要 2.4MB 的空间。接下来解释这其中的换算关系,让咱们记住一个公式:
图片占用内存 = 图片品质 * 宽 * 高
这外面有“图片品质”、“宽”、“高”三个因素,它波及到图片加载框架的实现,不同的框架,对于这三者的默认取值是不一样的,咱们以以后最风行的 Picasso 和 Glide 为例。
Picasso
在 Picasso 中,图片默认显示的宽高与原始图片宽高统一。依然以这张钢铁侠为例,图片自身是 350px 350px,当咱们把它加载到 200px 200px 的 ImageView 当中时,占用空间是0.49MB。
因而,在指标 ImageView 小于图片尺寸的状况下,好的做法是应用不超过 ImageView 尺寸的图片源,一方面能够缩短图片下载工夫,另一方面有助于优化内存占用。
Glide
Glide 则采纳截然不同的解决形式,它最终应用的宽高是指标 ImageView 的宽高。如果咱们把同样一张图片加载到 200px * 200px 的 ImageView 中,占用空间只有 0.16MB。
使 Picasso 达到与 Glide 同样的成果
Picasso 的设计者也发现了这一毛病,提供一系列办法用来调整最终加载进去的图片尺寸,其一就是 fit(),通过这个办法能够达到与 Glide 同样的成果。
Picasso().get().load(IMAGE_URL).fit().into(imageVIEW)
相同场景:小图加载到大 ImageView 中
通常为了提供更清晰的界面,避免图片拉伸后失真含糊,设计师提供的图片都是高分辨率的,咱们所面临的场景是将大图加载到小 ImageView 中。但也不排除相同的可能:将小图加载到大 ImageView 外面。这时 Glide 默认采纳的内存策略是存在有余的:它采纳指标 ImageView 的尺寸作为最终的宽和高。
举例说明,当把 350 350 的钢铁侠图片加载到 600 600 的 ImageView 中时,占用的内存高达 1.41MB。
600 600 4bytes = 1.41MB
有没有一种办法,能够兼顾原图片与指标 ImageView 不同的大小关系呢?——有的,这就是 centerInside()。
Glide.with(this).load(IMAGE_URL).centerInside().into(imageView)
借助 centerInside()办法,能够达到“在原图片和指标 ImageView 中取最小宽高作为最终加载图片的尺寸”这样的成果。
三、图片品质
什么是“图片品质”?简略说就是用多少字节来示意一个像素点的色彩,它的学名叫做“位深度”,在图片属性当中能够看到。
图片位深度通常有 1 位、8 位、16 位、24 位、32 位。
PNG 格局有 8 位、24 位、32 位三种模式,其中 8 位 PNG 反对两种不同 的通明模式(索引通明和 alpha 通明),24 位 PNG 不反对通明,32 位 PNG 在 24 位根底上减少了 8 位通明通道,因而可展示 256 级通明水平。
Glide 和 Picasso 默认采纳的图片品质都是 ARGB_8888、也就是带透明度的 32 位深度,一个像素点须要占用 4bytes 的内存,这也解释了为什么上文中的计算都是采纳宽_高_4bytes 的公式。
注:v4 开始,Glide 将 ARGB\_8888 作为默认配置。在那之前它始终默认应用 RGB\_565。
对客户端应用的大部分图片来说,32 位深度、16 位深度的显示品质是肉眼较难分辨的,但它们在占用内存上相差了整整一倍。因而,笔者倡议在大部分场景下,应用 RGB_565 作为加载图片的模式。以下两种场景除外:
1)含通明局部的图片:如果采纳 RGB_565 图片格式来显示图片,是无奈失常展示通明区域的。比方上方这个钢铁侠图片,本来通明的局部会被显示为彩色。
2)含渐变色并且对显示品质要求高的图片:32 位比 16 位能够反对更多的色彩,在突变的显示上出现更加天然的过渡(如下图)。这时咱们该当在显示品质和利用性能之间作取舍。对于低端设施,利用的稳定性比显示品质更加重要,笔者强烈建议采纳 16 位深度来显示。
四、drawable 目录下图片加载形式
我的项目的资源目录下,个别都有 drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi、drawable-xxxhdpi 目录,它们是用来匹配不同显示密度的设施的,对应表格如下。
通过 adb shell wm density 能够获取以后设施的 dpi,对 Nexus 6P 模拟器执行后,能够读取到它的 dpi 是 560,属于 xxxhdpi。
$ adb shell wm densityPhysical density: 560
那么同一个图片放在不同目录下,对分配内存是否有影响呢?答案是有的,基于两步简略的推导:
- 图片所在资源目录、设施密度两者决定图片最终显示在屏幕上的像素尺寸;
- 像素尺寸、图片品质独特决定分配内存。
其中第 2 点曾经在上文解说过,这里次要剖析第 1 点。应用图片编辑软件,将本来是 350 350 的钢铁侠图片放大至 700 700,并别离放入 xhdpi、xxxhdpi 两个目录下。
为什么应用这样的组合呢?因为从上表得悉,xhdpi 与 xxxhdpi 的显示密度是 1:2,意味着一台 xxxhdpi 的设施在显示 drawable-xhdpi 目录下的图片时,会将其放大为 2 倍进行展现。因而咱们将 350 350 的骨片放入 drawable-xhdpi,将 700 700 的图片放入 drawable-xxxhdpi,预期它们最终在屏幕上显示的尺寸雷同。
在布局里创立两个 ImageView,察看这两张图片最终的显示成果,以及分配内存状况。
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000">
<!-- 350 * 350,位于 drawable-xhdpi -->
<ImageView
android:id="@+id/iv_image_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="40dp"
android:src="@drawable/iron_man_350_square_xhdpi"
/>
<!-- 700 * 700,位于 drawable-xxxhdpi -->
<ImageView
android:id="@+id/iv_image_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="40dp"
android:layout_gravity="bottom"
android:src="@drawable/iron_man_700_square_xxxhdpi"
/>
</FrameLayout>
显示成果以及内存调配如下:
能够剖析得出以下论断:
对于显示尺寸 613 613 的图片,其占据内存为 613 613 * 4 = 1,503,076B ≈ 1.5MB,合乎上文中咱们对图片内存的剖析;
决定图片占用内存的是其最终显示在屏幕上的尺寸,与图片自身分辨率、在哪个 drawable 目录下没有间接关系;
因为 xxxhdpi 密度是 xhdpi 密度的两倍,故在屏幕密度属于 xxxhdpi 的 Nexus 6P 设施上,drawable-xxxhdpi 目录下的图片被以近似于原像素尺寸(700px)进行显示(显示为 613px),而位于 drawable-xhdpi 目录下的图片被放大至 2 倍显示,最终显示尺寸同样是 613px。
五、优化策略
在理论的开发中,咱们心愿中高端机型加载更清晰的图片(ARGB\_8888),以晋升用户体验,对于低端机型则心愿加载占用内存更小的图片(RGB\_565),以升高 OOM 产生的概率。能够在初始化 Glide 时进行这样的配置。须要注意的是不要对含通明区域的图片采纳这种优化计划。
@GlideModule
class MyGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {builder.setDefaultRequestOptions(RequestOptions().format(getBitmapQuality()))
}
private fun getBitmapQuality(): DecodeFormat {return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || hasLowRam()) {
// 低端机型采纳 RGB_565 以节约内存
DecodeFormat.PREFER_RGB_565
} else {DecodeFormat.PREFER_ARGB_8888}
}
}
六、小结
借助一些开源工具,咱们能够便捷地定位大图,如滴滴开源的 DoKit,篇幅起因不进行具体介绍。最初,对于咱们日常开发总结几点倡议,心愿大家的利用稳定性节节攀升。
- 在多图的场景(比方 RecyclerView)留神及时开释图片资源;
- 应用占据内存更小的图片格式;
- 图片源文件尺寸该当与指标 ImageView 相近;
- 优先满足 xxhdpi、xxxhdpi 的图片资源需要;
- 依据设施性能,采纳不同的图片加载策略。
作者:vivo 互联网客户端团队 -Li Lei