带你了解Android图片在内存中的大小以及工具的使用

870 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

前言

对于大部分Android应用,内存中占比最大的部分就是图片了,我们的应用也不例外。当一个内存优化的任务摆在我面前时,我首先想到的就是优化图片的内存,而优化图片的内存就先要了解它到底能占用多少内存,这也是本文的内容。

图片在内存中占用大小的计算方法

先抛出两个问题

问1:一张在内存中大小为1920*1080的图片,以ARGB_8888格式加载,它占用的内存是多少?

答:占用内存=1920 * 1080 * 4 Bytes = 8,294,400 Bytes ≈ 8 MB

问2:一张在hdpi路径下的1920*1080的图片,以ARGB_8888格式,加载在480dpi的手机屏幕上,它占用的内存又是多少?

答:占用内存=3840 * 2160 * 4 Bytes = 33,177,600 Bytes ≈ 32 MB

再给出计算公式

图片在内存中占用的空间,是由它所具有的像素点数 和 每个像素点占用空间的大小决定的

占用内存 = 内存中图片的像素点数 * 每个像素占用的大小

等价于

占用内存 = 内存中图片的宽 * 内存中图片的高 * 每个像素占用的大小

等价于

占用内存 = 图片资源的宽 * 缩放比例 * 内存中图片的高 * 缩放比例 * 每个像素占用的大小

解答问题1

问1:一张在内存中大小为1920*1080的图片,以ARGB_8888格式加载,它占用的内存是多少?

来看问题1,内存中的宽高我们都已知晓,只有“每个像素占用的大小”还不知道。其实它和加载的格式ARGB_8888有关。

ARGB_8888格式是图片的位图配置(bitmap configurations)的一种,其他常见的配置还有RGB_565、ARGB_4444、RGBA_F16等,它表示了每个像素存储在多大的空间上。

我们都知道光的三原色是红(R)绿(G)蓝(B),图片的色彩也是由这三个颜色的数值组合而成。 以前常用的RGB_565,就是每个像素存储三个颜色通道,分别是5/6/5,一共存储16bits,即2bytes,也就是说每个像素会占用2bytes的空间; 而现在常用的ARGB_8888,则多加了透明(A,alpha),每个通道都以8bits的精度存储,共32bites,即4bytes,于是每个像素会占用4bytes的空间。

至此,问题1的答案就迎刃而解了,

答:占用内存=1920 * 1080 * 4 Bytes = 8,294,400 Bytes ≈ 8 MB


解答问题2

问2:一张在hdpi路径下的1920*1080的图片,以ARGB_8888格式,加载在480dpi的手机屏幕上,它占用的内存又是多少?

来看问题2,这次不知道内存中图片的大小,只知道原始图片大小,以及所在的文件夹和屏幕像素密度。那么就需要知道原始图片加载到内存中是如何缩放的

1.首先明确一下屏幕密度的概念(dpi/ppi),它是指单位英寸上的像素数

以我的Redmi为例,屏幕是6.39英寸,分辨率是2340*1080。如果我知道屏幕宽或者高的英寸数,我可以直接用宽或高除以对应英寸获得dpi,但实际上手机只会告诉你屏幕对角线的英寸数6.39,那么就要 通过勾股定理,来计算出对角线的像素数,然后才能相除

勾股定理 dpi = √(屏幕宽^2 + 屏幕高^2) / 英寸

那么我的手机的dpi就是 √(2340^2 + 1080^2) / 6.39 ≈ 403.3。

2.屏幕密度说完了计算方法,接下来再说说不同资源文件路径对应的屏幕密度

Android通用的屏幕密度的对应关系如下:

文件夹屏幕密度
(drawable)160dpi
ldpi120dpi
mdpi160dpi
hdpi240dpi
xhdpi320dpi
xxhdpi480dpi
xxxhdpi640dpi

再看回问题2,图片在hdpi路径下即对应240dpi,加载在480dpi的手机屏幕上,则图片的宽和高要放大到原来的两倍,若是加载在640dpi的屏幕上,宽高各要放大8/3倍,若是加载在120dpi的屏幕上,宽高反而还要缩小两倍。

