1. 前言
做App开发的同学都知道bitmap是占用内存最多的地方,如果说进入某个页面异常的卡顿,第一反应就是先找找是不是加载了大图的原因。
很多开发者如果不知道bitmap内存的计算规则,并且不知道Android路径下各个drawable文件夹区别,随意放置图片资源可能会导致加载到手机上占用的内存有好几倍的差距,极大的浪费了内存资源和造成界面的卡顿。
那么下面将介绍应用开发中Bitmap的两大问题:
- bitmap占用的内存如何计算?
- Android目录下设置了各个多个的drawable文件夹,那么图片资源应该如何存放,实际代码中又是如何选择对应路径下的资源的?
如果只关注答案不想知道原理可直接看末尾的总结。
2. 屏幕基础知识
2.1 硬件概念
物理属性 | ||
---|---|---|
名称 | 定义 | 单位 |
尺寸 | 对角线的长度 | inch(英寸),约等于2.54厘米 |
分辨率 | 屏幕方向上的像素个数 | pixel,个 |
屏幕密度 | 单位英寸上的像素个数 | dpi(android )ppi(ios) |
尺寸:一般就是我们听到的屏幕尺寸,就是对角线的长度,比如黑鲨5Pro的尺寸就是6.67英寸。
分辨率:手机屏幕的像素点数,,比如5Pro的分辨率就是2400*1080就是竖直方向2400个像素点,水平方向1080个像素点。
像素密度:单位区域内的像素量,密度越高,成像效果就越好,比如厂家推出的2K,4K屏。
2.2 软件概念
软件概念 | |
---|---|
名称 | 定义 |
px | 像素点 |
dp | dip(Density independent pixels ),密度无关像素 |
sp | 字体的单位,同dp,但是可以根据系统的设置来改变 |
dpi | 单位英寸上的像素个数 |
density | 密度限定符,表示当前屏幕的密度水平 |
px:像素点,电子屏幕组成成像的最基本单位 。但是像素大小是可变的,华为荣耀7是5.2英寸,424dpi,苹果7是5.0英寸469ppi。
dp: Android开发常用的单位,可以保证在不同屏幕像素密度的设备上显示相同的效果。
sp:如果不想字体大小根据系统设置的调整,则使用dp。
dpi:单位英寸上的像素个数,dot per inch,在单一变化条件下,屏幕尺寸越小,分辨率越高,像素密度越大。
density:密度水平。
2.3 计算公式
(宽高单位px,屏幕尺寸单位英寸)
tips: 公式而已,与实际参数未必一致
比如 黑鲨5pro按照公式计算出来得Dpi = 394.5,density =2.465,但是实际项目里却是dpi = 440,density =2.75。
2.4 Drawable文件夹区别
密度类型 | 像素密度(dpi) | dp与px关系 (density) | 代表的分辨率 |
---|---|---|---|
低密度(ldpi) | 120 | 1dp=0.75px | 240x320 |
中密度(mdpi) | 160 | 1dp=1.0px | 320x480 |
高密度(hdpi) | 240 | 1dp=1.5px | 480x800 |
超高密度(xhdpi) | 320 | 1dp=2.0px | 720x1280 |
超超高密度(xxhdpi) | 480 | 1dp=3.0px | 1080x1920 |
超超超高密度(xxxhdpi) | 640 | 1dp=4.0px | 2160x3840 |
2.5 默认的Drawable是多少?
我们知道 drawable 文件夹有一个不带后缀的,其实对应的是中密度(mdpi)也就是160dpi。
为什么这么规定:
1.160方便计算
比如 ,如果以240为基准,则XHDPI 下
2.在Google的官方文档中也有给出了解释,因为第一款Android设备(HTC的T-Mobile G1)是属于160dpi的。
实际上我们的设备dpi的值有很多,不会刚好是上面提到的这几个值,比如一台设备的dpi是280 ,那应该选择哪个文件夹下的图片呢?
在回答这个问题之前,先看一下Android对于一个bitmap占用内存的计算规则。
3. Bitmap占用内存计算规则
Android中加载一个Bitmap占用内存大小的计算公式如下:
scss
bitmap内存大小 = bitmap长(px) * bitmap宽(px) * 一个像素点占用的字节数
这里出现了3个因数, 宽高以像素为单位,这个好理解。最后一个是加载bitmap的编码格式,定义在 Bitmap.java下的内部枚举类 Config中。 常见的有以下四种:
yaml
ALPHA_8 : 每个像素占1个字节,只存储透明度信息,不存储颜色信息。
RGB_565 : 每个像素占2个字节,R占5位精度,G占6位精度,B占5位精度,一共16位精度,折合2字节,只存储颜色信息,没有透明度信息。
ARGB_4444 : 每个像素占2个字节,A(Alpha)、R(Red)、G(Green)、B(Blue)各占4位精度,一共16位的精度,折合2字节,有颜色和透明度信息。
ARGB_8888 : 每个像素占4个字节,有透明度和颜色信息。
ARGB_4444和ARGB_8888都有透明度和颜色信息,不过由于ARGB_4444的图像质量较差,已经被启用了,现在是推荐使用ARGB_8888,也是默认的编码格式。
3.1 举例
以目前手上的一台设备为例,先输出一段adb命令查看分辨率和屏幕密度输出如下:
arduino
adb shell wm size;adb shell wm density
Physical size: 720x1600
Physical density: 280
可知当前设备的屏幕密度为280,对照上面的表格是在 hdip 和 xhdip之间,假设项目中 drawable-hdpi和 drawable-xhdpi文件夹下都放了图片资源,最终是从哪个文件夹里读取呢?

