⏺ Android 录屏缩放异常排查:Pixel 3 XL 上的完美风暴

📱 问题表现

在 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等 | 触发边界条件 |

为什么其他设备没事?

  1. Android 11+ 设备:Google 修复了 Android 10 的 bug,容错性更好

  2. 1080p 设备:缩放比例 1.5:1,DPI 差距只有 1.5 倍(影响较小)

  3. 定制 ROM:厂商为了兼容性会自动修正 DPI 不匹配的问题

  4. 低 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 不匹配导致的缩放异常 |

安全性分析

  1. 逻辑简单:只是一个乘法运算 scaledDensity = screenDensity * scaleRatio

  2. 符合规范:VirtualDisplay 的 DPI 应该和分辨率成正比(Android 官方建议)

  3. 向后兼容:对于 scaleRatio = 1.0 的设备,DPI 不变

  4. 不引入新依赖:没有调用额外的 API

结论:低风险、高收益。

总结与延伸

核心要点

  1. 问题本质:DPI 和分辨率不成正比,导致 VirtualDisplay 缩放逻辑错误

  2. 触发条件:需要特定的 Android 版本、分辨率、DPI、系统类型、缩放比例同时满足

  3. 解决方案:DPI 按照分辨率缩放比例同步缩放,确保"每像素密度"一致

  4. 影响范围:只改善,不破坏,对所有设备都是正向优化

技术启示

  1. 隐性 bug 难以发现:代码可能一直有问题,但只在特定条件下暴露

  2. 设备碎片化:Android 设备差异巨大,需要在不同设备上充分测试

  3. 原生系统更严格:Pixel 等原生设备会暴露更多兼容性问题,是最佳测试设备

  4. 查看官方文档:VirtualDisplay 的官方文档建议 DPI 应该根据分辨率调整

类似问题排查思路

遇到 VirtualDisplay/MediaProjection 相关问题时:

  1. 对比参数:检查分辨率、DPI、缩放比例是否匹配

  2. 查看日志:记录详细的创建参数,方便对比

  3. 计算密度:DPI / 分辨率 应该在 VirtualDisplay 和物理屏幕上保持一致

  4. 测试边界条件:在高分辨率、高 DPI、原生系统设备上测试

  5. 参考官方示例:Google 官方的 Grafika 项目是最佳参考

参考资料

相关推荐
a31582380613 小时前
Android 大图显示策略优化显示(一)
android·算法·图片加载·大图片
tangweiguo0305198713 小时前
从零开始:在 Windows 上使用命令行编译 Android .so 动态库(NDK + CMake + Ninja)
android
阿波罗尼亚13 小时前
Tcp SSE Utils
android·java·tcp/ip
知行合一。。。17 小时前
Python--03--函数入门
android·数据库·python
大、男人18 小时前
python之contextmanager
android·python·adb
不法20 小时前
java查看安卓证书信息
android
儿歌八万首20 小时前
Jetpack Compose 动画实战:让你的 UI 动起来
android·kotlin·动画·compose
千里马学框架21 小时前
如何改进车载三分屏SplitScreen启动交互方式?
android·智能手机·分屏·aaos·安卓framework开发·车载开发·3分屏
REDcker1 天前
Android WebView 版本升级方案详解
android·音视频·实时音视频·webview·js·编解码