让 MNN 大模型App 支持外部存储:MnnLlmChat fork v0.8.3-pisces.1 发布
基于 MNN 官方 MnnLlmChat v0.8.3 的 fork 版本,主要解决一个痛点:模型文件只能存在 app 内部沙箱,无法跨 app 共享、无法用文件管理器访问。本版本支持把模型下载到外部存储,方便共享和管理。
背景
MNN 是阿里开源的轻量级推理引擎,官方的 MnnLlmChat 是一个功能完整的 Android 端侧多模态 LLM 应用,支持 Qwen、Llama、Gemma 等模型在本机离线推理。
但用了一段时间发现一个明显问题:模型文件默认存在 /data/data/com.alibaba.mnnllm.android/files/.mnnmodels/,这是 app 私有目录:
- ❌ 其他 app 无法访问(Android 沙箱隔离)
- ❌ 文件管理器看不到
- ❌
adb pull需要 root - ❌ 同一个模型下多个 app 各下一份,浪费存储
- ❌ 内部存储空间紧张时无法迁移到 SD 卡
对于动辄几个 GB 的大模型,这个限制很烦人。于是我 fork 了一份,加了模型存储路径自定义功能。
版本信息
| 项目 | 值 |
|---|---|
| Fork 仓库 | pisces312/MNN |
| Release | v0.8.3-pisces.1 |
| 基线版本 | upstream MnnLlmChat 0.8.3 (versionCode 830) |
| Fork 版本 | 0.8.3-pisces.1 (versionCode 95000831) |
| MNN 引擎 | 3.6.0 |
| 架构 | arm64-v8a |
| Flavor | standard |
| APK 大小 | 42 MB |
版本号约定(fork 项目):
versionName:0.8.3-pisces.N—0.8.3是 upstream 基线,-pisces.N是 fork 第 N 个 releaseversionCode:95000831—9500命名空间前缀(避开 upstream),0831= 基线 + 序号
主要改进
1. 最新 MNN 3.6.0 native libraries
重新编译了 MNN 3.6.0 的 libMNN.so,包含:
- ARM82 fp16 优化
- OpenCL GPU 后端
- 低内存模式
- 权重反量化 GEMM
- Transformer 融合优化
- 视觉 / 音频 / Diffusion 全支持
2. 模型存储路径自定义(核心功能)
痛点:默认模型存在 app 私有目录,无法跨 app 共享。
方案:在设置里加了"模型存储路径"选项,支持选外部存储目录,比如 /storage/emulated/0/mnn-models/。
设置路径:
Settings → Model storage path → Browse / Type path
效果:
- ✅ 模型下到外部存储,文件管理器直接可见
- ✅
adb pull无需 root - ✅ 多个基于 MNN 的 app 可共用同一份模型
- ✅ 方便备份 / 迁移到 SD 卡
3. Flat Mode 下载(FUSE 兼容)
这个是核心技术难点,单独说一下。
问题
MnnLlmChat 下载模型时,文件结构仿照 HuggingFace cache:
.mnnmodels/
├── modelscope/
│ └── models--MNN--MiniMind2-MNN/
│ ├── blobs/
│ │ └── {sha256} ← 真实文件
│ └── snapshots/
│ └── _no_sha_/
│ └── config.json ← symlink → ../../blobs/{sha256}
blobs/ 存真实文件,snapshots/ 用 symlink 指过去。这种设计在 HF 官方是为了跨 commit 去重。
但 Android 外部存储(/storage/emulated/0/)是 FUSE 伪文件系统,不支持 symlink! 即使有 MANAGE_EXTERNAL_STORAGE 权限,Files.createSymbolicLink 也会抛 AccessDeniedException。
第一个文件 .gitattributes 的 symlink 就失败,整个下载直接挂。
方案:Flat Mode
检测路径是否支持 symlink,不支持时切换 flat mode:
| 内部存储(默认,ext4) | 外部存储(flat mode,FUSE) | |
|---|---|---|
| 结构 | blobs/{sha} + snapshots/{sha}/ symlink |
文件直接在 snapshots/{sha}/ |
| 空间 | 1x(symlink 不占额外空间) | 1x(无 blobs 层,无双倍) |
| 跨 commit 去重 | 有(HF 设计初衷) | 无(但本项目只下 main 单 commit,无去重需求) |
flat mode 结构:
mnn-models/
└── modelscope/
└── models--MNN--MiniMind2-MNN/
└── snapshots/
└── _no_sha_/
├── config.json ← 真实文件
├── model.mnn ← 真实文件
├── tokenizer.json ← 真实文件
└── ...
实现要点:
DownloadFileUtils.isSymlinkSupported(path):在目标路径创建 probe symlink 检测- flat mode 下
blobPath == pointerPath,文件直接下到snapshots/,跳过blobs/层 ModelFileDownloader.downloadFile:检测blobPath == pointerPath时跳过createSymlink(否则会删除刚下载的文件)- 兼容性:内部存储保持 legacy symlink 结构,外部存储用 flat mode,已下载模型不受影响
自动清理 legacy 残留
如果用户之前在 legacy 模式下下载失败,blobs/ 里会留孤儿文件。切换 flat mode 后,首次下载会检测并清理:
blobs/存在 且snapshots/还没有 flat 文件 → 清理blobs/+snapshots/- 避免孤儿文件占用空间
4. 应用内日志查看器
下载失败时,logcat 不一定方便看。本版本加了 in-memory logger:
Settings → View Logs
可以看到最近 200 条日志,包括:
- 下载启动 / 路由(HF / ModelScope)
- fetchRepoInfo 请求 / 响应码
- 文件下载进度
- 失败详情(异常类名 + message)
支持复制到剪贴板 / 清空。
5. 下载失败日志增强
之前下载失败只打 Log.d(Debug 级,logcat 默认过滤),且不写入 AppLogger。改为:
Log.e带 stacktraceAppLogger.e同步写入内存日志- 整个下载链路(
ModelDownloadManager.startDownload/onDownloadFailed/updateCacheDir/fetchRepoInfo/downloadChunk/ 重定向处理)都加了Log.i/e
使用教程
1. 下载安装
从 Release 页面 下载 app-standard-debug.apk。
Debug build,
applicationIdSuffix .debug,可与官方 release 版共存。
2. 授权
首次启动会请求 “All files access” 权限(MANAGE_EXTERNAL_STORAGE),这是使用外部存储路径的前提。
3. 设置外部存储路径
Settings → Model storage path → Browse
选择一个外部存储目录,比如 /storage/emulated/0/mnn-models/。
也可以 “Type path” 手动输入绝对路径,或 “Restore default” 恢复默认内部存储。
4. 下载模型
进入 Model Market,选一个模型下载。下载完成后,用文件管理器查看:
/storage/emulated/0/mnn-models/modelscope/models--MNN--MiniMind2-MNN/snapshots/_no_sha_/
├── .gitattributes
├── README.md
├── config.json
├── model.mnn
├── model.mnn.weight
└── tokenizer.json
5. 推理
下载完后在 “My Models” 里能看到该模型,点击即可对话。flat mode 结构对推理无影响——native 层只关心能不能通过路径读到文件内容。
6. 跨 app 共享
其他基于 MNN 的 Android app,只要有存储权限,可以直接指向这个目录加载模型,省一份存储。
技术细节
为什么 HF 要用 symlink
HF 官方 cache 用 blobs/{sha} + snapshots/{commit}/ symlink 的设计,核心目的是跨 commit 去重:同一个文件(同 sha256)在多个 commit 里出现时,只存一份 blob,每个 commit 的 snapshot 用 symlink 指向它。HF 仓库历史长,跨 commit 重复文件多,去重收益明显。
为什么 MnnLlmChat 不需要
MnnLlmChat 每个模型只下载 main 分支单个 commit,单 commit 内每个文件 sha 唯一,没有跨 commit 重复。所以:
- blobs 层在本项目里去重收益为 0
- symlink 唯一作用就是满足 HF cache 的目录结构规范
flat mode 在单 commit 场景下没有空间代价(1x,与 legacy 相同),只是结构更扁平。
Android FUSE 限制
Android 11+ 对外部存储的访问收紧:
/storage/emulated/0/通过 FUSE 挂载- FUSE 不支持
symlink、hardlink、mknod等操作 - 即使
MANAGE_EXTERNAL_STORAGE权限也只是放宽了访问范围,不改变 FUSE 文件系统能力
所以即使有权限,symlink 也会失败。flat mode 是绕过这个限制的唯一方案。
内部存储 vs 外部存储对比
| 内部存储(默认) | 外部存储(自定义) | |
|---|---|---|
| 文件系统 | ext4 | FUSE |
| symlink 支持 | ✅ | ❌ |
| 其他 app 访问 | ❌ 沙箱隔离 | ✅ 有权限即可 |
| 文件管理器 | ❌ | ✅ |
adb pull |
❌ 需 root | ✅ |
| 跨 app 共用模型 | ❌ | ✅ |
| 下载结构 | legacy(blobs+symlink) | flat mode |
| 推理 | 正常 | 正常 |
Release
- GitHub Release: https://github.com/pisces312/MNN/releases/tag/v0.8.3-pisces.1
- APK 下载:
app-standard-debug.apk(42 MB) - 源码: feature/model-storage-path 分支
相关链接
- MNN 官方仓库: https://github.com/alibaba/MNN
- MnnLlmChat 官方文档: https://www.codebuddy.cn/docs/workbuddy/Overview
- 本 Fork 仓库: https://github.com/pisces312/MNN
- Release: https://github.com/pisces312/MNN/releases/tag/v0.8.3-pisces.1
如果觉得有用,欢迎 star ⭐ 和提 issue 反馈。
更多推荐



所有评论(0)