Android 屏幕适配的前世今生

把一张 300 × 200 的 PNG 放进 res/drawable-mdpi/,在两台手机上用同一行 imageView.setImageResource() 显示。代码没变、文件没变,调试时看到的 Bitmap 却可能一台是 300 × 200,另一台是 900 × 600。后者的像素内存恰好是前者的 9 倍。

一句话结论:屏幕分辨率决定屏幕有多少物理像素;Android 用逻辑密度把 dp 换成 px;图片占多少内存则取决于解码后的像素尺寸和像素格式,不能仅凭手机分辨率或图片文件大小判断。

本文讨论 Android 公开的密度与资源规则,以及通常以 ARGB_8888 解码的普通软件 Bitmap。文中的手机参数是为了计算而设的假想设备,不是某款机型的实测值;具体解码结果还会受资源目录、解码参数和图片库影响。

1. 为什么 Android 要发明 dp

早期 Android 手机常以 HVGA、约 160 dpi 的屏幕作为设计基准。那时写一个 100px 宽的按钮,看起来似乎没问题。后来,同样大小的玻璃面板塞进了更多像素:如果仍固定用 100px,按钮在高密度屏幕上就会显得更小。

Android 1.6(API 4)引入多屏幕支持和密度兼容规则;随后出现更多密度档位。再往后,Android 3.2(API 13)提供 sw<N>dp 之类的最小宽度限定符,让开发者按可用空间 切换布局。Android 5.0(API 21)加入平台 VectorDrawable,简单图标不再必须为每个密度准备一份 PNG。今天还有平板、折叠屏、分屏和自由窗口:同一设备的应用窗口也可能随时变宽或变窄。多屏幕历史与兼容配置、资源限定符、VectorDrawable API、自适应布局

阶段 当时的主要问题 Android 给出的工具
早期手机 像素尺寸几乎被当成界面尺寸 以约 160 dpi 为 dp 基准
多密度手机 同样的 px 在高密度屏幕上变小、位图变糊 dp、sp、drawable-mdpi 等密度资源
平板加入 密度相同也可能有完全不同的可用面积 sw<N>dp 与更灵活的布局
矢量与高分屏 多套 PNG 维护成本变高 适合简单图形的 VectorDrawable
折叠屏与多窗口 一台设备运行时也会改变窗口宽度 按当前窗口空间做响应式布局

这段历史可以压缩成一张地图:

text 复制代码
屏幕硬件:物理尺寸 + 像素行列 → 分辨率、物理 PPI
Android 配置:逻辑密度 densityDpi → density = densityDpi / 160
界面布局:dp × density → 实际绘制用的 px
图片资源:源图片尺寸 × 目标密度 / 资源密度 → 解码后尺寸
像素内存:解码后宽 × 解码后高 × 每像素字节数

这条链里,分辨率、逻辑密度、图片像素内存是三个不同量。先把词讲准,后面的 9 倍就好理解了。

2. 分辨率、PPI、像素比,分别在说什么

屏幕分辨率 是屏幕像素的行列数。例如 1080 × 1920 表示宽方向 1080 个物理像素、高方向 1920 个物理像素。它不告诉你屏幕有多大:同样的分辨率放在 6 英寸和 10 英寸面板上,像素疏密不同。

物理像素密度常用 PPI 表示,即每英寸有多少像素。知道对角线英寸数时,可以近似计算:

text 复制代码
PPI = √(屏幕宽像素² + 屏幕高像素²) / 屏幕对角线英寸

Android 逻辑密度 是系统用于界面缩放的 densityDpi;density = densityDpi / 160。160 dpi 是基准,此时约 1dp = 1px。一台逻辑密度为 480 dpi 的设备,density = 3,所以 100dp 通常对应 300px。这里的 480 是系统采用的逻辑值 ,不必等于面板真实测得的 PPI;xdpi、ydpi 才用于描述物理方向上的像素密度。DisplayMetrics 官方 API、密度适配指南

开发中说的"像素比"容易指两件事:

说法 含义 例子
Android 布局缩放比 px / dp,通常就是 density density = 3 时,100dp → 300px
Web/H5 设备像素比 window.devicePixelRatio,描述 CSS 像素与设备像素的缩放关系 WebView 页面常据此准备高清图片

做原生 View 或 Compose 布局时,先说 density;讨论网页时再说 devicePixelRatio。两者经常数值接近,但它们属于不同坐标体系,不能把 CSS 的 px 直接当成 Android 的原生 px。Android WebView 屏幕支持文档

