要彻底搞懂 Android 的 drawable 密度适配,我们需要从「基础概念→核心原理→错误后果→源码拆解→通俗类比」5 个维度层层深入,最终让小白也能 get 到本质。
一、先搞懂 2 个基础概念:屏幕密度(dpi)与 drawable 目录的对应关系
在讲 "为什么要对应放" 之前,必须先明确 Android 对「屏幕密度」和「图片目录」的规则定义 ------ 这是所有适配的前提。
1. 什么是屏幕密度(dpi)?
dpi(Dots Per Inch)指 "每英寸屏幕的像素点数",直接决定屏幕的清晰度:
- 同样大小的屏幕,dpi 越高,像素越密集,显示越清晰(比如 5.5 英寸手机,1080P 屏比 720P 屏 dpi 更高);
- Android 为了统一适配标准,将「mdpi」定义为基准密度(160dpi) ,其他密度都是相对于 mdpi 的 "比例值"。
2. drawable 目录与 dpi 的对应规则
Android 通过 drawable 目录的 "密度后缀"(如 - hdpi、-xhdpi),告诉系统 "这个目录下的图片适合哪种 dpi 的设备"。具体对应关系如下表:
drawable 目录 | 对应 dpi 范围 | 相对于 mdpi 的密度比例 | 常见设备举例 |
---|---|---|---|
drawable-mdpi | 120~160 | 1.0x(基准) | 早期低端手机 |
drawable-hdpi | 160~240 | 1.5x | 早期中端手机 |
drawable-xhdpi | 240~320 | 2.0x | 主流手机(如早期 iPhone 8) |
drawable-xxhdpi | 320~480 | 3.0x | 中高端手机(如 iPhone 13) |
drawable-xxxhdpi | 480~640 | 4.0x | 旗舰手机(如三星 S23 Ultra) |
drawable(无后缀) | 默认 mdpi | 1.0x | 未指定密度的 "通用目录" |
关键结论:每个 drawable 目录都绑定了一个 "目标密度",图片放在哪个目录,系统就默认这张图是为该密度设备设计的。
二、核心问题 1:为什么必须把图片放到对应的密度目录?
答案很简单:平衡「显示清晰度」和「内存占用」 ------ 这是 Android 设计密度适配的核心目标。
我们用一张 "100x100 像素" 的图片举例(假设是 mdpi 基准图),不同密度设备的理想图片尺寸和加载逻辑如下:
-
mdpi 设备(1.0x):需要 100x100 像素的图,直接加载 drawable-mdpi 的图,无需缩放 ,清晰且内存占用低(1001004 字节 = 40KB,按 ARGB8888 计算);
-
hdpi 设备(1.5x):需要 150x150 像素的图(1001.5),加载 drawable-hdpi 的图,无需缩放,清晰且内存占用合理(150150*4=90KB);
-
xhdpi 设备(2.0x):需要 200x200 像素的图,加载 drawable-xhdpi 的图,无需缩放 ,清晰且内存占用可控(2002004=160KB)。
如果我们不给每个密度目录放对应尺寸的图,系统就会 "被迫缩放图片"------ 这就会引发问题。
三、核心问题 2:放错目录会有哪些具体问题?
放错目录的本质是 "图片尺寸与设备密度不匹配",系统会根据「设备密度 / 资源密度」计算缩放因子 ,强制拉伸 / 压缩图片,最终导致 2 类问题:模糊(像素化) 或 内存浪费(甚至两者并存)。
我们分 2 种典型错误场景分析:
场景 1:低分辨率图放到高密度目录(如 mdpi 图放 xhdpi 目录)
假设:把 100x100 像素的 mdpi 图,错误放到 drawable-xhdpi 目录,设备是 xhdpi(2.0x)。
系统计算逻辑:
- 系统识别到图片在 xhdpi 目录,默认这张图是 "为 xhdpi 设备设计的"(资源密度 = 2.0x);
- 设备实际密度是 xhdpi(2.0x),计算缩放因子:缩放因子 = 设备密度 / 资源密度 = 2.0 / 2.0 = 1.0;
- 系统会按 1.0 倍加载图片(即 100x100 像素),但 xhdpi 设备需要 200x200 像素的图才能填满屏幕 ------ 最终会把 100x100 的图拉伸到 200x200。
最终后果:
- 模糊(像素化) :100x100 的图被强行拉伸到 200x200,像素点被 "复制放大",画面出现锯齿、颗粒感(比如文字边缘模糊);
- 内存占用异常 :拉伸后的图片像素数是 200200=40000,内存占用 = 400004=160KB(和正确加载 xhdpi 图的内存一样),但清晰度完全不如正确的图。
场景 2:高分辨率图放到低密度目录(如 xhdpi 图放 mdpi 目录)
假设:把 200x200 像素的 xhdpi 图,错误放到 drawable-mdpi 目录,设备是 xhdpi(2.0x)。
系统计算逻辑:
- 系统识别到图片在 mdpi 目录,默认这张图是 "为 mdpi 设备设计的"(资源密度 = 1.0x);
- 设备实际密度是 xhdpi(2.0x),计算缩放因子:缩放因子 = 2.0 / 1.0 = 2.0;
- 系统会按 2.0 倍加载图片 ------ 但这里有个误区:不是先加载 200x200 的图再放大,而是先计算 "目标尺寸"(200*2.0=400x400),再把原图拉伸到 400x400。
最终后果:
- 严重模糊:200x200 的图被拉伸到 400x400,像素点被强行放大,画面模糊程度比场景 1 更严重;
- 内存暴增 :拉伸后的像素数是 400400=160000,内存占用 = 1600004=640KB------ 是正确加载 xhdpi 图(160KB)的 4 倍,会导致内存紧张,甚至 OOM(内存溢出)。
场景 3:图片放无后缀 drawable 目录(默认 mdpi)
所有非 mdpi 设备加载时,都会按 "设备密度 / 1.0x" 的缩放因子拉伸图片,相当于场景 1/2 的 "通用错误版":
- hdpi 设备(1.5x):mdpi 图被拉伸 1.5 倍,模糊 + 内存增加;
- xxhdpi 设备(3.0x):mdpi 图被拉伸 3 倍,严重模糊 + 内存暴增(1003=300 像素,300300*4=360KB,是正确 xxhdpi 图的 4 倍)。
四、深入源码:Android 如何处理密度适配?
要理解本质,必须看 Android 加载 drawable 资源的核心流程 ------ 关键逻辑在Resources
、AssetManager
、DisplayMetrics
三个类中,我们拆解核心步骤和代码。
1. 第一步:获取设备的屏幕密度(DisplayMetrics)
当 App 启动时,系统会通过DisplayMetrics
记录当前设备的密度信息,核心参数:
-
density
:设备密度相对于 mdpi 的比例(如 xhdpi 是 2.0f); -
densityDpi
:设备实际 dpi 值(如 xhdpi 是 320dpi); -
scaledDensity
:带字体缩放的密度(适配系统字体大小,此处暂不关注)。
代码获取方式(开发者可调用):
java
DisplayMetrics metrics = getResources().getDisplayMetrics();
float deviceDensity = metrics.density; // 如2.0f(xhdpi)
int deviceDpi = metrics.densityDpi; // 如320dpi(xhdpi)
2. 第二步:查找匹配密度的 drawable 资源(AssetManager)
当调用getResources().getDrawable(R.drawable.ic_xxx)
时,系统会通过AssetManager
(资源管理器)查找资源,核心逻辑是 "优先匹配设备密度的目录":
- AssetManager 根据资源 ID,先查找与设备密度完全匹配的目录(如 xhdpi 设备先找 drawable-xhdpi);
- 如果找不到,会按 "密度从高到低" 查找(如 xhdpi→hdpi→mdpi→ldpi),或 "从低到高"(取决于系统版本,默认优先高密目录以保证清晰度);
- 找到资源后,会将资源的「密度信息」(如 xhdpi 对应 2.0f)存入
TypedValue
对象。
3. 第三步:计算缩放因子并加载图片(Resources.loadDrawable)
找到资源后,Resources
的loadDrawable
方法会处理缩放,核心代码逻辑如下(简化版,来自 Android 13 源码):
java
private Drawable loadDrawable(TypedValue value, int id, Theme theme) {
// 1. 获取设备的DisplayMetrics(密度信息)
DisplayMetrics metrics = getDisplayMetrics();
// 2. 获取资源的密度(value.density:如mdpi是160dpi,xhdpi是320dpi)
int resourceDpi = value.density;
if (resourceDpi == 0) {
// 无密度后缀的目录(drawable),默认mdpi(160dpi)
resourceDpi = DisplayMetrics.DENSITY_DEFAULT; // 160dpi
}
// 3. 计算缩放因子:设备密度 / 资源密度(注意单位转换:dpi→比例)
float scale = metrics.densityDpi / (float) resourceDpi;
// 4. 根据缩放因子,计算图片的目标尺寸
int targetWidth = (int) (originalWidth * scale);
int targetHeight = (int) (originalHeight * scale);
// 5. 加载图片:按目标尺寸缩放Bitmap
BitmapFactory.Options options = new BitmapFactory.Options();
options.inDensity = resourceDpi; // 资源的原始密度
options.inTargetDensity = metrics.densityDpi; // 设备的目标密度
Bitmap bitmap = BitmapFactory.decodeResourceStream(
getAssets(), value, is, null, options
);
// 6. 生成Drawable并返回
return new BitmapDrawable(metrics, bitmap);
}
源码关键结论:
- 缩放因子由「设备 dpi / 资源 dpi」决定,而非 "目录名称"(目录名称只是告诉系统资源的 dpi);
BitmapFactory
会根据inDensity
和inTargetDensity
自动缩放图片,最终生成的 Bitmap 尺寸是「原始尺寸 * 缩放因子」;- 放错目录本质是 "资源 dpi 与设备 dpi 不匹配",导致缩放因子≠1.0,触发强制缩放。
五、通俗类比:用 "电影院海报" 讲透所有原理
为了让小白彻底记住,我们用一个生活中的故事类比:
假设「Android 小镇」里有 4 家不同大小的电影院(对应不同密度的设备):
-
小影院(mdpi):屏幕宽 100cm,需要 100cm 宽的海报(基准尺寸);
-
中影院(hdpi):屏幕宽 150cm,需要 150cm 宽的海报;
-
大影院(xhdpi):屏幕宽 200cm,需要 200cm 宽的海报;
-
超大影院(xxhdpi):屏幕宽 300cm,需要 300cm 宽的海报。
小镇的 "海报工厂"(开发者)需要给每家影院准备对应尺寸的海报,并存放在对应标注的仓库(drawable 目录):
- 小仓库(drawable-mdpi):放 100cm 的海报;
- 中仓库(drawable-hdpi):放 150cm 的海报;
- 大仓库(drawable-xhdpi):放 200cm 的海报;
- 超大仓库(drawable-xxhdpi):放 300cm 的海报。
1. 正确操作:对应仓库拿对应海报
大影院(xhdpi)的经理(系统)去大仓库(drawable-xhdpi)拿 200cm 的海报,直接贴在 200cm 的屏幕上 ------大小刚好,画面清晰,贴海报时也不费力气(内存占用合理) 。
2. 错误操作 1:小海报放大大仓库(mdpi 图放 xhdpi 目录)
大影院经理去大仓库,发现里面只有 100cm 的小海报(mdpi 图)。为了贴满 200cm 的屏幕,只能用 "拉伸机" 把 100cm 的海报拉到 200cm------海报上的文字变模糊(像素化),拉伸时还可能扯坏海报(内存异常) 。
3. 错误操作 2:大海报放小仓库(xhdpi 图放 mdpi 目录)
大影院经理去小仓库拿海报,发现里面是 200cm 的大海报(xhdpi 图)。但小仓库标注的是 "小影院专用"(mdpi),经理误以为这张海报是 100cm 的(资源 dpi=160),于是用拉伸机把 200cm 的海报再拉到 400cm(2.0 倍缩放)------海报严重模糊,还占满了整个墙面(内存暴增) 。
4. 错误操作 3:海报放 "无标注仓库"(drawable 目录)
所有影院经理都默认这个仓库里的海报是 "小影院专用"(mdpi)。中影院经理拿了 100cm 的海报,拉到 150cm;超大影院经理拿了 100cm 的海报,拉到 300cm------所有非小影院的海报都模糊,超大影院的模糊最严重。
六、最终总结:3 句话记住核心
-
对应放:什么密度的设备,就用对应密度目录的图(如 xhdpi 设备用 drawable-xhdpi 的图),系统无需缩放,清晰又省内存;
-
放错惨:低分辨率图放高密度目录→模糊;高分辨率图放低密度目录→模糊 + 内存暴增;
-
原理根:系统通过「设备 dpi / 资源 dpi」算缩放因子,放错目录就是让缩放因子≠1.0,强制拉伸导致问题。
从此,再也别把图片放错 drawable 目录啦!