直接回答
文档 URI 是提供方契约,不是文件系统路径。模型运行时需要字节时,先读它实际支持的输入形式:顺序流、完整文件描述符、带偏移和长度的描述符,或可 seek 的路径。Android 的 ContentResolver.openFileDescriptor() 通过 ContentProvider.openFile() 打开内容;只读时提供方可以返回 pipe 或 socket,所以不能假设它支持 seek。它也不能表达某些提供方返回的文件子区间。若 URI 可能代表一个资源切片,openAssetFileDescriptor() 才是可表达范围的接口,因为 AssetFileDescriptor 同时保存 ParcelFileDescriptor、起始偏移和声明长度。无 CancellationSignal 的 openFileDescriptor 是 API 1,带 signal 的重载是 API 19;openAssetFileDescriptor 基础重载是 API 3,取消重载是 API 19。应按应用 minSdk 选重载,不照抄超出支持范围的 API。 不要把描述符长度字段误当作通用文件大小查询。AssetFileDescriptor.getDeclaredLength() 返回提供方声明的值;UNKNOWN_LENGTH 是 -1,表示未声明,不是零字节,也不是允许无限读取。getLength() 在声明长度未知时可能调用 ParcelFileDescriptor.getStatSize() 查询,但仍可能返回 UNKNOWN_LENGTH。显示名称、MIME 或查询得到的 size 是不同观察值,不等同描述符的边界契约。只读 content URI 还可能走 typed-asset 打开路径,因此应验证实际返回的字节,不要从名称推断字节表示。 如果解析器接受顺序流,AssetFileDescriptor.createInputStream() 是读取资源的有界包装流。它会处理 AFD offset 与 extent,不要再手动 skip(startOffset)。API 说明完整文件或子区间会返回相应自动关闭流,且同一资源只能调用一次。应在所有者作用域内消费并确定性关闭。pipe 测试夹具应从所选资源的字节零开始。若提供方流不能暴露准确子区间,从父容器 byte 0 复制无法修复范围;应请求一个从所选字节开始的流,或拒绝输入。 若 native reader 接收文件描述符,只有在准确的运行时 API 文档明示这些参数及语义时,才传 offset 和 length。一个只收裸 FD 的接口不会自动获得 AssetFileDescriptor 的子区间信息。支持范围参数的消费者也许能免复制读取选定切片,但必须让 ParcelFileDescriptor 在 native 消费完成前持续打开,并遵循运行时的资源归属与关闭契约。不能因为 Android 有 FD API,就发明 LiteRT-LM、TFLite 或 Cove 的输入 API。仓库下载器会写自己的临时下载文件,但这不能证明 Cove 已支持模型 URI 导入或把 provider 描述符传给 native 推理。 使用两种独立范围夹具。第一种是可 seek 的 backing file,放置哨兵头部、非零 startOffset、已知长度和不同尾部,验证消费者只拿到该范围。第二种是 stream 或 pipe,其首字节已经是所选资源,offset 为零,并分别测试已知长度和 UNKNOWN_LENGTH。AssetFileDescriptor 契约要求长度未知时 offset 必须为零。负 offset、除 UNKNOWN_LENGTH 外的负 length、非零 offset 搭配未知长度、offset 加 length 溢出,以及已知长度提前 EOF 均是非法输入。合法的 offset-zero UNKNOWN_LENGTH 流可在独立字节上限内读取到 EOF;unknown 不是空文件或无限许可,也不能仅因为长度未知就拒绝有效流或默认复制整个父文件。 若必须检查 seek 能力,优先在所有权明确的 FD 上用不移动位置的 Os.lseek(fd, 0, SEEK_CUR) 查询;Os 包装和 SEEK_CUR 常量为 API 21,pipe 会报告不可 seek。seek 到其他位置会改动共享 open-file-description 游标,因此 native consumer 或 stream 正在读取时不能探测。Android 文档说明 ParcelFileDescriptor.dup(FileDescriptor) 为 API 13、实例 dup() 为 API 14;二者都与原件共享 file position,所以 dup 不是独立探测句柄。若必须移动探测,只能用没有活动 reader 的独立打开句柄,并在交接前恢复保存的位置。不能只为 seek 请求读写权限。 把访问、字节身份、运行时兼容、取消和资源生命周期分开记录。ACTION_OPEN_DOCUMENT 赋予所选 URI 的访问授权;只有以后确实要重开,且返回 flags 允许时才持久化授权。传入 open 操作的 CancellationSignal 可取消提供方打开过程,但不保证中断任意阻塞的 stream read 或 native consumer。复制循环可以在 read 之间响应取消;如果读取仍阻塞或 native 调用仍在运行,必须保留描述符和候选文件所有权,直到实际操作返回或文档化取消完成。随后关闭自有资源,只删除应用创建的临时字节。永远不要删除、重写、改名或声称拥有 provider 原件。没有独立可信的预期摘要并完成比较时,不宣称通过可信 hash 校验。 用确定性夹具测试边界,至少包括:可 seek 且非零 offset 的已知长度文件切片;offset-zero、内容已是所选资源的 known/unknown pipe;完整文件 FD;以及必须失败、不能复制父容器的非零 offset 不兼容 pipe。还要覆盖提前 EOF、负值与溢出范围、字节上限、provider-open 取消、迟迟不返回的 read、provider 故障和 native 完成后的 FD 生命周期。断言原件不变,应用临时部分文件仅在写入者结束后清理,失败时不替换 active model。本文仅是通用兼容方法,不表示 Cove 当前已实现 URI 模型导入。
文件切片与提供方流从不同字节开始
一个模型夹具是普通 backing file,包含头部、非零偏移的已知长度切片和尾部;另一个 provider 返回的 pipe 首字节就是所选资源。native runtime 需要 seek,团队不能假装 pipe 能跳过父文件头部。
决策
把已知 offset 的文件切片和 offset-zero 流分开测试。遵循精确 runtime 契约,或有界复制 provider 实际暴露的字节;不兼容的范围应拒绝,而不是复制整个容器。
预期结果
运行时只收到已验证的目标字节,或明确报告输入能力不兼容;provider 原件与当前模型保持不变。
需要收集的证据
只有将以下证据与同一模型文件和测试运行关联起来,结论才可复核:
- ANDROID-PRIVACY-01:Android 权限、数据访问、存储和用户控制仍是应用责任。
- COVE-DOWNLOAD-01:当前 Cove 下载器写入自己的临时文件并在完成后落位;这不证明 Cove 已实现 provider URI 导入或 native FD 输入。
- ANDROID-PROVIDER-FD-2026-10:ContentResolver/AssetFileDescriptor 的子区间、长度、pipe、seek 和关闭边界。
- ContentResolver、AssetFileDescriptor 与 Storage Access Framework 官方文档定义 URI 打开、授权和描述符范围语义。
收到完整字节流还不够;最终落盘、完整性、运行时加载和恢复路径也必须验证。
实施流程
- 固定运行时输入契约. 查准确运行时版本文档,确认模型加载器接收顺序流、完整 FD、FD 加 offset/length,还是可 seek 路径。不要从 Android API 或其他 runtime 推断支持。
- 用正确 API 打开提供方资源. 通过已授权的 content URI 使用 ContentResolver。提供方可能返回子区间时优先使用 openAssetFileDescriptor;只有 provider 和 runtime 契约都支持完整文件时才用 openFileDescriptor。
- 记录并验证范围. 将 provider metadata 与 AFD startOffset、declared length 分开。未知长度只接受 offset 为零;拒绝其他负值、非零 offset 加未知长度和溢出,并始终使用产品字节上限。
- 选择流、原始描述符或有界复制. 顺序读取足够时使用 createInputStream,它已应用 AFD offset,不能二次 skip。pipe 首字节必须就是所选资源;只复制实际暴露字节,否则拒绝父文件切片。
- 保护 seek 与取消的资源所有权. 只把 Os.lseek(fd, 0, SEEK_CUR) 当作不移动查询;其他 seek 会改变共享游标,ParcelFileDescriptor.dup 也共享该位置。CancellationSignal 可取消 provider open,但不保证打断任意阻塞读取或 native 操作。
- 验证关闭和失败边界. 分别测试非零 offset 已知长度切片、offset-zero 流、提前 EOF、范围溢出、字节上限、provider 取消、迟迟不返回的 read 和 native 完成。实际操作安全结束后才关闭句柄并清理临时副本。
让每个阶段的状态都可观察。失败信息应指出阶段、模型、运行时和恢复动作,同时避免记录用户私密内容。
验收标准
- 准确 runtime 版本的输入形式有文档与测试依据;不得把裸 FD 当作路径或自动支持子区间的 API。
- 只有可 seek 夹具测试非零 offset 已知长度切片;pipe 夹具从所选字节零开始,负值、溢出或非零 offset 搭配 UNKNOWN_LENGTH 均失败关闭。
- UNKNOWN_LENGTH 仅在 offset 为零时接受,并在独立上限内读到 EOF;不兼容且不可 seek 的父容器子区间直接拒绝,不从原文件字节零复制。
- 取消仅按文档化的 open/provider 行为解释;阻塞 read 或 native 消费真正完成前,描述符与候选文件持续由所有者持有。
决策对照表
| 维度 | 选择或证据 | 解读方式 |
|---|---|---|
| 已知偏移的 AFD 切片 | 对普通 backing file 中已知 offset 和 length 的子区间使用。 | createInputStream 已应用 asset offset,不能再次手动 skip。 |
| offset-zero provider 流 | pipe 内容本身从所选资源字节开始时使用。 | 已知长度可限制范围;UNKNOWN_LENGTH 只能在独立上限内读到 EOF。 |
| native FD 加 offset/length | 仅当精确 runtime API 明确支持这些切片参数时使用。 | 使用期间保持 FD 存活并传入 AFD 范围;不能认为裸 FD 自带这些值。 |
| 应用自有临时文件 | provider 暴露字节正确,但 runtime 需要 path 或 seek 时有界复制。 | 复制无法从 pipe 恢复隐藏的非零子区间;请求准确切片流,否则拒绝。 |
需要避免的失败模式
- 把 content URI 当成普通文件路径,或假设只读 ParcelFileDescriptor 一定支持 seek。
- 对 AssetFileDescriptor 子区间从底层文件偏移 0 读取,或把整份容器交给只应消费切片的模型解析器。
- 把 UNKNOWN_LENGTH 当成空文件或无界读取许可,或不检查 offset + length 溢出和提前 EOF。
- 为获得 seek 能力请求不必要的 rw 权限,或在 native consumer 使用共享 FD 时用 lseek 改变游标。
- 把 provider 源文件当作应用自有文件删除、改名,或在导入失败时覆盖当前可用模型。
还要避免静默回退、版本未固定、缺少取消处理,以及把不同配置的结果混在一起下结论。失败运行也应与成功记录一并保存。
最小复现记录
| 层级 | 记录内容 |
|---|---|
| 设备 | 厂商、型号、芯片、内存档位、操作系统版本 |
| 软件 | 应用版本和 Git 提交号 |
| 模型 | 系列、变体、版本、格式、文件长度、摘要、量化方式 |
| 运行时 | 名称、版本、请求后端及实际后端证据 |
| 工作负载 | 样例版本、输入摘要、提示词摘要、输出策略 |
| 结果 | 完成、失败、取消、回退及保护隐私的诊断信息 |
发布检查表
- ☐ 主要搜索问题已回答,且没有未经支持的数字。
- ☐ 所有模型文件和运行时版本均已固定。
- ☐ 代表性任务和失败处理策略均有说明。
- ☐ 生命周期、取消、清理和回退均已测试。
- ☐ 用户内容与网络边界均有说明。
- ☐ 结论只适用于被记录的配置。
- ☐ 新增测试结果时链接到原始方法或证据。
来源与相关证据
- Android privacy and security
- Android API:ContentResolver.openFileDescriptor 与 openAssetFileDescriptor
- Android API:AssetFileDescriptor 范围、长度与流所有权
- Android API:Os.lseek 与文件位置操作
- Android API:OsConstants seek 起点常量
- Android API:ParcelFileDescriptor.dup 共享文件位置语义
- Android API:CancellationSignal 操作取消
- Android Developers:使用 Storage Access Framework 访问文档
中文部署与排错专题见。奇连 AI 端侧专题。
常见问题
createInputStream 还要手动 skip(startOffset) 吗?
不要。AssetFileDescriptor 包装流会应用资源范围。用已知长度、可 seek 的非零 offset 夹具测试;pipe 无法暴露所选字节时就拒绝,不复制父容器。
UNKNOWN_LENGTH 能对应有效的已选流吗?
可以,但 AssetFileDescriptor startOffset 必须为零。unknown 表示未知而非空;只在独立上限内读到 EOF,并拒绝非零 offset 搭配 UNKNOWN_LENGTH。
CancellationSignal 会停止阻塞的 native 模型读取吗?
不能这样保证。它可取消文档化的 provider open,但任意流读取或 native 调用可能不响应;保留资源直到真实所有者安全完成。
dup() 能隔离 seekability 探测和 native reader 吗?
不能。Android 文档说明 PFD.dup 与原文件共享 file position。Os.lseek(fd, 0, SEEK_CUR) 是不移动查询;改变位置的探测需要独占所有权。