还有一个常被漏掉的区别:dp 用于控件尺寸、间距;sp 用于字号,并受用户字体大小设置影响。不要从屏幕分辨率反推字号。新的 Android 字体缩放还可能是非线性的,因此也不宜长期用一个固定的 scaledDensity 公式处理所有字号。DisplayMetrics 官方 API

3. 不同分辨率,界面为什么还能一样宽

假设下面三台设备都给应用约 360 × 640dp 的逻辑空间,并暂时忽略状态栏、导航栏等系统区域:

假想设备 屏幕分辨率 逻辑密度 density 100dp 对应
A 360 × 640px 160 dpi,mdpi 1 100px
B 720 × 1280px 320 dpi,xhdpi 2 200px
C 1080 × 1920px 480 dpi,xxhdpi 3 300px

因此,B 的分辨率是 A 的 2 倍宽高,但同一个 100dp 控件依旧占约相同的逻辑宽度。反过来,同为 1080 × 1920px 的两台手机,如果系统逻辑密度不同,可用的 dp 宽度也可能不同 。厂商设置、用户显示大小、窗口尺寸都会影响实际观察结果。做页面布局应看当前窗口的 dp 空间,而不是只列一张"720p/1080p/2K 适配表"。自适应布局指南

不过,这还没有解释开头那张图片的 9 倍内存。原因在资源解码环节。

4. 同一张图片,在不同手机上究竟占多少内存

先规定计算口径:**同一个 300 × 200px 文件,按 ARGB_8888 解码,每像素 4 字节;只计算 Bitmap 像素数据的理论大小,不算 Java 对象、缓存、图形纹理、行对齐和临时解码缓冲。**下表中的 MiB 使用 1024 × 1024 字节换算。

情况一:放在 drawable-mdpi,让系统按密度缩放

Android 会根据资源目录的源密度与设备目标密度缩放 Bitmap。简化计算式是:

text 复制代码
解码后宽 ≈ 源宽 × 目标 densityDpi / 资源 densityDpi
解码后高 ≈ 源高 × 目标 densityDpi / 资源 densityDpi
像素内存 ≈ 解码后宽 × 解码后高 × 4

在上面的 A/B/C 三台假想设备上,同一份 drawable-mdpi 文件得到:

设备 缩放比 解码后 Bitmap 像素字节数 约合 MiB
A:mdpi 160 / 160 = 1 300 × 200 240,000 B 0.23 MiB
B:xhdpi 320 / 160 = 2 600 × 400 960,000 B 0.92 MiB
C:xxhdpi 480 / 160 = 3 900 × 600 2,160,000 B 2.06 MiB

宽高各放大 3 倍,像素总数就是 9 倍,所以 C 的 Bitmap 像素数据是 A 的 9 倍。真正触发变化的是资源密度到目标密度的缩放 ;表中的屏幕分辨率只是与逻辑密度一起变化,不能把"1080p"本身当成 Bitmap 变大的原因。BitmapFactory.Options 文档明确说明,decodeResource() 会填入资源密度与目标密度,默认 inScaled = true 时可在加载时完成缩放。BitmapFactory.Options 官方 API

情况二:相同像素的文件不做密度缩放

如果仍是这份 300 × 200px 图片,但从文件或网络按原始尺寸解码,且没有设置密度缩放或额外的目标尺寸,那么 A/B/C 上的 Bitmap 都是 300 × 200 × 4 = 240,000 B ≈ 0.23 MiB。放进 drawable-nodpi 的位图资源也用于避免按设备密度自动缩放。资源限定符中的 nodpi、BitmapFactory.Options 官方 API

它们在屏幕上看起来可能大小不同 :同样的 300 个物理像素,在高密度屏幕上代表更少的 dp。若 ImageView 把它拉伸到固定的 300dp,显示尺寸会变,但原 Bitmap 的像素缓冲区不一定随之变大;如果图片库为了目标 View 重新采样解码,则又会产生另一种尺寸。要回答内存,必须问清"显示时拉伸"还是"解码时重采样"。

情况三:各档密度提供对应图片

如果设计要求图片始终显示为 300 × 200dp,可以为 mdpi、xhdpi、xxhdpi 分别准备 300 × 200、600 × 400、900 × 600px 素材。系统选到对应资源后,三台设备的解码尺寸及像素内存仍大致是上表的 0.23、0.92、2.06 MiB,只是高密度设备直接拿到足够清晰的原生像素,避免把低分辨率图硬放大。密度资源的常用比例是 ldpi:mdpi:hdpi:xhdpi:xxhdpi:xxxhdpi = 0.75:1:1.5:2:3:4。资源限定符与密度比例

