为什么 MIUI 截图越看越糊?一次从相册文件到 DEX 字节码的完整溯源

为什么 MIUI 截图越看越糊?一次从相册文件到 DEX 字节码的完整溯源

Oct 02 ·
17 Min Read

为什么 MIUI 截图越看越糊?一次从相册文件到 DEX 字节码的完整溯源

设备环境:POCO 22041216G · Android 13 · MIUI V14.0.6.0.TLOEUXM (V140) · 1080×2460 @ 440dpi 工具链:Termux + Node.js + root(Magisk) + 手写 DEX 解析 结论先行:MIUI 把截图存成了有损 JPEG,而截图内容恰恰是 JPEG 最不擅长的「锐利文字 + 纯色块」。分辨率没降,降的是高频细节。


0. 一个日常抱怨引发的调查

“我手机截图怎么总是被压缩的?”

这句话里其实藏着一个错误的前提。第一反应大家都以为”压缩 = 分辨率变小了”。但事实是:

  • 你的屏幕是 1080×2460
  • 系统截图出来的还是 1080×2460

分辨率根本就没降。

那”糊”从哪来?这篇文章就带你从相册里的一个 .jpg 文件出发,一路挖到系统 APK 的 DEX 字节码,把这条链路彻底讲清楚。


1. 第一步:先看事实——截图文件到底长什么样

调查任何技术问题,第一步永远是观察现象,不要先下结论。

我们进入截图目录,统计真实数据:

Terminal window
$ cd ~/storage/shared/DCIM/Screenshots
# 统计文件格式
$ ls *.jpg *.png | sed 's/.*\.//' | sort | uniq -c
170 jpg
43 png
# 单独看「编辑过」的文件(文件名带 -edit)
$ ls *-edit.* | wc -l
74
$ ls *-edit.* | sed 's/.*\.//' | sort | uniq -c
74 jpg
0 png

第一个关键事实浮出水面:

74 张编辑过的截图,100% 全是 JPEG,没有一张 PNG。

这不是巧合。它强烈暗示:MIUI 在”保存/编辑”这条链路上,写死了使用 JPEG。

再深入看单个文件的真实属性。我用 Node.js 手写了一个图片头解析器,直接读文件字节里的分辨率:

// 读取 JPEG 的 SOF 段 / PNG 的 IHDR 段,拿到真实宽高
function probe(p) {
const b = fs.readFileSync(p);
if (b[0] === 0x89 && b[1] === 0x50) { // PNG magic
return { type: 'PNG', w: b.readUInt32BE(16), h: b.readUInt32BE(20) };
}
if (b[0] === 0xFF && b[1] === 0xD8) { // JPEG magic
// 遍历 segment,找 SOF0/SOF2 段
...
}
}

对几个代表性文件取样,结果如下:

文件分辨率格式大小
Screenshot_...bili.jpg(原始截图)1080×2460JPEG-baseline794 KB
Screenshot_...globallauncher.jpg(老截图)1080×2460JPEG-baseline1384 KB
Screenshot_...aether-edit.jpg(编辑过)1080×838JPEG-baseline216 KB
Screenshot_...chrome.png1079×1578PNG459 KB

第二个关键事实:

满屏截图依然是完整的 1080×2460。所谓”被压缩”,不是分辨率缩水。

那到底缩了什么?继续。


2. 第二步:做一个对照实验——用 screencap 直出 PNG

既然怀疑格式有问题,那就做个对照:绕过 MIUI 的截图 UI,直接用底层命令截图。

Android 系统自带的 screencap 命令,默认输出无损 PNG:

Terminal window
$ su -c 'screencap -p /sdcard/test_screencap.png'
$ node probe.js ~/storage/shared/test_screencap.png
1080x2460 PNG 545 KB

第三个关键事实:

同一块屏幕,screencap 输出的是 1080×2460 无损 PNG,仅 545 KB。

这证明了两件事:

  1. 屏幕完全有能力输出无损、满分辨率的图;
  2. 问题不在硬件,而在 MIUI 截图这条软件链路的格式选择上。

而且注意——545 KB 的 PNG 比 794 KB 的 JPEG 还小! 所谓”JPEG 更省空间”在这个场景根本不成立。因为截图是”大片纯色 + 锐利边缘”,PNG 的无损压缩对这种内容效率极高。

到这里,现象清楚了:

MIUI 不是”压小了”,而是”存糊了”——它选了 JPEG。


3. 第三步:为什么 JPEG 对截图是灾难?(编码原理)

要理解”糊”,必须理解 JPEG 是怎么工作的。

3.1 JPEG 的设计假设:为照片而生

