为什么 MIUI 截图越看越糊?一次从相册文件到 DEX 字节码的完整溯源
为什么 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. 第一步:先看事实——截图文件到底长什么样
调查任何技术问题,第一步永远是观察现象,不要先下结论。
我们进入截图目录,统计真实数据:
$ 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×2460 | JPEG-baseline | 794 KB |
Screenshot_...globallauncher.jpg(老截图) | 1080×2460 | JPEG-baseline | 1384 KB |
Screenshot_...aether-edit.jpg(编辑过) | 1080×838 | JPEG-baseline | 216 KB |
Screenshot_...chrome.png | 1079×1578 | PNG | 459 KB |
第二个关键事实:
满屏截图依然是完整的 1080×2460。所谓”被压缩”,不是分辨率缩水。
那到底缩了什么?继续。
2. 第二步:做一个对照实验——用 screencap 直出 PNG
既然怀疑格式有问题,那就做个对照:绕过 MIUI 的截图 UI,直接用底层命令截图。
Android 系统自带的 screencap 命令,默认输出无损 PNG:
$ su -c 'screencap -p /sdcard/test_screencap.png'$ node probe.js ~/storage/shared/test_screencap.png1080x2460 PNG 545 KB第三个关键事实:
同一块屏幕,
screencap输出的是 1080×2460 无损 PNG,仅 545 KB。
这证明了两件事:
- 屏幕完全有能力输出无损、满分辨率的图;
- 问题不在硬件,而在 MIUI 截图这条软件链路的格式选择上。
而且注意——545 KB 的 PNG 比 794 KB 的 JPEG 还小! 所谓”JPEG 更省空间”在这个场景根本不成立。因为截图是”大片纯色 + 锐利边缘”,PNG 的无损压缩对这种内容效率极高。
到这里,现象清楚了:
MIUI 不是”压小了”,而是”存糊了”——它选了 JPEG。
3. 第三步:为什么 JPEG 对截图是灾难?(编码原理)
要理解”糊”,必须理解 JPEG 是怎么工作的。
3.1 JPEG 的设计假设:为照片而生
JPEG 的核心假设来自人眼生理特性:
- 人眼对 亮度 敏感,对 色度 迟钝;
- 人眼对 大面积平滑渐变 容忍,对 高频细节 不敏感。
于是 JPEG 干三件事:
-
色度下采样(Chroma Subsampling) 把颜色信息降到亮度的 1/2 甚至 1/4(常见 4:2:0)。颜色分辨率直接砍半。
-
DCT 变换(离散余弦变换) 把图像按 8×8 像素分块,从”空间域”变换到”频率域”。
-
量化(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 定位截图组件
# 看截图手势绑定到哪个动作$ settings list system | grep gesturethree_gesture_down=screen_shot # 三指下滑 = screen_shot
# 找截图应用$ pm list packages | grep -i screenshotpackage:com.miui.screenshot
# 找 APK$ ls /product/app/MIUIScreenshot/MIUIScreenshot.apkMIUIScreenshot.odexMIUIScreenshot.vdex截图链路已经浮现:
三指下滑手势 │ (system setting: three_gesture_down = screen_shot) ▼触发 screen_shot 动作 ▼com.miui.screenshot / ScreenShotService ▼保存位图 → JPEG4.2 提取 APK 并解包
# 用 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.dex4.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_offconst 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,直接调底层命令:
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. 复盘:这次调查做对了什么
回顾整条路径,方法论值得记录:
-
先观察,不预设结论 第一反应是”分辨率被压了”,但实测发现分辨率没动——推翻了错误前提。
-
做对照实验 用
screencap直出 PNG 作为对照组,一举锁定”格式问题”而非”硬件问题”。 -
从低成本信号入手 先在 DEX 字符串表里搜索关键词,而不是一上来就反编译。几行代码就拿到了关键证据。
-
层层递进,确证与推断分离 “使用 JPEG”是确证的;“quality=80”是合理推断,明确标注区别。
-
从现象回到原理 不仅回答”是什么”,还回答了”为什么”(JPEG 编码原理 + 高 PPI + 二次压缩)。
10. 待办 / 未竟事项
- 用 MT 管理器反编译
classes.dex,确认quality的确切常量值 - 实测 LSPosed Hook 的副作用(该进程内是否有其他必需 JPEG 的场景)
- 验证相册编辑链路的编码格式,是否能一并 Hook 成 PNG
- 若 JEX 方案可行,整理成可发布的 LSPosed 模块
附录:本文用到的关键命令
# 查看设备信息getprop ro.product.model && getprop ro.build.version.release
# 统计截图格式cd ~/storage/shared/DCIM/Screenshotsls *-edit.* | sed 's/.*\.//' | sort | uniq -c
# 用 screencap 直出无损 PNGsu -c 'screencap -p /sdcard/test.png'
# 提取系统 APKsu -c 'cp /product/app/MIUIScreenshot/MIUIScreenshot.apk /sdcard/'
# 解包 dexunzip MIUIScreenshot.apk "classes*.dex"
# 搜索 dex 关键字符串(Node.js 见正文 4.3 节)node dexstr.js本文基于真实设备(POCO 22041216G / MIUI V14.0.6.0)的实测数据分析撰写。 所有”确证”结论均有原始数据支撑;推断性内容已明确标注。