这里还有一笔容易算错的账:即便一张 PNG 在 APK 中只有 80 KB,解码后也可能占 2.06 MiB。PNG/JPEG/WebP 文件大小是编码后的磁盘或网络体积 ;Bitmap 的像素内存看的是解码后宽高与像素格式 。单纯降低 JPEG 质量通常能减小文件,却不必然降低解码后的像素内存。Bitmap 内存指南

5. 现在常见的屏幕适配方式,原理分别是什么

前面解决的是"一个 dp 和一张图片怎样变成像素"。真正做页面时,还要回答另一个问题:窗口从窄变宽后,内容是继续拉伸,还是改变排列方式?常见方案可以按谁决定尺寸分成几层。

方式 核心原理 适合解决 主要边界
dp / sp + 密度资源 系统把逻辑尺寸换成像素,并按密度选择或缩放图片 不同像素密度下保持控件大小、图标清晰 不会自动把单栏页面变双栏
弹性布局与相对约束 父容器把当前可用空间作为测量约束,子控件按内容、剩余空间或比例分配尺寸 同一页面适应相近的手机宽度 极窄或极宽窗口仍可能需要换结构
sw<N>dp、w<N>dp 资源断点 系统按配置匹配合适的布局或尺寸资源 手机和平板使用不同布局、间距 多套 XML / 资源需要共同维护
窗口尺寸等级 + 自适应布局 运行时按当前应用窗口的宽高区间选择单栏、双栏、导航形式等 折叠屏、平板、分屏、桌面窗口 需要管理布局切换时的页面状态
修改 density 的设计稿宽度适配 人为改变 dp → px 比例,让设计稿宽度恰好填满目标宽度 旧项目大量固定 dp 尺寸、短期统一缩放 会影响整个资源和控件体系,无法代替结构适配

基础层:dp 加弹性布局

dp 解决的是密度差异;弹性布局解决的是窗口实际给了多少空间 。在 View 页面中,wrap_content 让控件按内容测量,match_parent 占用父容器给定空间,ConstraintLayout 中宽度为 0dp 的 match constraints 会在约束边界之间分配宽度,还能设置最小/最大宽度、宽高比或百分比参考线。Compose 中的 fillMaxWidth()、weight()、BoxWithConstraints 遵循相同的测量思路:父布局先传入约束,子布局再决定尺寸。View 响应式布局、ConstraintLayout 官方指南、Compose 布局约束

例如,一张卡片设置为"填满可用宽度,但最多 600dp",在 360dp 的窗口里会收窄,在 900dp 的窗口里不会无限拉长。百分比布局也属于这层:50% 应是父容器当前宽度的一半,而不是某张设计稿或整块物理屏幕的一半。它适合分栏、图片比例,不适合把字号、点击区域和所有间距都按屏幕宽度同比放大。

断点层:资源限定符与窗口尺寸等级

单栏卡片再怎么拉伸,也不能充分利用平板宽度。这时要改变页面结构。传统 View 项目可放一份 res/layout/main.xml,再放一份 res/layout-w600dp/main.xml:当当前可用宽度 达到 600dp 时,系统选择后者。sw600dp 看的是配置中的最小宽度,更适合表达设备或窗口的整体尺寸级别;w600dp 看当前可用宽度,横竖屏或窗口变化时更直接。断点是"有足够空间放下另一套内容"的业务决定,不是某个手机分辨率的代号。资源限定符、View 自适应布局

现在更常见的做法,是用窗口尺寸等级 在运行时判断可用空间,再选择页面形态。例如窄窗口显示"列表页 → 详情页",宽窗口同时显示列表和详情;底部导航也可以在宽窗口改成侧边导航。窗口等级会随旋转、折叠和分屏变化,判断对象应是应用窗口 ,不是设备标称分辨率。Compose 可以结合 Material 3 Adaptive 的窗口信息与多窗格组件;View 项目也能按窗口等级切换布局或使用 SlidingPaneLayout。窗口尺寸等级、典型自适应布局

这里的关键不是"把两个页面都缩小一点",而是决定什么时候并排、什么时候只显示一个。切换时还要保留当前选中项和滚动位置,避免窗口一变就把用户带回首页。

全局缩放层:所谓"设计稿宽度适配"

