Android 中的图片内存
提出问题
先提出一个问题,将两张分辨率相同(48px * 48px),但文件大小不同的 png 图片,放在 drawable-xhdpi 文件夹下,在不同分辨率的手机上,所加载出来的 Bitmap 的占用内存大小分别是多少?
PS. 使用 Bitmap.getByteCount() 所获取的值作为占用内存的大小
通过以下代码获取 Bitmap 所占用的内存大小
1 | override fun onCreate(savedInstanceState: Bundle?) { |
PS. 表格中的 dpi 的通过
resources.displayMetrics.densityDpi
方法获取
图片1 (分辨率 48px * 48px,文件大小 1.24kb)
图片2 (分辨率 48px * 48px,文件大小 1.27kb)
手机 | 分辨率 | dpi | 图片1 的 byteCount | 图片2 的 byteCount |
---|---|---|---|---|
Nexus S | 480 * 800 | hdpi/240dpi | 5184 | 5184 |
Nexus 4 | 768 * 1280 | xhdpi/320dpi | 9216 | 9216 |
Pixel 1 | 1080 * 1920 | 420dpi | 15876 | 15876 |
小米6 | 1080 * 1920 | 480dpi | 20736 | 20736 |
Nexus 5 | 1080 * 1920 | xxhdpi/480dpi | 20736 | 20736 |
Pixel 3a | 1080 * 2220 | 440dpi | 17424 | 17424 |
Pixel XL | 1440 * 2560 | 560dpi | 28244 | 28244 |
通过以上的数据对比,可见图片1和图片2的即使文件大小不同,但运行时所占的内存都是相同的,因此我们猜测:图片运行时的内存大小和文件大小无关,只与图片的分辨率有关
接下来,我们再将两张图片放到 drawable-xxhdpi 文件夹下,再通过同样的方式进行计算,得出以下表格
手机 | 分辨率 | dpi | 图片1 的 byteCount | 图片2 的 byteCount |
---|---|---|---|---|
Nexus S | 480 * 800 | hdpi/240dpi | 2304 | 2304 |
Nexus 4 | 768 * 1280 | xhdpi/320dpi | 4096 | 4096 |
Pixel 1 | 1080 * 1920 | 420dpi | 7056 | 7056 |
小米6 | 1080 * 1920 | 480dpi | 9216 | 9216 |
Nexus 5 | 1080 * 1920 | xxhdpi/480dpi | 9216 | 9216 |
Pixel 3a | 1080 * 2220 | 440dpi | 7744 | 7744 |
Pixel XL | 1440 * 2560 | 560dpi | 12544 | 12544 |
这回我们同样发现图片1和图片2所占的内存都是相同的,更加肯定了我们在第一个表格后的猜测
但是,通过表格②中的「Pixel 1、小米6 和 Nexus 5」的纵向对比,三个手机的分辨率均为1080 * 1920,但三个手机的 dpi 分别为 420dpi,480dpi 和 480dpi,占用的内存为 7056、9216 和 9216,因此我们猜测,图片所占内存的大小变化也跟手机的 dpi 有关,跟手机的分辨率无关
源码分析
既然有以上的猜测,我们不如从源码中探索缘由
PS. 以下代码基于 Android 30 版本
在分析 decodeResource
方法之前,我们先来看一些基础概念
基本概念
android.util.DisplayMetrics#density: Float
显示的逻辑密度,这是 dip 单位的比例系数.
一个 dip 大概是 160dpi 屏幕上的的一个像素(例如分辨率240x320.尺寸为 1.5”x2”的屏幕),按这个标准提供显示的基准。
因此在 160dpi 的屏幕上,这个值为1,在120dpi 的屏幕上,这个值为 0.75,在480dpi 的屏幕上,这个值为3.依次类推
1 | 计算方式:density = densityDpi/160 |
android.util.DisplayMetrics#densityDpi: Int
缩写为 dpi
dots-per-inch
屏幕密度,表示每英寸屏幕上的像素点个数
1 | 计算方式为 dpi = 斜边长/英寸 |
在 Android 设备中,将 densityDpi 将设备分成多个显示级别,如下表
ldpi | mdpi | hdpi | xhdpi | xxhdpi | |
---|---|---|---|---|---|
dpi | 0-120 | 120-160 | 160-320 | 320-480 | 480-640 |
比例 1dp | 0.75px | 1px | 2px | 3px | 4px |
由于 mdpi 中 1dp 刚好等于 1px 所以将 mdpi 作为基准屏幕密度
一般来说设备都会在出厂时设置一个默认的 dpi ,设置其范围内的最大值
dip/dp
全称为 Density Independent Pixel
密度独立像素
1 | 计算方式为 dip/dp = px / (dpi / 160) |
TypeValue
1 | TypedValue typedValue = new TypedValue(); |
android.util.TypedValue#density: Int
如果是从 resource 中加载的图片等,这个值将会存储相对应的像素密度
例如:
从 ldpi 加载的 density == 120
从 mdpi 加载的 density == 160
decodeResource
decodeResource(Resources res, int id)
1 | public static Bitmap decodeResource(Resources res, int id) { |
decodeResource(Resources res, int id, Options opts)
1 | public static Bitmap decodeResource(Resources res, int id, Options opts) { |
-> 最后走到 android.graphics.BitmapFactory#decodeResourceStream
方法
1 |
|
最后走到 decodeStream()
中的 native 方法中对图片进行解码
在 native 代码中,会根据前面的方法中的 BitmapFactory.Options 中设置的参数 inDensity 和 inTargetDensity 参数,对图片进行缩放
简单来说:
inDensity 代表资源文件所在的文件夹 dpi
inTargetDenisity 代表 bitmap 会被绘制的地方的像素密度
从 BitmapFactory.cpp 中可以看到
1 | if (env->GetBooleanField(options, gOptions_scaledFieldID)) { |
当 isDenisity 、inTargetDensity 不为0,且 isDenisity != inScreenDensity 时候,会将图片进行缩放
缩放比例为 (float)inTargetDensity/isDenisity
内存大小计算
至此,我们可以回到文章开头的两个表格,来看一下图片所占用的内存是如何计算出来的
在计算之前,我们还要再看一个参数 Bitmap.Config
在 BitmapFactory.Options 中,inPreferredConfig 段默认为 ARGB_8888
该参数代表图片解码时使用的颜色模式
Bitmap.Config | 字节数 | 备注 |
---|---|---|
ALPHA_8 | 1 | 每个像素占8bit,存储图片的透明值 |
RGB_565 | 2 | 每个像素占16bit,RGB 通道分别占用5,6,5bit,存储图片的 RGB 值 |
ARGB_4444(已废弃) | 2 | 每个像素占16bit,即每个通道用4bit表示 |
ARGB_8888 | 4 | 每个像素占32bit,即每个通道用8bit表示 |
RGBA_F16 | 8 |
各个枚举中的数字之和代表其位数,例如 ARGB_8888 则占用 8+8+8+8 = 32bit = 4byte
现在我们可以计算出一张图片在解码后所占用的内存了
内存 = (图片像素宽 * scale ) * (图片像素高 * scale ) * 每个像素点内存占用
其中 scale = (float)inTargetDensity/inDenisity
我们来对上述的表格进行验证
对于表格1中
由于我们将资源文件放在了 xhdpi 文件夹中,所以 inDenisity = 320
Nexus S 设备
scale = (float)240/320 = 0.75
图片的内存 = (48 * 0.75 ) * (48 * 0.75 ) * 4 = 5184Pixel 1
scale = (float)420/320 = 1.3125
图片的内存 = (48 * 1.3125 ) * (48 * 1.3125 ) * 4 = 15876小米6
scale = (float)480/320 = 1.5
图片的内存 = (48 * 1.5 ) * (48 * 1.5 ) * 4 = 20736
同理可以验证表格2 中的数据
内存优化
由以上的 Bitmap 内存占用可知,要优化 bitmap 的内存大小
可以从以下几个方面出发
- 将正确的资源图片放到正确的目录下🐶
通过表格2和表格1的对比,如果本应放在 xxhdpi 下的图片如果放到了 xhdpi 下,会导致 bitmap 占用的内存变大
- 对资源图片进行取样,根据所展示的 view 的宽高修改图片解码时采样率,以降低图片的分辨率
eg.
1 | /** |