📱 问题表现
在 Pixel 3 XL (Android 10) 设备上使用 MediaProjection 进行自动录屏时,出现了一个诡异的现象:
-
第一帧显示正常:录制开始时画面完整
-
后续帧异常:录制内容突然"放大"到左上角,只能看到屏幕左上角 720×1280 区域的内容
-
偶现概率较大:并非每次都出现,但复现率很高
-
特定设备:目前只在 Pixel 3 XL (Android 10) 上发现此问题,其他系统和机型正常
录制的视频效果像是把整个屏幕按 1:1 渲染到了 720×1280 的画布上,导致只能看到左上角的内容。
🔍 问题原因及定位
初步排查
从日志可以看到录屏流程正常:
Screen: 1440x2621 @ 560 dpi
Record: 720x1280 @ 560 dpi ← 问题就在这里
Creating VirtualDisplay - width: 720, height: 1280, density: 560
VirtualDisplay created successfully
注意到一个关键信息:录制分辨率缩小了一半(1440→720),但 DPI 没有变化(依然是 560)。
根本原因分析
VirtualDisplay 的缩放逻辑
Android 系统在创建 VirtualDisplay 时,会根据 DPI 和分辨率 计算"每像素密度"来决定如何缩放屏幕内容:
每像素密度 = DPI / 分辨率宽度
原始代码的问题:
// 物理屏幕
屏幕分辨率: 1440 × 2960
DPI: 560
每像素密度: 560 / 1440 = 0.389 dpi/px
// VirtualDisplay(错误配置)
录制分辨率: 720 × 1280
DPI: 560 ← 问题:没有按比例缩放
每像素密度: 560 / 720 = 0.778 dpi/px ← 比屏幕高了2倍!
系统发现 VirtualDisplay 的"每像素密度"比物理屏幕高一倍,会认为这是一个**"高密度小屏幕"**,需要显示更多内容才能填满。结果就是:
-
❌ 不是将整个屏幕内容缩放到 720×1280
-
❌ 而是将屏幕左上角的 720×1280 区域按 1:1 渲染到 VirtualDisplay
为什么第一帧正常?
这是 VirtualDisplay 的初始化时序问题:
T0: VirtualDisplay 创建
→ 配置了错误的 DPI,但还未开始渲染
T1: 第一帧渲染
→ 系统使用默认缩放逻辑,尚未严格检查 DPI
→ ✅ 第一帧显示正常
T2: 后续帧渲染
→ 系统检测到 DPI 不匹配
→ 重新计算缩放比例
→ ❌ 错误判断为"高密度小屏幕",只渲染左上角
为什么只在 Pixel 3 XL (Android 10) 上出现?
这需要 5 个条件同时满足:
| 条件 | Pixel 3 XL | 其他设备 | 说明 |
|------------|--------------|----------------------|---------------------------------|
| Android 10 | ✅ | ❌ 多数是 11+ 或 8-9 | 过渡版本,VirtualDisplay 有 bug |
| 2K 分辨率 | ✅ 1440×2960 | ❌ 多数 1080p | 缩放比例刚好 2:1 |
| 极高 DPI | ✅ 560 | ❌ 多数 400-480 | DPI 差距最大(2倍) |
| 原生系统 | ✅ Pixel | ❌ 定制 ROM | 严格按规范,不做兼容处理 |
| 整数倍缩放 | ✅ 0.5 | ❌ 0.67等 | 触发边界条件 |
为什么其他设备没事?
-
Android 11+ 设备:Google 修复了 Android 10 的 bug,容错性更好
-
1080p 设备:缩放比例 1.5:1,DPI 差距只有 1.5 倍(影响较小)
-
定制 ROM:厂商为了兼容性会自动修正 DPI 不匹配的问题
-
低 DPI 设备:DPI < 500 时,不匹配的影响不明显
Pixel 3 XL 刚好踩中了所有"地雷开关",所以问题暴露了。
🛠️ 解决方案
核心思路
DPI 必须和分辨率成正比,确保 VirtualDisplay 的"每像素密度"与物理屏幕一致。
代码修改
修改前: // 获取屏幕 DPI,直接传给 VirtualDisplay
java
DisplayMetrics metrics = LMResourceHelper.sharedInstance().getDisplayMetrics();
int screenDensity = metrics.densityDpi; // 560
mVirtualDisplay = mMediaProjection.createVirtualDisplay(
"Recording Display",
720, // mRecordWidth
1280, // mRecordHeight
560, // screenDensity ❌ 错误:没有缩放
0,
mInputSurface,
callback,
null
);
修改后:
DisplayMetrics metrics = LMResourceHelper.sharedInstance().getDisplayMetrics();
int screenDensity = metrics.densityDpi;
// 1. 计算缩放比例:录制分辨率与屏幕分辨率的比例
float scaleRatio = (float) mRecordWidth / metrics.widthPixels;
// 例:720 / 1440 = 0.5
// 2. DPI 也按比例缩放,避免 Android 10 高分辨率设备录屏缩放异常
int scaledDensity = (int) (screenDensity * scaleRatio);
// 例:560 * 0.5 = 280
// 3. 记录日志,方便调试
Log.d(TAG, "Screen: " + metrics.widthPixels + "x" + metrics.heightPixels
- " @ " + screenDensity + " dpi");
Log.d(TAG, "Record: " + mRecordWidth + "x" + mRecordHeight
- " @ " + scaledDensity + " dpi (scaled by " + scaleRatio + ")");
mVirtualDisplay = mMediaProjection.createVirtualDisplay(
"Recording Display",
720, // mRecordWidth
1280, // mRecordHeight
280, // scaledDensity ✅ 正确:DPI 按比例缩放
0,
mInputSurface,
callback,
null
);
修改效果对比
| 设备 | 修改前 DPI | 修改后 DPI | 效果 |
|-----------------|------------|------------|---------------------------|
| Pixel 3 XL (2K) | 560 | 280 | ✅ 修复缩放异常 |
| 1080p 设备 | 440 | 220 | ✅ 改善稳定性 |
| 720p 设备 | 320 | 320 | ✅ 无影响(缩放比 1.0) |
| 低分辨率设备 | - | - | ✅ 无影响(不走缩放逻辑) |
修复后的日志:
Screen: 1440x2621 @ 560 dpi
Record: 720x1280 @ 280 dpi (scaled by 0.5) ✅
Creating VirtualDisplay - width: 720, height: 1280, density: 280
VirtualDisplay created successfully
现在"每像素密度"一致了:
-
物理屏幕:560 / 1440 = 0.389 dpi/px
-
VirtualDisplay:280 / 720 = 0.389 dpi/px ✅
系统会正确地将整个屏幕内容等比例缩放到 720×1280,不再出现左上角放大的问题。
📊 影响范围评估
对其他设备的影响
| 设备类型 | 影响 | 说明 |
|----------------------|--------------|----------------------------------|
| 低分辨率设备 (<720p) | ✅ 无影响 | 不走缩放逻辑,直接使用屏幕分辨率 |
| 720p 设备 | ✅ 无影响 | scaleRatio = 1.0,DPI 不变 |
| 1080p 设备 | ✅ 改善 | DPI 正确缩放,避免潜在问题 |
| 2K/4K 设备 | ✅ 修复/改善 | 解决 DPI 不匹配导致的缩放异常 |
安全性分析
-
逻辑简单:只是一个乘法运算 scaledDensity = screenDensity * scaleRatio
-
符合规范:VirtualDisplay 的 DPI 应该和分辨率成正比(Android 官方建议)
-
向后兼容:对于 scaleRatio = 1.0 的设备,DPI 不变
-
不引入新依赖:没有调用额外的 API
结论:低风险、高收益。
总结与延伸
核心要点
-
问题本质:DPI 和分辨率不成正比,导致 VirtualDisplay 缩放逻辑错误
-
触发条件:需要特定的 Android 版本、分辨率、DPI、系统类型、缩放比例同时满足
-
解决方案:DPI 按照分辨率缩放比例同步缩放,确保"每像素密度"一致
-
影响范围:只改善,不破坏,对所有设备都是正向优化
技术启示
-
隐性 bug 难以发现:代码可能一直有问题,但只在特定条件下暴露
-
设备碎片化:Android 设备差异巨大,需要在不同设备上充分测试
-
原生系统更严格:Pixel 等原生设备会暴露更多兼容性问题,是最佳测试设备
-
查看官方文档:VirtualDisplay 的官方文档建议 DPI 应该根据分辨率调整
类似问题排查思路
遇到 VirtualDisplay/MediaProjection 相关问题时:
-
对比参数:检查分辨率、DPI、缩放比例是否匹配
-
查看日志:记录详细的创建参数,方便对比
-
计算密度:DPI / 分辨率 应该在 VirtualDisplay 和物理屏幕上保持一致
-
测试边界条件:在高分辨率、高 DPI、原生系统设备上测试
-
参考官方示例:Google 官方的 Grafika 项目是最佳参考
参考资料