有些老项目按一张固定宽度的设计稿写了大量 dp,会采用"今日头条方案"及类似库:先指定设计宽度,例如 360dp,再计算新的逻辑密度。

text 复制代码
目标 density = 当前可用宽度 px / 设计宽度 dp

假设窗口宽 1080px、设计宽 360dp,目标 density = 3,于是原布局的 100dp 被换算为 300px。方案通常通过修改 DisplayMetrics.density 等值,让已有 dp 在整个页面按新比例计算。它改的是坐标尺 ,所以接入旧页面看起来省事;同时,文本、系统控件、第三方控件、图片资源密度以及获取窗口 dp 宽度的代码都可能受影响。相关开源实现的维护者也明确讨论过多个组件改写同一 DisplayMetrics.density 时的冲突。AndroidAutoSize 项目、项目维护者对密度冲突的说明

我的选择是:**新页面先用系统 dp、弹性布局和窗口断点;已有大量固定尺寸的旧页面,才评估全局密度缩放。**它可以让固定设计稿"整体变大或变小",但遇到平板、折叠屏和分屏,仍要补单栏/双栏等结构变化。适配是否成功,最终要看不同窗口宽度、字体大小和图片内存表现,而不是只看一台手机上的截图。

6. 实际项目应该怎么做

我会按图片用途做选择,而不是给所有图片套同一种"适配方案":

用途 选择 原因
按钮图标、简单插画 优先考虑矢量图;需要时用密度位图 保持清晰,避免维护多份简单图形
有明确 dp 尺寸的位图 UI 素材 提供合适的密度版本 防止低密度素材被放大而模糊
照片、列表缩略图、网络大图 按目标 View 尺寸采样解码 原图像素往往远超实际显示所需
必须保持原始像素尺寸的资源 drawable-nodpi 或明确控制解码参数 避免隐式密度缩放
平板、折叠屏和分屏页面 根据当前窗口可用 dp 宽度布局 窗口大小比设备标称分辨率更直接

遇到"这张图片为什么在某台手机上更占内存",我的排查顺序是:先看图片来自网络还是 drawable-* ;再看资源目录和设备 densityDpi;接着打印解码后的 bitmap.width、bitmap.height、bitmap.config;最后比较 getByteCount() 与 getAllocationByteCount()。getByteCount() 表示当前像素数据所需的最小字节数,复用 Bitmap 时实际分配容量可能更大,应看 getAllocationByteCount();硬件位图的像素数据又属于图形内存,不能直接拿 Java 堆占用套公式。Bitmap API、Bitmap 内存指南

对大图,先读取宽高,再按控件需要的尺寸采样,比"整张解码后交给 ImageView 缩小显示"更有效。官方给的典型例子是:2048 × 1536 图片用 inSampleSize = 4 解码,约得到 512 × 384 Bitmap;这是从像素数量上减少内存,不只是改变显示比例。大图采样解码指南

最后记住开头那张 300 × 200 图:它在 xxhdpi 手机上可能是 0.23 MiB,也可能是 2.06 MiB。差别不在"手机是不是 1080p"这个标签,而在它以什么密度进入解码器,以及解码器最终交出了多少像素。

相关推荐
andyweike1 小时前
kotlin dsl library工程怎么配置
android·开发语言·kotlin
啵啵啵鱼1 小时前
九、JDBC 操作 MySQL 数据库
android·数据库·mysql
用户69371750013841 小时前
当我说 Claude 搭配 GLM-5.3-Flash 太好用了,同事却提醒我:你可能正在失去对代码的掌控
android·后端·ios
张小姐的猫2 小时前
【MySQL】数据类型
android·linux·数据库·c++·mysql·adb·php
2501_916008892 小时前
马甲包过审卡在哪?Ipa Guard 能做的和做不到的
android·小程序·https·uni-app·iphone·webview
换元不配限3 小时前
Android 接入 protobuf 实战:从 Gradle 配置到避开 R8 混淆坑
android·grpc·序列化·protobuf·protocolbuffers·二进制序列化·.proto
Marco_Marco1353 小时前
定位SDK接入实战:Android/iOS/鸿蒙三端怎么选、怎么接
android·ios·harmonyos
芝麻别开门15 小时前
gst-shark安卓编译使用方法
android·gstreamer·gst-shark
2501_9159214318 小时前
iOS应用耗电测试:使用Energy Log、Keymob和Instruments进行性能监控与能源消耗分析
android·ios·小程序·https·uni-app·iphone·webview