1. 背景:性能怪兽与消失的掉帧
在高性能设备(如 Samsung S22,搭载骁龙 8 Gen 1)上,用户对 UI 的流畅度有着极高的预期。然而,在处理包含 4000+ PDF 文件的极端场景时,简单的文件扫描逻辑往往会导致明显的 UI 卡顿甚至 ANR。
经过 Trace 分析,我们发现卡顿并非硬件性能不足,而是典型的主线程负载过重。在 120Hz 刷新率下,每一帧的绘制时间仅为 8.3ms。任何在 UI 链路上执行的磁盘 IO(如 File.exists())或 级别的计算,都是对硬件性能的亵渎。
2. 核心痛点分析
传统的全盘物理扫描方案在现代 Android 开发中面临三大挑战:
-
Google Play 合规性:
MANAGE_EXTERNAL_STORAGE权限审核极其严格,盲目申请会导致应用下架。 - IO 瓶颈:在大规模文件目录下,递归遍历(BFS/DFS)会产生巨大的 IO 消耗和 CPU 峰值。
-
数据转换黑洞:在
Flow的转换操作符(如map)中进行数据加工,会导致 UI 刷新与数据计算在主线程竞争资源。
3. 二级扫描策略:权衡隐私与深度
为了兼顾合规性与功能完整性,我们设计了分层扫描模型:
第一级:基础扫描(Default / Passive)
-
核心源:
MediaStore+SAF (Storage Access Framework)手动导入。 - 原理:利用系统级索引数据库。
- 优点:秒开级别的响应速度,无需特殊权限,符合 Google Play 隐私规范。
- 局限:存在索引延迟,无法触达某些非标准目录。
第二级:深度扫描(Optional / Active)
- 核心源:全盘物理递归遍历(BFS)。
- 开启条件:用户主动在设置中开启并授予“所有文件访问权限”。
-
职责:作为第一级的补充,通过底层的物理扫描补全
MediaStore可能漏掉的文件。
4. 架构实现:异步预处理管道
为了彻底消除 4000 个文件带来的卡顿,我们引入了数据成品化(Data Ready)机制。
逻辑解耦:读写分离
-
写入侧(Write Side):无论是来自
MediaStore还是物理遍历,原始文件数据在进入数据库(Room)前,必须在后台线程(Dispatchers.IO/Default)完成所有重型加工。- 执行
file.exists()校验。 - 计算并格式化
sizeLabel(如 "1.2 MB")和dateLabel。
- 执行
-
读取侧(Read Side):UI 观察的
Flow<List<PdfFile>>仅包含已经格式化好的字符串。
线程调度优化
严禁在 Flow.map 或 Flow.combine 等操作符中执行磁盘 IO。通过 .flowOn(Dispatchers.Default) 将数据加工逻辑与 UI 收集逻辑物理隔离。
5. 极限测试结论
在 4000+ PDF 文件的模拟环境下:
-
重构前:由于在主线程 Flow 转换中执行
emergencyFilter(包含文件 IO),Tab 切换动画出现明显撕裂,掉帧严重。 - 重构后:采用“入库即成品”策略,UI 仅负责渲染预存的字符串。在 S22 上,列表滚动与 Tab 切换完全回归 120Hz 满帧表现。
6. 总结与展望
高性能不是“跑得快”,而是“路不堵”。通过二级扫描策略解决合规与深度问题,通过异步预处理管道解决 UI 性能问题,我们将一个底层的文件操作模块升级为了一个高性能的系统组件。
这种架构设计不仅适用于 PDF 管理,在后续的音频处理(EchoFlow)或 AI 知识库构建中,都可以作为通用的基础设施进行复用。在 Android 碎片化的生态中,这种“分层防御 + 数据成品化”的思路,是实现商用级丝滑体验的唯一路径。