把一张
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"这个标签,而在它以什么密度进入解码器,以及解码器最终交出了多少像素。