现在由以下几点信息:
- 设置屏幕密度为:280
- 图片信息为:600 * 600
在XML里加载图片资源,并且在代码中获取bitmap相关的信息显示出来,布局和代码如下:

注意: 代码中 toBitmap 方法默认是使用的 Config.ARGB_8888 格式。
为了得到结论做以下3个测试:
-
只在drawable-hdpi下放图片
-
只在drawable-xhdpi下放图片
-
2个文件夹都放同一张图片
实际输出结果如下:
hdpi下放图片
xhdpi下放图片
2个文件夹都放图片
可以发现2个文件都放图片,和只在xhdpi下放图片的结果是一样的,所以得出结论:280dpi的设备图片读取的是drawable-xhdpi下的图片资源。
不过按照计算规则,预想的占用内存结果应该是:
1440000 byte = 600 * 600 * 4 = 1406 kb
但是实际上的显示是下面2种:
1102500 byte = 525 * 525 * 4 = 1076 kb 1960000 byte = 700 * 700 * 4 = 1914 kb
所以现在又有另外2个问题:
- 图片宽高都为600, 但是不同路径下实际显示却是 525 或 700
- 同一张图片放在不同的文件夹下,实际占用的内存差距非常大,是几倍的关系
3.2 问题解答
3.2.1 drawable文件下的图片的缩放规则
宽高为600的图片,实际加载的大小为 525 或者 700 是因为 drawable 下的图片会根据实际显示的设备进行缩放,缩放的计算规则也很简单,如下:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 当前屏幕密度 读取 d r a w a b l e 文件夹对应密度 = 当前显示的图片宽度 实际图片资源宽读 \frac{当前屏幕密度}{读取drawable文件夹对应密度} = \frac{当前显示的图片宽度}{实际图片资源宽读} </math>读取drawable文件夹对应密度当前屏幕密度=实际图片资源宽读当前显示的图片宽度
以当前设备280 dpi 计算宽度为例:
实际使用的是 drawable-xhdpi下的资源,也就是使用了 320 dpi 的资源。
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 280 d p i 320 d p i = 当前图片宽 600 \frac{280 dpi}{320 dpi} = \frac{当前图片宽}{600} </math>320dpi280dpi=600当前图片宽
因此可得当前屏幕的宽为 525。
3.2.2 放错图片资源实际内存的差距有多大
为了让第二个问题放大化, 我将图片只放在 drawable 文件夹下(也就是mdpi),那么在当前这台 280 dpi设备下加载出来会占用多少内存呢?
只在drawable下放图片
结果为:
4410000 byte = 1050 * 1050 * 4 = 4306 kb
我相信很多刚入坑的同学,对于图片资源的放置是不在乎的,直接放在默认的drawable目录下就好了,但是根据上面的结果会发现实际加载图片占用的内存差距高达有4倍以上。
现在市面上很多设备的屏幕密度都达到了 xxhdpi 也就是480dpi。如果图片放在了drawable下,那么实际上宽高各自会放大3倍 (480/160 = 3),实际占用内存就会被放大9倍。这是很大的资源浪费,也是非常不必要的。
所以在正确的资源路径下放图片,是非常重要且有必要的事情 那么源码中的如何定义图片文件夹选择的逻辑呢?
4. 文件夹选择规则
在多个drabable都有同名图片时,一个资源ID对应不止一个图片,在底层ResourceTypes.cpp文件下的getEntry方法里面就有一个循环,通过isBetterThan()函数选出最合适的图片
xref: /frameworks/base/libs/androidfw/ResourceTypes.cpp
ini
status_t ResTable::getEntry(const PackageGroup* packageGroup, int typeIndex, int entryIndex,const ResTable_config* config,Entry* outEntry) const{
...省略代码
const size_t typeCount = typeList.size();
for (size_t i = 0; i < typeCount; i++) {
if (bestType != NULL) {
//就在这 isBetterThan是找到更合适的配置(文件)
if (!thisConfig.isBetterThan(bestConfig, config)) {
//这个配置比上次更差,直接跳过
if (!currentTypeIsOverlay || thisConfig.compare(bestConfig) != 0) {
continue;
}
}
}
bestType = thisType;
bestOffset = thisOffset;
bestConfig = thisConfig;
bestPackage = typeSpec->package;
actualTypeIndex = realTypeIndex;
...省略代码
}
}
isBetterThan()方法关于Drawable相关的代码如下:
php
bool ResTable_config::isBetterThan(const ResTable_config& o,const ResTable_config* requested) const {
if (screenType || o.screenType) {
if (density != o.density) {
//注释1
// Use the system default density (DENSITY_MEDIUM, 160dpi) if none specified.
const int thisDensity = density ? density : int(ResTable_config::DENSITY_MEDIUM);
const int otherDensity = o.density ? o.density :
int(ResTable_config::DENSITY_MEDIUM);
// We always prefer DENSITY_ANY over scaling a density bucket.
if (thisDensity == ResTable_config::DENSITY_ANY) {
return true;
} else if (otherDensity == ResTable_config::DENSITY_ANY) {
return false;
}
int requestedDensity = requested->density;
if (requested->density == 0 || requested->density == ResTable_config::DENSITY_ANY) {
requestedDensity = ResTable_config::DENSITY_MEDIUM;
}
// DENSITY_ANY is now dealt with. We should look to
// pick a density bucket and potentially scale it.
// Any density is potentially useful
// because the system will scale it. Scaling down
// is generally better than scaling up.
int h = thisDensity;
int l = otherDensity;
bool bImBigger = true;
//注释2
if (l > h) {
int t = h;
h = l;
l = t;
bImBigger = false;
}
//注释3
if (requestedDensity >= h) {
// requested value higher than both l and h, give h
return bImBigger;
}
//注释4
if (l >= requestedDensity) {
// requested value lower than both l and h, give l
return !bImBigger;
}
//注释5
// saying that scaling down is 2x better than up
if (((2 * l) - requestedDensity) * h > requestedDensity * requestedDensity) {
return !bImBigger;
} else {
return bImBigger;
}
}
}
...省略代码
}
thisDensity是当前资源密度,otherDensity是之前循环时,已有最合适的密度,requestedDensity是请求密度(需要适配手机屏幕密度,比如400)
注释1:没有指明density的时候,默认就为160.这也就是为什么drawable文件夹下的图片默认为mdpi的原因
注释2:顺序调整
注释3:requestedDensity >= High > Low 返回High
注释4:requestedDensity <= Low < High 返回Low
注释5:如下图

