Android适配完全指南

188 阅读11分钟

Android适配完全指南

概述

Android设备具有各种屏幕尺寸和密度,为了确保应用在不同设备上都能良好显示,需要进行分辨率适配。本文将从基础概念、核心原理、实际应用等多个维度深入解析Android的适配机制。

一、屏幕密度基础概念

1. 什么是屏幕密度(dpi)?

dpi(Dots Per Inch)指"每英寸屏幕的像素点数",直接决定屏幕的清晰度:

  • 同样大小的屏幕,dpi越高,像素越密集,显示越清晰
  • Android为了统一适配标准,将「mdpi」定义为基准密度(160dpi),其他密度都是相对于mdpi的"比例值"

2. Android屏幕密度分类

Android系统将屏幕密度分为以下几个等级:

密度类别DPI范围相对于mdpi的比例常见设备举例
mdpi120~1601.0x(基准)早期低端手机
hdpi160~2401.5x早期中端手机
xhdpi240~3202.0x主流手机(如早期iPhone 8)
xxhdpi320~4803.0x中高端手机(如iPhone 13)
xxxhdpi480~6404.0x旗舰手机(如三星S23 Ultra)

3. 密度计算公式

density = dpi / 160

二、Drawable资源适配详解

1. Drawable目录与dpi的对应规则

Android通过drawable目录的"密度后缀"(如-hdpi、-xhdpi),告诉系统"这个目录下的图片适合哪种dpi的设备"。

关键结论:每个drawable目录都绑定了一个"目标密度",图片放在哪个目录,系统就默认这张图是为该密度设备设计的。

2. 为什么必须把图片放到对应的密度目录?

答案很简单:平衡"显示清晰度"和"内存占用"——这是Android设计密度适配的核心目标。

以一张"100x100像素"的图片举例(假设是mdpi基准图),不同密度设备的理想图片尺寸和加载逻辑如下:

  • mdpi设备(1.0x):需要100x100像素的图,直接加载drawable-mdpi的图,无需缩放,清晰且内存占用低(100×100×4字节=40KB,按ARGB8888计算)
  • hdpi设备(1.5x):需要150x150像素的图(100×1.5),加载drawable-hdpi的图,无需缩放,清晰且内存占用合理(150×150×4=90KB)
  • xhdpi设备(2.0x):需要200x200像素的图,加载drawable-xhdpi的图,无需缩放,清晰且内存占用可控(200×200×4=160KB)

如果我们不给每个密度目录放对应尺寸的图,系统就会"被迫缩放图片"——这就会引发问题。

3. 放错目录的具体问题

放错目录的本质是"图片尺寸与设备密度不匹配",系统会根据「设备密度/资源密度」计算缩放因子,强制拉伸/压缩图片,最终导致两类问题:模糊(像素化)或内存浪费(甚至两者并存)。

场景1:低分辨率图放到高密度目录(如mdpi图放xhdpi目录)

假设:把100x100像素的mdpi图,错误放到drawable-xhdpi目录,设备是xhdpi(2.0x)。

系统计算逻辑:

  1. 系统识别到图片在xhdpi目录,默认这张图是"为xhdpi设备设计的"(资源密度=2.0x)
  2. 设备实际密度是xhdpi(2.0x),计算缩放因子:缩放因子=设备密度/资源密度=2.0/2.0=1.0
  3. 系统会按1.0倍加载图片(即100x100像素),但xhdpi设备需要200x200像素的图才能填满屏幕——最终会把100x100的图拉伸到200x200

最终后果:

  • 模糊(像素化):100x100的图被强行拉伸到200x200,像素点被"复制放大",画面出现锯齿、颗粒感
  • 内存占用异常:拉伸后的图片像素数是200×200=40000,内存占用=40000×4=160KB(和正确加载xhdpi图的内存一样),但清晰度完全不如正确的图
场景2:高分辨率图放到低密度目录(如xhdpi图放mdpi目录)

假设:把200x200像素的xhdpi图,错误放到drawable-mdpi目录,设备是xhdpi(2.0x)。

系统计算逻辑:

  1. 系统识别到图片在mdpi目录,默认这张图是"为mdpi设备设计的"(资源密度=1.0x)
  2. 设备实际密度是xhdpi(2.0x),计算缩放因子:缩放因子=2.0/1.0=2.0
  3. 系统会按2.0倍加载图片——但这里有个误区:不是先加载200x200的图再放大,而是先计算"目标尺寸"(200×2.0=400x400),再把原图拉伸到400x400