那么,问题2的也解决了,

答:占用内存=1920 * 2 * 1080 * 2 * 4 Bytes = 33,177,600 Bytes ≈ 32 MB

从计算公式推导的一些结论

原始图片的尺寸不一定等价于内存中图片的尺寸,还要看图片的缩放

放在某个固定路径下的某张图片,相较于路径对应的dpi,加载在越大dpi的屏幕上,图片越大,占用内存越大;反之,则图片越小,占用内存变少,所以要选择合适的文件夹路径

图片放大,则图片变得模糊;图片缩小,似乎看起来更清晰,但在低dpi的屏幕上很多细节也看不出来,精细的部分其实也浪费掉了,这也可以看出每个文件夹放不同清晰度图片的意义

……

查看内存中加载的图片情况

知道图片在内存中占用多大的计算方法,往往还不够,还要分析各个时候内存中加载了多少图片,哪些图片占用空间最大。

Android 8.0开始,图片内存加载在native层,8.0之前加载在java层。可以通过Android studio自带的 Profiler查看图片内存占用情况。

如下图所示:

profiler.png

此处是在Android11上运行,可以看到是Native层占用内存最多。点击Capture heap dump按钮,开始记录当前内存的占用情况。

记录后:

Dump.png

从这个dump记录可以看到,图片总共占用了多少内存(看Retained Size),也可以看到是每个图片分别占用的内存。有时候,光看这些数据还不够,还需要知道占用空间最大的图片是哪张,让犯人露出它的真面目!这时候就需要用到hprof-conv.exe,MAT,GIMP三个工具了,不过此时就需要在Android8.0以下的设备上dump记录,MAT才能解析

hprof-conv.exe

首先,Android studio记录的hprof文件,需要使用hprof-conv.exe转换后才能使用。

hprof-conv.exe的路径在SDK的platform-tools下,执行的命令为:

hprof-conv a.hprof a_conved.hprof

MAT

MAT是Eclipse提供的内存分析工具,操作也很简单。只需要打开MemoryAnalyzer.exe,选择File-> Open Heap Dump -> 选择转化后的hprof文件即可。

打开后效果如图所示:

image.png

然后如图点击Dominator Tree

image.png

我们知道图片是以byte数组的形式存在内存中的,所以最后在Dominator Tree选项卡中,从Bitmap或者byte[]开始都可以。最终我们需要在Bitmap的属性中获得图片的宽高,并保存byte[],以供GIMP使用。

  • Bitmap 记住左侧的宽高,等下在GIMP里需要用到。

image.png

展开Bitmap,可以看到byte数组,按图所示保存为后缀为.data的文件

image.png

  • byte[] 如图所示选择 incoming references

image.png

即可获得下图:

image.png

之后的操作就和上面Bitmap的一样了

GIMP

使用GIMP打开上面保存的.data文件,有时会需要输入宽高,同时也要注意图像类型是不是正确的

image.png

当一切都选择好后,这张图片就可以正确解析出来了。

image.png

利用工具检查内存泄漏

上述的工具,在某些情况下还有查找内存泄漏的功用。

比如我有一个仅有背景的Activity在销毁时,因为存在静态context,导致内存泄漏的情况。那么在dump了hprof文件后(dump前最好多点几次强制GC按钮),使用MAT打开会发现内存中多了一张图片,导出图片,用GIMP打开,会发现这是一张本不应该存在的图片,那么此时就会知道内存泄漏了,然后还可以用MAT找到持有这个图片的地方,进而找到内存泄漏的位置。

举个栗子:

class MainActivity : AppCompatActivity() {
    companion object {
        var mContext: Context? =null
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        mContext = this
    }
}

//之后的操作,伪代码
startSecondActivity
finishMainActivity

选择GC Root的路径,排除掉弱引用、软引用,因为这些不会导致内存泄漏,仅查看强引用

image.png

然后就可以看到最终是本该被销毁的MainActivity的mContext持有了引用,导致不能释放资源,不能回收,发生了内存泄漏的问题

image.png

好啦,这次内存中图片的内容就到这里啦,感谢阅读,如有疏漏,还请不吝赐教。