直接回答
文件描述符泄漏是资源生命周期问题,不等于 Java heap 泄漏或模型内存增长。Android 应用可能为模型制品、用户选取的内容、媒体流、数据库、socket、管道或原生运行时文件打开描述符。任务虽然已经在界面显示完成,其中某个句柄仍可能保持打开。重复执行后,即使 heap 稳定,进程的打开描述符清单也可能增长。应通过基线、固定任务及每个成功/失败/取消/清理边界的观察来诊断。 先画所有权图。对于每个返回 Closeable 或由描述符支持的对象,记录谁获得所有权、谁负责关闭,以及包装对象是否同时拥有底层描述符。Android ParcelFileDescriptor 文档说明该对象可关闭,也提供自动关闭流的包装类;使用包装器时要确认关闭外层是否会关闭底层文件描述符,避免既泄漏又误以为可以重复关闭。导入内容时,Provider URI 和文件流可能有不同生命周期。原生推理运行时也可能自行打开文件,要记录这一所有者边界,不能假设应用代码控制了全部句柄。 使用可复现顺序:先采集描述符基线,打开模型或导入输入,运行预处理与推理,再取消或注入异常,关闭所有者流/会话,最后再次测量。使用同一个夹具多轮运行,按阶段记录数量或可识别的句柄类别。不要硬编码所谓通用最大描述符数;进程限制和运行时行为因配置而异。计数短暂上升后回落可能是预期临时句柄;若清理后每轮持续增加,则应检查所有权路径。任何结论都应在实际构建和设备上复现。 StrictMode 可在调试/测试策略中帮助发现 Closeable 对象尚未关闭就被回收的情况。它是开发期绊线,不是完整描述符清单,也不能证明覆盖所有 native 句柄。应与定向描述符快照及注入失败路径配合。对资源应使用确定性结构,例如适用时的 try-with-resources 或 Kotlin use;不要等垃圾回收来关闭打开文件。每个获取动作之后都测试提前返回和异常,仅测成功路径会漏掉缺少 close 的分支。 描述符证据要保护隐私。记录任务阶段、句柄类别、数量变化、请求 ID 和清理结果即可;避免记录私有路径、导入文档名、提示词、转写或音频内容。若句柄指向用户选择的 URI,只保留脱敏 Provider 类别或测试夹具 ID。Cove 生命周期事实卡描述模型加载状态与清理行为,Android 内存工具可以观察进程行为;两者都不能证明某个发布版一定存在描述符泄漏。本文是测量方法,不声明 Cove 当前故障,也不给固定安全阈值。 测试结果要同时回答“哪一步打开了资源”和“哪一步释放了资源”。同一文件可能被一个 stream wrapper 和一个 native handle 分别持有;仅关闭其中一个并不能证明全部结束。错误处理分支应确保 close 发生在所有可达出口,而且关错顺序不会令另一个读取方继续使用失效句柄。若库内部所有权无法观察,应标注未知并通过其官方契约验证,不要猜测。 将描述符增长与内存指标分开。一个进程可以保持相近 heap,却逐渐耗尽可用文件句柄;反过来,描述符数量稳定也不能证明内存不会泄漏。重复测试需要固定输入、生命周期和重试策略,并保留所有失败。任何公开结论都限定到被测设备、系统、应用和运行时;不能从一次多次运行直接推导系统级上限或每个 Android 版本的表现。
多次导入后,打开模型或媒体文件开始失败
Android 应用可以导入文档或音频、运行本地推理并回到空闲页面。反复成功与取消后,后续文件打开失败;heap 没有明显增长,团队也没有描述符生命周期记录。
决策
在归咎于内存压力或模型运行时之前,用固定任务跨成功、异常和取消追踪句柄所有权与数量。
预期结果
团队能定位缺少确定性 close 的资源路径,或记录在被测配置下未观察到清理后持续增长。
需要收集的证据
只有将以下证据与同一模型文件和测试运行关联起来,结论才可复核:
- COVE-LIFECYCLE-01:项目事实描述 ModelManager 的加载状态和清理状态,不代表已经发现文件句柄泄漏。
- MEMORY-03:Android Studio Memory Profiler 与系统诊断可观察指定应用构建和设备的实际内存行为。
设备兼容结论须绑定具体设备、系统、模型、运行时、后端和工作负载,不能靠参数量推测。
实施流程
- 盘点每个创建描述符的阶段. 列出模型文件、Content Provider 流、相机/音频源、临时文件、数据库游标、socket、管道及 native 运行时句柄。注明每项由应用、库还是平台持有。
- 写清所有权转移和关闭契约. 逐项记录创建者、当前所有者、转移时点、包装关系、关闭动作及错误路径。确认关闭包装对象是否也会关闭 ParcelFileDescriptor,不能依赖终结器。
- 采集基线并重复固定任务. 保存初始描述符数量或类别快照,执行同一导入/推理夹具,并在各阶段和清理后采样。保持构建与输入不变,重复成功、失败、取消和重试。
- 在获取之后立即注入退出. 在每次打开之后注入解析失败、模型初始化错误、取消、导航和存储异常。断言资源恰好关闭一次,下一任务仍能打开输入。
- 使用诊断检查并脱敏证据. 只在受控调试/测试策略中开启 StrictMode 未关闭 Closeable 检查,并与描述符快照对照,因为它不一定覆盖 native 句柄。仅保存阶段与计数,不记个人路径和文件内容。
让每个阶段的状态都可观察。失败信息应指出阶段、模型、运行时和恢复动作,同时避免记录用户私密内容。
验收标准
- 每种创建描述符的资源都明确命名所有者、转移规则及成功/失败时的确定性关闭路径。
- 重复固定任务覆盖取消和异常路径,并记录清理前后的描述符观察。
- 只有可复现的清理后增长才作为泄漏线索,不依赖通用阈值或单独的 heap 曲线。
- 诊断不包含用户内容、私有路径或导入文件名,结论限定到所测构建与设备。
决策对照表
| 维度 | 选择或证据 | 解读方式 |
|---|---|---|
| 托管堆分析 | 检查 Java/Kotlin 对象分配和保留 | 适合排查 heap 泄漏;heap 稳定不能证明文件描述符已关闭。 |
| StrictMode Closeable 检查 | 调试时发现部分对象回收前未 close | 可作 Java 资源提示,但不是 native 描述符完整清单。 |
| 描述符生命周期追踪 | 比较固定阶段前后的进程描述符观察 | 直接针对句柄增长;需要脱敏并记录准确配置。 |
需要避免的失败模式
- 用 Java heap 曲线代替文件描述符数量和关闭时机。
- 等垃圾回收关闭文件,或从一次失败推导通用句柄上限。
还要避免静默回退、版本未固定、缺少取消处理,以及把不同配置的结果混在一起下结论。失败运行也应与成功记录一并保存。
最小复现记录
| 层级 | 记录内容 |
|---|---|
| 设备 | 厂商、型号、芯片、内存档位、操作系统版本 |
| 软件 | 应用版本和 Git 提交号 |
| 模型 | 系列、变体、版本、格式、文件长度、摘要、量化方式 |
| 运行时 | 名称、版本、请求后端及实际后端证据 |
| 工作负载 | 样例版本、输入摘要、提示词摘要、输出策略 |
| 结果 | 完成、失败、取消、回退及保护隐私的诊断信息 |
发布检查表
- ☐ 主要搜索问题已回答,且没有未经支持的数字。
- ☐ 所有模型文件和运行时版本均已固定。
- ☐ 代表性任务和失败处理策略均有说明。
- ☐ 生命周期、取消、清理和回退均已测试。
- ☐ 用户内容与网络边界均有说明。
- ☐ 结论只适用于被记录的配置。
- ☐ 新增测试结果时链接到原始方法或证据。
来源与相关证据
中文部署与排错专题见。奇连 AI 端侧专题。
常见问题
Java heap 不增长是否证明没有文件描述符泄漏?
不能。描述符和 native 句柄是不同资源。应独立记录描述符观察与所有权,不能用托管堆或进程内存指标替代;还要检查各类打开流。
StrictMode 能发现所有 native 文件描述符泄漏吗?
不能。未关闭 Closeable 检查是开发辅助,只覆盖部分对象。应与阶段描述符快照和可用的 native/运行时证据结合;它不保证观察到 native 层每个句柄。
能否等垃圾回收自动关闭导入文件?
不要依赖它。成功、异常、取消和提前返回时都要确定性关闭拥有该资源的流或 ParcelFileDescriptor;终结时间不是资源契约,所有路径均应显式关闭。