JPEG 的核心假设来自人眼生理特性:

  • 人眼对 亮度 敏感,对 色度 迟钝;
  • 人眼对 大面积平滑渐变 容忍,对 高频细节 不敏感。

于是 JPEG 干三件事:

  1. 色度下采样(Chroma Subsampling) 把颜色信息降到亮度的 1/2 甚至 1/4(常见 4:2:0)。颜色分辨率直接砍半。

  2. DCT 变换(离散余弦变换) 把图像按 8×8 像素分块,从”空间域”变换到”频率域”。

  3. 量化(Quantization) 高频分量(锐利边缘)的系数被大量丢弃——这部分不可逆。

3.2 为什么这对照片没问题、对截图是致命伤

内容类型主要成分JPEG 表现
照片平滑渐变(天空、皮肤、虚化)高频本就少,丢一点看不出来
截图纯色背景 + 锐利文字/线条/UI 边缘全是高频!

截图的内容构成,恰恰精准命中了 JPEG 最不擅长的部分。量化一上,问题立刻显现:

  • 振铃效应(Ringing):文字边缘出现幽灵般的光晕;
  • 块效应(Blockiness):8×8 方块痕迹,纯色区出现格子;
  • 颜色渗色:细彩色元素糊成一团;
  • 文字发虚:笔画交叉处最先崩坏。

3.3 高 PPI 是”放大器”

为什么以前没人抱怨、现在人人吐槽?因为屏幕变了。

你的 POCO 是 1080×2460,约 6.4 英寸,折算约 440 dpi(PPI ≈ 400+)。

在这个密度下:

  • 人眼能分辨极细的边缘;
  • JPEG 的振铃/块效应肉眼清晰可见;
  • 小字、细笔画首当其冲。

低 PPI 屏本身就把 JPEG 的缺陷”糊”掉了,高 PPI 屏则把它们原形毕露。

一句话总结这一步:

分辨率没降,降的是”高频细节”。而截图恰恰是靠高频细节吃饭的。


4. 第四步:进入系统,找到”凶手”

现象和原理都清楚了,现在去系统里找到底是哪个组件、哪行代码决定的。

4.1 定位截图组件

Terminal window
# 看截图手势绑定到哪个动作
$ settings list system | grep gesture
three_gesture_down=screen_shot # 三指下滑 = screen_shot
# 找截图应用
$ pm list packages | grep -i screenshot
package:com.miui.screenshot
# 找 APK
$ ls /product/app/MIUIScreenshot/
MIUIScreenshot.apk
MIUIScreenshot.odex
MIUIScreenshot.vdex

截图链路已经浮现:

三指下滑手势
│ (system setting: three_gesture_down = screen_shot)
▼
触发 screen_shot 动作
▼
com.miui.screenshot / ScreenShotService
▼
保存位图 → JPEG

4.2 提取 APK 并解包

Terminal window
# 用 root 把 APK 拷出来
$ su -c 'cp /product/app/MIUIScreenshot/MIUIScreenshot.apk /sdcard/'
# APK 里有 2 个 dex
$ unzip -l MIUIScreenshot.apk | grep dex
5798308 classes.dex
791476 classes2.dex

4.3 在 DEX 里”嗅探”关键字符串

DEX 是 Android 的字节码容器,里面有一张字符串表。我们先不改代码,只搜索关键词——这一步成本最低,收益最高。

用 Node.js 直接解析 DEX 头部,读出字符串表:

// DEX 头部固定偏移:
// 0x38: string_ids_size 0x3c: string_ids_off
// 0x40: type_ids_size 0x44: type_ids_off
// 0x58: method_ids_size 0x5c: method_ids_off
const stringIdsSize = buf.readUInt32LE(0x38);
const stringIdsOff = buf.readUInt32LE(0x3c);
// 逐个读取 MUTF-8 字符串
for (let i = 0; i < stringIdsSize; i++) {
const off = buf.readUInt32LE(stringIdsOff + i * 4);
const [len, p] = uleb128(buf, off);
... // 读取 len 个字节并解码
}

结果,classes.dex 的 37,104 个字符串里,命中了这些关键项:

"JPEG"
"image/jpeg"
"image/png"
".jpg"
".png"
"quality"
"CompressFormat"
"Landroid/graphics/Bitmap$CompressFormat;" ← 关键!
"Landroid/view/SurfaceControl$ScreenshotHardwareBuffer;"
"saveBitmapToUri finish"
"ImageSaveTask"
"/system/media/audio/ui/screenshot.ogg"

第四个关键事实:

MIUIScreenshot 明确引用了 Bitmap$CompressFormat 类型,并且字符串表里 .jpg、JPEG、image/jpeg、quality 一应俱全。

4.4 确认只有一个”压缩”入口

DEX 的方法表里,我们搜索所有 Bitmap.compress 的引用:

// method_id_item: class_idx(ushort) proto_idx(ushort) name_idx(uint)
for (let i = 0; i < methodIdsSize; i++) {
const off = methodIdsOff + i * 4;
const cn = type(buf.readUInt16LE(off)); // 类名
const mn = str(buf.readUInt32LE(off + 4)); // 方法名
if (cn === 'Landroid/graphics/Bitmap;' && mn === 'compress') {
compressIdx = i; // ← 唯一的调用目标
}
}

结果:

整个 APK 里只有一个 Bitmap.compress 方法引用。

这非常关键——说明截图保存只有一条路径,没有”PNG 分支、JPEG 分支”的切换逻辑。保存 = compress(JPEG) = 写死。

4.5 这段代码几乎必然长这样

综合以上线索(Bitmap$CompressFormat + JPEG + quality + 单一 compress 调用),对应的 Java 源码几乎可以断定是:

// MIUI 截图保存核心逻辑(等价重构)
Bitmap bmp = surfaceControlScreenshot(); // 拿到截屏位图
FileOutputStream fos = new FileOutputStream(file);
bmp.compress(
Bitmap.CompressFormat.JPEG, // ← 注意: 元凶:写死 JPEG
80, // ← quality 常量(80~90 之间,大概率 80/85)
fos
);
fos.close();

那个 CompressFormat.JPEG,就是你嫌糊的根源。

注意: 诚实标注:quality 的确切数值需要反编译成 Java 才能 100% 确认。DEX 字符串表只能证明”存在 quality 参数”,无法直接读出常量值(那是 DEX 字节码里的 const 指令)。但”使用 JPEG”这一结论是确证的。


5. 第五步:为什么”编辑后更糊”——二次编码

前面统计里最刺眼的一条:

74 张 -edit 文件 → 100% 是 JPEG

原因是另一个独立但叠加的问题:JPEG 的二次压缩。

原始截图 (JPEG, quality≈80,已经损失一次)
│
▼ 相册里打开、编辑、保存
解码 → 重新编码 (JPEG 再压一次)
│
▼
二次损失!质量进一步下降

JPEG 是有损的,每次”打开→编辑→保存”都会重新量化一遍,损伤不断累积。

这就是为什么你会觉得:

“编辑一次糊一点,再编辑更糊。”

对比之下,PNG 是无损的,编辑多少次都不掉画质。


6. 完整因果链

把所有环节串起来:

flowchart TD
START(["你感受到的「糊」"])
START --> CAUSE1
START --> CAUSE2
CAUSE1["主因 · 格式选错<br/>保存为 JPEG 而非 PNG<br/>com.miui.screenshot"]
CAUSE2["叠加 · 二次压缩<br/>编辑 → 保存 → 再编码<br/>相册编辑链路"]
CAUSE1 --> WHY
WHY["JPEG 的量化<br/>正对「截图内容」的命门<br/>锐利文字 · 细线条 · 纯色块"]
WHY --> ARTIFACT["振铃效应 / 块效应 / 颜色渗色"]
ARTIFACT --> SCREEN
SCREEN["高 PPI 屏幕<br/>1080×2460 @ 440dpi<br/>把人眼分辨力推到极限"]
SCREEN --> END(["结论:分辨率没降<br/>降的是「高频细节」"])
classDef cause fill:#ffe0e0,stroke:#d33,stroke-width:2px,color:#111
classDef effect fill:#fff3cd,stroke:#e0a800,stroke-width:2px,color:#111
classDef result fill:#d4edda,stroke:#28a745,stroke-width:2px,color:#111
classDef term fill:#e7e0ff,stroke:#6f42c1,stroke-width:2px,color:#111
class CAUSE1,CAUSE2 cause
class WHY,ARTIFACT effect
class SCREEN effect
class START,END term

用一句话复述这条链

MIUI 把截图存成有损 JPEG(而非无损 PNG),而截图的内容恰恰是 JPEG 最不擅长的「锐利文字 + 纯色块」,再叠加编辑时的二次压缩,在 440dpi 高分屏上就表现为「越来越糊」。


7. MIUI 到底是”缺德”还是”技术权衡”?

网络上普遍骂厂商”缺德压缩”,但客观拆解,这是多个工程决策叠加的结果:

原因说明
存储指标JPEG 数字上更”小”,迎合”截图占空间少”的普遍认知
编辑链路统一MIUI 截图后立即进入可编辑状态,编辑管线为照片设计,默认走 JPEG,截图被”顺带”拖下水
历史惯性该逻辑年代久远,早期低分屏、存储紧张时 JPEG 缺陷不明显,代码一直没改
可能的内容保护部分受保护画面走 JPEG 能规避某些检测(此为推测)

更准确的评价是:

MIUI 为了”文件小 + 编辑链路统一”,把截图塞进了为照片优化的 JPEG 通道,牺牲了截图最需要的高频清晰度。说是”恶意”过重,但**“对截图场景选错格式”是实打实的工程决策问题**。


8. 那么,怎么改?(方案汇总)

原理讲完,落到可操作层面。按推荐度排序:

方案 A:LSPosed Hook(保留手势 + 出 PNG)

你的设备已装 LSPosed(zygisk_lsposed + riru_lsposed)。

原理:写一个 Xposed 模块,Hook com.miui.screenshot 进程里的 Bitmap.compress(CompressFormat.JPEG, ...),把 JPEG 参数替换成 PNG。

  • 三指下滑手势照常用
  • 输出无损 PNG
  • 可逆、不动系统文件、不怕 OTA
// Hook 伪代码
XposedHelpers.findAndHookMethod(Bitmap.class, "compress",
Bitmap.CompressFormat.class, int.class, OutputStream.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (param.args[0] == Bitmap.CompressFormat.JPEG) {
param.args[0] = Bitmap.CompressFormat.PNG; // 偷梁换柱
}
}
});

作用域需勾选 com.miui.screenshot。注意:此 Hook 会影响该进程内所有 JPEG 压缩行为,需实测副作用。

方案 B:screencap + 自动化(最可靠,立刻能用)

绕过 MIUI 截图 UI,直接调底层命令:

Terminal window
su -c 'screencap -p /sdcard/Pictures/Screenshots/shot_$(date +%s).png'

配合 MacroDroid / Tasker,绑定到某个触发器(音量键组合、悬浮球、快捷方式),每次截图都是无损 PNG。

  • 100% 无损、不碰系统、不怕 OTA
  • 注意需要另设触发方式(三指手势无法直接重映射到命令)

方案 C:Magisk 模块替换 APK(不推荐)

反编译 MIUIScreenshot.apk → 改 JPEG 为 PNG → 重打包 → Magisk 挂载覆盖。

  • 注意 APK 签名 / odex 校验问题,易导致截图功能崩溃
  • 注意 OTA 后失效
  • 仅适合熟悉 apktool 的折腾党

9. 复盘:这次调查做对了什么

回顾整条路径,方法论值得记录:

  1. 先观察,不预设结论 第一反应是”分辨率被压了”,但实测发现分辨率没动——推翻了错误前提。

  2. 做对照实验 用 screencap 直出 PNG 作为对照组,一举锁定”格式问题”而非”硬件问题”。

  3. 从低成本信号入手 先在 DEX 字符串表里搜索关键词,而不是一上来就反编译。几行代码就拿到了关键证据。

  4. 层层递进,确证与推断分离 “使用 JPEG”是确证的;“quality=80”是合理推断,明确标注区别。

  5. 从现象回到原理 不仅回答”是什么”,还回答了”为什么”(JPEG 编码原理 + 高 PPI + 二次压缩)。


10. 待办 / 未竟事项

  • 用 MT 管理器反编译 classes.dex,确认 quality 的确切常量值
  • 实测 LSPosed Hook 的副作用(该进程内是否有其他必需 JPEG 的场景)
  • 验证相册编辑链路的编码格式,是否能一并 Hook 成 PNG
  • 若 JEX 方案可行,整理成可发布的 LSPosed 模块

附录:本文用到的关键命令

Terminal window
# 查看设备信息
getprop ro.product.model && getprop ro.build.version.release
# 统计截图格式
cd ~/storage/shared/DCIM/Screenshots
ls *-edit.* | sed 's/.*\.//' | sort | uniq -c
# 用 screencap 直出无损 PNG
su -c 'screencap -p /sdcard/test.png'
# 提取系统 APK
su -c 'cp /product/app/MIUIScreenshot/MIUIScreenshot.apk /sdcard/'
# 解包 dex
unzip MIUIScreenshot.apk "classes*.dex"
# 搜索 dex 关键字符串(Node.js 见正文 4.3 节)
node dexstr.js

本文基于真实设备(POCO 22041216G / MIUI V14.0.6.0)的实测数据分析撰写。 所有”确证”结论均有原始数据支撑;推断性内容已明确标注。

Last edited Oct 10