最终后果:

  • 严重模糊:200x200的图被拉伸到400x400,像素点被强行放大,画面模糊程度比场景1更严重
  • 内存暴增:拉伸后的像素数是400×400=160000,内存占用=160000×4=640KB——是正确加载xhdpi图(160KB)的4倍,会导致内存紧张,甚至OOM(内存溢出)
场景3:图片放无后缀drawable目录(默认mdpi)

所有非mdpi设备加载时,都会按"设备密度/1.0x"的缩放因子拉伸图片,相当于场景1/2的"通用错误版":

  • hdpi设备(1.5x):mdpi图被拉伸1.5倍,模糊+内存增加
  • xxhdpi设备(3.0x):mdpi图被拉伸3倍,严重模糊+内存暴增(100×3=300像素,300×300×4=360KB,是正确xxhdpi图的4倍)

4. Android如何处理密度适配(源码解析)

要理解本质,必须看Android加载drawable资源的核心流程——关键逻辑在Resources、AssetManager、DisplayMetrics三个类中。

第一步:获取设备的屏幕密度(DisplayMetrics)

当App启动时,系统会通过DisplayMetrics记录当前设备的密度信息,核心参数:

  • density:设备密度相对于mdpi的比例(如xhdpi是2.0f)
  • densityDpi:设备实际dpi值(如xhdpi是320dpi)
  • scaledDensity:带字体缩放的密度(适配系统字体大小,此处暂不关注)

代码获取方式(开发者可调用):

DisplayMetrics metrics = getResources().getDisplayMetrics();
float deviceDensity = metrics.density; // 如2.0f(xhdpi)
int deviceDpi = metrics.densityDpi;   // 如320dpi(xhdpi)
第二步:查找匹配密度的drawable资源(AssetManager)

当调用getResources().getDrawable(R.drawable.ic_xxx)时,系统会通过AssetManager(资源管理器)查找资源,核心逻辑是"优先匹配设备密度的目录":

  1. AssetManager根据资源ID,先查找与设备密度完全匹配的目录(如xhdpi设备先找drawable-xhdpi)
  2. 如果找不到,会按"密度从高到低"查找(如xhdpi→hdpi→mdpi→ldpi),或"从低到高"(取决于系统版本,默认优先高密目录以保证清晰度)
  3. 找到资源后,会将资源的"密度信息"(如xhdpi对应2.0f)存入TypedValue对象
第三步:计算缩放因子并加载图片(Resources.loadDrawable)

找到资源后,Resources的loadDrawable方法会处理缩放,核心代码逻辑如下(简化版):

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);
}

源码关键结论:

  1. 缩放因子由「设备dpi/资源dpi」决定,而非"目录名称"(目录名称只是告诉系统资源的dpi)
  2. BitmapFactory会根据inDensity和inTargetDensity自动缩放图片,最终生成的Bitmap尺寸是「原始尺寸×缩放因子」
  3. 放错目录本质是"资源dpi与设备dpi不匹配",导致缩放因子≠1.0,触发强制缩放

5. 图片缩放计算公式

图片缩放比例 = 当前设备dpi / 图片文件夹dpi
示例说明

假设UI按照320dpi设计切图:

  1. 如果放到xhdpi目录下(xhdpi对应320dpi): 运行在320dpi设备上时,图片缩放比例=320dpi/320dpi=1,图片宽高都不变

  2. 如果放到xxxhdpi目录下(xxxhdpi对应640dpi): 运行在320dpi设备上时,图片缩放比例=320dpi/640dpi=0.5,图片宽高都变为原来的0.5倍,实际显示效果变小

  3. 如果放到mdpi目录下(mdpi对应160dpi): 运行在320dpi设备上时,图片缩放比例=320dpi/160dpi=2,图片宽高都变为原来的2倍,实际显示效果变大

6. 切图放置原则

UI按照什么dpi切图,就应该放到对应dpi的文件夹中,否则会出现图片实际显示效果偏大或偏小。

7. 图片适配查找顺序

系统按特定顺序查找图片资源:

