一、故事开篇:小明租房记
小明刚到大城市打工,想租一间房。他有两个选择:
- 方案A:找中介问“这套房子的实际居住面积是多少?”——中介会告诉他:卧室、客厅、厨房加起来共50平米,但不包括公摊的走廊、电梯间。
- 方案B:直接找开发商问“这块地皮上房子盖了多大?”——开发商说:整层楼建筑面积80平米,包含公摊、外墙、甚至楼道的消防栓。
在 Android 世界里,getDisplayMetrics() 就是那个“中介”,告诉你应用能用的显示区域(扣除了状态栏、导航栏、刘海等系统装饰)。
getRealSize() 就是那个“开发商”,告诉你屏幕的物理完整尺寸(整个显示面板,哪怕被系统 UI 遮住的地方也算)。
二、代码直观对比
// 在 Activity 或 View 中
DisplayMetrics metrics = getResources().getDisplayMetrics();
int usableWidth = metrics.widthPixels; // 可用宽度(像素)
int usableHeight = metrics.heightPixels; // 可用高度(像素)
// 另一种方式
DisplayManager displayManager = (DisplayManager) getSystemService(DISPLAY_SERVICE);
Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
Point realSize = new Point();
display.getRealSize(realSize);
int physicalWidth = realSize.x; // 物理宽度(全屏幕)
int physicalHeight = realSize.y; // 物理高度(全屏幕)
举个真实例子:一部手机物理分辨率 1080×2340,但底部有虚拟导航栏(高 150 像素),顶部有状态栏(高 80 像素)。
getDisplayMetrics()返回的heightPixels≈ 2340 - 150 - 80 = 2110。
getRealSize()返回的physicalHeight= 2340。
三、深入源码:中介 vs 开发商
1. 中介的算盘 —— getDisplayMetrics()
调用链:
Resources.getDisplayMetrics() → ResourcesImpl.getDisplayMetrics() → DisplayMetrics 从哪里填充?
最终追到 Display 类的方法:
// android.view.Display
public void getMetrics(DisplayMetrics outMetrics) {
// 注意:这里传入的 outMetrics 会被填入“应用区域”尺寸
synchronized (this) {
if (mDisplayInfo == null) {
updateDisplayInfoLocked();
}
mDisplayInfo.getAppMetrics(outMetrics, mCompatibilityInfo, mDisplayAdjustments);
}
}
mDisplayInfo 是 DisplayInfo 对象,由系统服务 DisplayManagerService 定期更新。关键在 getAppMetrics:
// android.view.DisplayInfo
public void getAppMetrics(DisplayMetrics outMetrics, CompatibilityInfo compatInfo,
DisplayAdjustments displayAdjustments) {
// 1. 先拿到物理尺寸
int width = logicalWidth;
int height = logicalHeight;
// 2. 减去系统窗口(状态栏、导航栏、手势区域)占用的空间
// 这一步通过 WindowManager 的 Policy(如 PhoneWindowManager)计算
Rect appRect = new Rect();
getNonDecorDisplaySize(appRect); // 计算扣除装饰后的可用矩形
outMetrics.widthPixels = appRect.width();
outMetrics.heightPixels = appRect.height();
// ... 密度等属性
}
核心原理:
系统在 WindowManagerService 中维护了一个“窗口策略”(WindowPolicy),它会根据当前的系统 UI 可见性、导航模式(手势/三键)、刘海屏缺口等,实时计算出一个 “安全的应用显示矩形” 。getDisplayMetrics() 拿到就是这个矩形的大小。
2. 开发商的账本 —— getRealSize()
调用链更直接:
// android.view.Display
public void getRealSize(Point outSize) {
synchronized (this) {
updateDisplayInfoLocked();
// 直接返回物理尺寸,不扣任何装饰
outSize.x = mDisplayInfo.logicalWidth;
outSize.y = mDisplayInfo.logicalHeight;
}
}
那 logicalWidth/Height 又是哪来的?它们来自 DisplayInfo 的 getPhysicalSize:
// android.view.DisplayInfo
public void getPhysicalSize(Point outSize) {
outSize.x = (int)(physicalWidth * displayScalingDisabled ? 1 : compatibilityScale);
outSize.y = (int)(physicalHeight * ...);
}
physicalWidth/Height 是从底层 SurfaceFlinger 读取的原始显示模式分辨率。SurfaceFlinger 通过 HWC(硬件合成器)直接询问屏幕驱动:“兄弟,你这块玻璃到底多少个发光点?”驱动回答:“1080×2340”。然后一路传上来,没有任何窗口策略的干预。
四、时序图:亲眼见证数据的旅程
下面用 mermaid 格式描述一次从应用调用到底层驱动的完整过程。
(注:如果渲染不出,可以想象成一群小人在传递纸条)
关键差异:
getDisplayMetrics()多了一次到WMS的“窗口策略”询问,会实时扣除系统 UI。getRealSize()直接从SurfaceFlinger拿最底层的物理值,干净利落。
五、为什么会有这种差别?—— Android 的设计哲学
Android 早期(2.x ~ 4.x)手机都是有物理按键的,屏幕就是纯粹的显示区,那时候两个方法返回值一样。后来有了虚拟导航栏、手势条、刘海、挖孔……Google 为了让应用开发者不用关心这些花里胡哨的异形屏,设计了 “应用可用区域” 的概念——你只管在你得到的尺寸里画 UI,那些系统区域由系统自动处理。
于是:
getDisplayMetrics():适合布局 UI,保证你的按钮不会被虚拟导航栏挡住。getRealSize():适合全屏沉浸式场景(游戏、视频),或者你想精确知道“这手机到底有多长”时使用。
六、小彩蛋:还有两个亲戚叫 getSize() 和 getMetrics()
其实 Display 还有 getSize(Point) 方法,它和 getDisplayMetrics() 返回的可用尺寸基本一致,只是单位不同(一个是 Point 像素对,一个是 DisplayMetrics 结构)。而 getRealMetrics(DisplayMetrics) 则类似于 getRealSize,填充分辨率和真实密度。
所以记忆口诀:
- Real 家族 = 真实物理尺寸(开发商)
- 非Real 家族 = 扣除系统UI后的可用尺寸(中介)
七、最后:一段让你恍然大悟的代码
// 在 Activity 中,假设全屏且导航栏是虚拟按键
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
// metrics.heightPixels = 2110(可用区域)
Point realSize = new Point();
getWindowManager().getDefaultDisplay().getRealSize(realSize);
// realSize.y = 2340(物理全屏)
// 那你猜导航栏高度是多少?
int navBarHeight = realSize.y - metrics.heightPixels;
// 结果是 230 像素(状态栏+导航栏?实际要分开算,但道理一样)