直接回答
重复推理后内存曲线升高是调查信号,不是泄漏结论。曲线可能包含正常缓存、延迟垃圾回收、保留的对话历史、原生张量、图像缓冲区或仍由活动任务拥有的对象。有效的泄漏测试要重复同一个有限工作负载,在相同生命周期检查点采样内存,由对应所有者关闭引擎和对话,给应用短暂稳定时间,再比较仍保留的实例,而不是只看一个峰值。Android Studio heap dump 可以检查托管对象;托管堆视图不能说明所有原生分配。 先定义一个可重复任务:同一个模型制品、提示结构、输出限制、图像尺寸或音频时长、后端请求和轮次数。记录模型加载前基线、加载后、任务峰值、输出释放后、对话关闭后和引擎关闭后。如果只有进程重启后内存才下降,这提示所有权或分配器保留问题,但还不能指出准确泄漏对象。若工具能提供指标,应分别记录 Java 堆、进程 RSS、原生堆、GPU 分配和文件缓存。 调查应检查对象数量是否随周期增长、Activity 或 Context 是否仍可达、观察者或协程是否持有 ViewModel、流式订阅是否在取消后存活、Camera ImageProxy 是否关闭,以及原生引擎是否恰好关闭一次。预热后和多次相同周期后各抓取堆快照,比较被保留对象类型和引用路径,再只改一个清理边界重测。一次分配后稳定可能是缓存;任务对象无限保留更像生命周期问题。没有设备、构建、工具、指标与原始运行序列,就不要发布内存增长数字。 测试生命周期,而不只是推理调用:旋转设备、离开再返回页面、流式期间取消、触发异常、后台再前台并重复任务。引擎创建和关闭应有明确所有者;初始化失败也必须进入清理路径。防止旧结果持有整个屏幕树。摄像头到模型链路中,即使推理抛异常,也要释放图像帧;音频路径则在收尾后释放缓冲与流。 报告应把观察与结论分开,例如“关闭后托管堆回到接近实测基线”“当前工具无法观测原生内存”或“Activity 通过该引用路径仍可达”。只有同一重复测试不再显示该对象路径,任务仍通过验收,才说泄漏已修复。这是工程验证指南,不是 Cove 基准测试结果。 流程:固定任务、制品、运行时、设备和周期;在基线、加载、峰值、取消后和关闭后采集匹配指标;检查协程、监听器、Activity/Context、媒体缓冲和原生资源所有者;每次只改一个清理动作并重复取消、异常、旋转和重进;最后明确证据不能解释的原生、GPU 或分配器行为。最终结果应是一份有界、可复现的诊断,而非断言所有本地 AI 都会泄漏。
每次重复翻译后内存都似乎增加
测试人员反复运行相同的短文本翻译,并在页面间导航,观察到内存持续上升。单张截图无法证明是模型缓存稳定增长、协程遗留、对话被保留,还是原生分配正常行为。
决策
用命名检查点重复同一工作负载,比较关闭后的对象引用,每次只改变一个生命周期所有者。
预期结果
团队得到可回归测试的解释,而不会把一个峰值误认为确定泄漏。
需要收集的证据
只有将以下证据与同一模型文件和测试运行关联起来,结论才可复核:
- 重复会话的固定工作负载
- 关闭后的对象引用路径
- 堆与进程指标的分层记录
设备兼容结论须绑定具体设备、系统、模型、运行时、后端和工作负载,不能靠参数量推测。
实施流程
- 固定重复工作负载. 保持应用构建、模型文件、运行时、后端请求、提示、输出限制、媒体尺寸和周期数一致。将预热与测量周期分开,不混入其他屏幕或任务。
- 采集可比较的生命周期检查点. 记录基线、模型加载后、任务峰值、输出释放后、对话关闭后和引擎关闭后。区分堆与进程指标,并记录是否等待了垃圾回收或稳定时间。
- 追踪仍然可达的对象. 比较堆快照中关闭后仍可达的 Activity、Context、ViewModel、监听器、订阅者、图像、音频和会话对象。找出每个对象由谁持有,以及该引用是否应该跨屏幕存在。
- 修复一个清理路径后再做压力测试. 只改一个资源所有者或关闭路径,然后用相同周期测试取消、异常、旋转、导航和重进。确认任务结果仍通过验收,再说明怀疑的保留问题已解决。
让每个阶段的状态都可观察。失败信息应指出阶段、模型、运行时和恢复动作,同时避免记录用户私密内容。
验收标准
- 证据记录模型、应用构建、运行时、设备、工作负载和准确内存指标。
- 泄漏判断具有跨相同周期增长的保留对象或分配路径,并能关联到具体所有者。
- 清理改动后重复任务仍成功,先前观察到的引用路径不再被保留。
决策对照表
| 维度 | 选择或证据 | 解读方式 |
|---|---|---|
| 一次性预热后上升 | 可能是缓存或初始化成本 | 需比较重复检查点;单个高值不是泄漏诊断。 |
| 关闭后仍保留托管对象 | 检查堆引用路径 | 堆快照有助于定位 Java/Kotlin 对象可达性和生命周期所有者。 |
| 原生或 GPU 内存不下降 | 进程或运行时分配调查 | 仅靠托管堆工具无法解释全部原生与驱动分配。 |
需要避免的失败模式
- 把单次峰值当泄漏结论
- 将所有内存指标混为一谈
- 只验证推理成功而不验证关闭
还要避免静默回退、版本未固定、缺少取消处理,以及把不同配置的结果混在一起下结论。失败运行也应与成功记录一并保存。
最小复现记录
| 层级 | 记录内容 |
|---|---|
| 设备 | 厂商、型号、芯片、内存档位、操作系统版本 |
| 软件 | 应用版本和 Git 提交号 |
| 模型 | 系列、变体、版本、格式、文件长度、摘要、量化方式 |
| 运行时 | 名称、版本、请求后端及实际后端证据 |
| 工作负载 | 样例版本、输入摘要、提示词摘要、输出策略 |
| 结果 | 完成、失败、取消、回退及保护隐私的诊断信息 |
发布检查表
- ☐ 主要搜索问题已回答,且没有未经支持的数字。
- ☐ 所有模型文件和运行时版本均已固定。
- ☐ 代表性任务和失败处理策略均有说明。
- ☐ 生命周期、取消、清理和回退均已测试。
- ☐ 用户内容与网络边界均有说明。
- ☐ 结论只适用于被记录的配置。
- ☐ 新增测试结果时链接到原始方法或证据。
来源与相关证据
- Android Memory Profiler
- Android Developers — 捕获堆转储
- Android Developers — 内存分析器
- Android Developers — Activity 生命周期
中文部署与排错专题见。奇连 AI 端侧专题。
常见问题
一次请求后内存不降就证明泄漏吗?
不能。缓存和分配器可能保留内存以便重用,垃圾回收也可能延迟。应重复固定任务,比较关闭后的保留对象,并将进程内存与托管堆区分。
为什么要在多次会话后抓取堆快照?
Activity、监听器、订阅者或对话对象可能只有在多个生命周期周期后才暴露问题。对一致检查点比较对象数量和引用路径,比单份快照更有信息量。
堆快照能确认原生引擎泄漏吗?
不能单独确认。堆转储检查托管对象;原生引擎、GPU 和分配器行为可能需要其他进程或运行时诊断。如果无法测量,应明确保留为未知。