以320dpi设备为例:

  1. 优先查找目标xhdpi
  2. 大于目标dpi从低到高:xxhdpi>xxxhdpi...
  3. 小于目标dpi,从高到低:hdpi>mdpi

因此320dpi设备完整查找顺序为:xhdpi->[xxhdpi->xxxhdpi]->[hdpi->mdpi]

对于213dpi设备: 因为213dpi与240dpi接近,所以会以hdpi作为目标文件夹 查找顺序为:hdpi->[xhdpi->xxhdpi->xxxhdpi]->[mdpi]

注意:如果按照213dpi设计切图并放到hdpi,图片缩放比例=213dpi/240dpi=0.8875,并不是1,所以目前的1.33倍切图不能放到hdpi。目标切图要按照标准dpi切图,这样才能正确适配。

8. 其他Drawable目录

  • drawable-nodpi: 不管设备什么dpi,都不缩放
  • drawable-sw[xx]dp-hdpi: 限定最小尺寸xx的hdpi目录
  • drawable-sw[xx]dp-xhdpi: 限定最小尺寸xx的xhdpi目录

三、Values资源适配详解

1. 实际像素计算公式

实际px = dp * (dpi / 160)

2. 标准Values目录

Values目录的查找顺序与图片查找顺序一致: 目标文件夹->[目标文件夹以上从低到高]->[目标文件夹以下从高到低]

例如,在213dpi设备上,由于213dpi与240dpi接近,所以会以values-hdpi作为目标文件夹: values-hdpi->[values-xhdpi->values-xxhdpi->values-xxxhdpi]->[values-mdpi]

3. 宽度限定符(w)

计算公式
屏幕可用宽度 / (设备dpi / 160)

例如,分辨率为2560x1440,320dpi的设备: 对应的文件夹为2560/(320/160)=1280,即values-w1280dp

查找原则

如果同时存在以下目录:

  • values-w719dp
  • values-w720dp
  • values-w721dp

如果目标w720dp中没有找到,则会向下查找最接近720的719,而不是向上的721

同样有高度限定符:values-h[xx]dp,但在实际开发中,一般都按照宽度适配,高度自适应。

宽高限定符:values-w[xx]dp-h[xx]dp

4. 最小宽度限定符(sw)

"Smallest Width"字面意思是最小宽度,实际上是指可用屏幕区域的最小尺寸,即屏幕可用宽度和高度中的较小值。

例如,2560x1440分辨率的设备,最小尺寸是1440(不是宽度2560)。

计算公式
最小尺寸 / (设备dpi / 160)

例如,2560x1440 320dpi的设备: 对应values-sw(1440/(320/160))dp=values-sw720dp

查找原则

与宽度限定符相同,如果找不到目标的,则向下查找最接近目标的。

如果同时存在以下目录:

  • values-sw719dp
  • values-sw720dp
  • values-sw721dp

如果目标sw720dp中没有找到,则会向下查找最接近720的719,而不是向上的721

四、资源查找优先级

以2560x1440 320dpi为例:

目标文件夹优先级: values-sw720dp>values-w1280dp>values-xhdpi

目标文件夹满足同级规则:

[values-sw720dp>values-sw719dp...]>
[values-w1280dp>values-w1279dp...]>
[values-xhdpi,[向上values-xxhdpi>values-xxxhdpi][向下values-hdpi>values-mdpi]]

五、Values目录vs图片目录

  1. 图片目录会参与缩放换算,需要严格按照目标切图dpi放置到对应的目录中。 例如,UI按照320dpi的切图就一定要放到xhdpi目录中。

  2. Values目录不会参与缩放换算,任何values目录下的1dp=(dpi/160)px,所以不同的values目录要使用正确的dp值。

六、最佳实践建议

  1. 切图规范:严格按照标准dpi进行切图,并放置在对应的drawable目录中
  2. 多密度适配:为不同密度提供对应的图片资源,确保在各种设备上都有良好的显示效果
  3. 内存优化:避免将高分辨率图片放在低密度目录中,防止内存浪费
  4. 布局适配:使用dp单位进行布局,配合values-sw限定符实现屏幕适配
  5. 测试验证:在不同密度和尺寸的设备上测试应用显示效果

通过合理运用这些适配机制,可以确保Android应用在各种设备上都能提供一致且优质的用户体验。