更直观的表格为:

在Android 13源码里修改的更为简单
arduino
int h = thisDensity;
int l = otherDensity;
bool bImBigger = true;
if (l > h) {
std::swap(l, h);
bImBigger = false;
}
if (h == requestedDensity) {
// This handles the case where l == h == requestedDensity.
// In that case, this and o are equally good so both
// true and false are valid. This preserves previous
// behavior.
return bImBigger;
} else if (l >= requestedDensity) {
// requested value lower than both l and h, give l
return !bImBigger;
} else {
// otherwise give h
return bImBigger;
}
只有当请求的dpi小于 l 和 h时,才选l。其他情况都是选最大的h 。
5. 总结
- Bitmap占用内存计算公式
scss
bitmap内存大小 = bitmap长(px) * bitmap宽(px) * 一个像素点占用的字节数
第三个因素为Bitmap.Config下的值,默认使用 Config.ARGB_8888,每个像素占4个字节。
- 图片资源选择规则

和当前设备正好匹配的drawable文件夹时,优先选择比自己大一级的资源。
这个也是能理解的, 因为选择了比自己小的图片,需要做放大操作,图片会被拉伸,并且内存也会放大。所以相对来说选择比自己大的是更优解。当然理想型还是有和当前设备正好匹配的资源选择。