Android 分区存储限制了 Capacitor 应用在其自身文件夹之外可读写的内容。自 Android 10 起,并在 Android 11 中全面强制执行,应用可自由使用其私有目录,可通过 MediaStore 向共享集合添加文件,并需通过存储访问框架(Storage Access Framework)让用户选择其他任何文件。本指南解释了每个 Android 版本的变化,以及哪些 Capacitor Directory 值仍然有效,以及如何在 Capacitor 8 上以受支持的方式保存、导入和导出文件。
各版本的变化
| Android | API | 对 Capacitor 应用有重要影响的变更 |
|---|---|---|
| 10 | 29 | 引入 Scoped storage。应用可通过以下方式选择退出: requestLegacyExternalStorage="true" 同时针对 API 29 |
| 11 | 30 | 针对 30+ 的应用强制实施 Scoped storage。 requestLegacyExternalStorage 被忽略。 WRITE_EXTERNAL_STORAGE 不再授予任何权限。 MANAGE_EXTERNAL_STORAGE 引入。Tree picker 无法选择 Download 根目录或卷根目录 |
| 12 | 31-32 | 无重大存储变更 |
| 13 | 33 | READ_EXTERNAL_STORAGE 替换为 READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO。没有任何权限可授予对其他应用文档的访问权 |
| 14 | 34 | 部分照片访问权限,适用于 READ_MEDIA_VISUAL_USER_SELECTED |
| 15 和 16 | 35-36 | 没有新的存储模型,Play 对照片和视频权限的政策已强制执行 |
Capacitor 8 面向 API 36,因此你的应用在现代设备上始终以完整的分区存储运行。没有 flag 可以恢复旧行为。
旧版 Capacitor code 中哪些功能失效了
为 Capacitor 2 或 3 编写、或从 Cordova 移植的 Code,在新设备上经常会静默失败:
Filesystem.writeFile({ directory: Directory.Documents })或Directory.ExternalStorage“保存到手机”。在 Android 11+ 上,这些路径指向共享存储,写入会失败,或者重新安装后文件无法读回。- 读取任意路径,例如
/storage/emulated/0/Download/file.pdf来自在设置中输入的路径,或从服务器接收的路径。 - 请求
READ_EXTERNAL_STORAGE以列出用户的文档。在 Android 13+ 上,该权限对非媒体文件无效。 - 使用
Filesystem.requestPermissions()并假设granted意味着应用可以写入任意位置。 - 从选择器返回的 URI 构建文件路径
content://。
无需任何权限仍可用的功能
应用自身目录不受 Scoped Storage 影响,也无需额外权限:
Capacitor Directory |
Android 路径 | 用户可见 | 卸载时移除 |
|---|---|---|---|
Data, Library, LibraryNoCloud |
/data/data/<package>/files |
否 | 是 |
Cache |
/data/data/<package>/cache |
否 | 是 |
External |
/storage/emulated/0/Android/data/<package>/files |
仅可通过 USB 或部分文件管理器 | 是 |
ExternalCache |
/storage/emulated/0/Android/data/<package>/cache |
同上 | 是 |
Documents |
共享 Documents 文件夹 |
是 | 否 |
ExternalStorage |
共享存储根目录 | 是 | 否 |
前四行是安全的。 Documents 以及 ExternalStorage 是旧版行:从 Android 11 起,应用只能看到自己创建的文件,而且重装后连这些文件也会失去访问权限。对于新的 code,请将它们视为不受支持。
External 适用于大文件(离线视频、地图),因为它可能位于更大的分区上,且其中的文件不在 Auto Backup 范围内。从 Android 11 起,其他应用无法读取 Android/data,所以实际上它仍然是私有的。
保存用户能找到的文件:MediaStore
用户点击发票上的“Download”时,会期望在 Downloads 文件夹里找到它。在 Android 10+ 上,应用可以将文件插入共享 Downloads, Pictures, Movies, Music 以及 Documents 集合,通过 MediaStore,无需任何权限。
@capgo/capacitor-file-sharer 将此封装为 save():
bun add @capgo/capacitor-file-sharer
bunx cap sync
import { FileSharer } from '@capgo/capacitor-file-sharer';
import { Filesystem, Directory } from '@capacitor/filesystem';
const { uri } = await Filesystem.getUri({ path: 'exports/invoice-1042.pdf', directory: Directory.Data });
const result = await FileSharer.save({
filename: 'invoice-1042.pdf',
path: uri,
contentType: 'application/pdf',
android: {
saveDirectory: 'downloads', // 'downloads' | 'pictures' | 'movies' | 'music' | 'documents'
relativePath: 'MyApp/Invoices', // subfolder, Android 10+
},
});
console.log(result.uri); // content:// URI of the saved file
在 iOS 上,相同的调用会打开分享面板,用户选择“保存到文件”,因此一个 code 路径即可覆盖两个平台。
在 Android 9 及更低版本中,写入共享存储仍需要 WRITE_EXTERNAL_STORAGE。仅为这些版本声明:
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
Capacitor 8 支持 Android 7 (API 24) 及以上版本,因此如果您仍向 Android 7 到 9 的设备发布应用,请保留该行。
照片和视频
对于媒体文件,请优先使用平台流程,而不是自行写入文件。 @capacitor/camera 使用以下代码写入图库: saveToGallery: true,并且仅在旧版 Android 上需要遗留存储权限。请参阅我们的指南: 在 Capacitor 中拍摄和编辑照片.
读取用户文件:用选择器代替权限
Scoped storage 预期由用户选择文件。Android 选择器返回一个 content:// 即使应用没有存储权限也能读取的 URI。
import { CapgoFilePicker } from '@capgo/capacitor-file-picker';
import { Filesystem, Directory } from '@capacitor/filesystem';
const { files } = await CapgoFilePicker.pickFiles({ types: ['application/pdf'], limit: 1 });
const picked = files[0];
// Copy into app storage right away: picker grants can expire
await Filesystem.copy({
from: picked.path!,
to: `imports/${picked.name}`,
toDirectory: Directory.Data,
});
对于照片和视频, @capgo/capacitor-file-picker的 pickMedia 以及 @capacitor/camera的 chooseFromGallery 在 Android 13+ 上请使用 Android Photo Picker,它完全不需要权限。
避免使用 READ_MEDIA_* 权限
Google Play 的照片和视频权限政策仅允许 READ_MEDIA_IMAGES 以及 READ_MEDIA_VIDEO 核心功能需要广泛访问相册的应用使用,例如相册应用或照片编辑器。其他所有应用都必须使用 Picker,并在 Play Console 中声明。如果你使用的插件声明了这些权限,请从合并后的 manifest 中将其移除:
<uses-permission
android:name="android.permission.READ_MEDIA_IMAGES"
tools:node="remove" />
<uses-permission
android:name="android.permission.READ_MEDIA_VIDEO"
tools:node="remove" />
添加 xmlns:tools="http://schemas.android.com/tools" 到 <manifest> 先打标签。
使用 Storage Access Framework 访问文件夹
某些应用需要持续访问某个文件夹:比如同步到用户文件夹的笔记应用,或外勤应用的导出文件夹。Storage Access Framework (SAF) 允许用户通过以下方式授予文件夹访问权限: ACTION_OPEN_DOCUMENT_TREE:
const { path } = await CapgoFilePicker.pickDirectory();
// On Android: a content://com.android.externalstorage.documents/tree/... URI
需要了解的三点:
- 得到的是 tree URI,而不是路径。 不能把它传给
Filesystem.writeFile。里面的文件通过DocumentsContract或DocumentFile在原生 code 中创建和读取。 - 重启后访问权限不会保留 除非你的应用调用
takePersistableUriPermission来操作该 URI。如果需要此功能,请添加一个简单的原生方法:
@PluginMethod
public void persist(PluginCall call) {
Uri uri = Uri.parse(call.getString("uri"));
getContext().getContentResolver().takePersistableUriPermission(
uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
call.resolve();
}
- 部分文件夹无法授权。 在 Android 11+ 上,选择器会拒绝内部存储的根目录、
Download文件夹的根目录,以及Android/data以及Android/obb. 请用户创建或选择一个子文件夹。
每个应用的持久化授权数量有限,用户可以在系统设置中或通过清除应用数据来撤销授权。请在 getPersistedUriPermissions() 启动时检查访问权限,如果权限已失效则重新请求。
所有文件访问权限:MANAGE_EXTERNAL_STORAGE
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
此权限提供旧的“读写所有文件”行为,但它不是运行时提示。用户必须在专用设置界面中手动启用,且 Google Play 会进行人工审核。被接受的用例包括文件管理器、备份与恢复、杀毒软件、文档管理、设备端文件搜索,以及少数以广泛访问为核心功能的应用。笔记应用、从下载文件夹打开文件的 PDF 阅读器,或“只需保存导出文件”的应用将被拒绝。
如果你在 Play 之外分发(企业 MDM、侧载的 kiosk 设备),这是一个选项。否则,请围绕 MediaStore 和 SAF 进行设计。
其他插件如何配合
| 插件 | Scoped storage 的影响 |
|---|---|
@capacitor/filesystem |
应用目录不受影响。 Documents 以及 ExternalStorage 在 Android 11+ 上已是旧版。 requestPermissions() 只影响旧版 Android 上的这些目录 |
@capacitor/camera |
使用系统相机和 Photo Picker,无需存储权限,除非 saveToGallery 在 Android 9 及更低版本上 |
@capacitor/share |
通过以下方式进行共享 FileProvider。仅列出在 res/xml/file_paths.xml 才能分享 |
@capgo/capacitor-file-sharer |
使用其自带的 FileProvider 以及 MediaStore 来处理 save(),无需配置 |
@capgo/capacitor-downloader |
下载到应用外部存储,然后使用 save() 用于导出 |
@capgo/capacitor-file-picker |
基于 Picker,Android 13+ 无需权限 |
@capgo/capacitor-photo-library |
其 Manifest 会添加 READ_MEDIA_* 用于完整读取媒体库,需遵守上述 Play 政策。 pickMedia() 使用系统 Picker 且无需权限,因此如果只用它,请移除 READ_MEDIA_* 配合 tools:node="remove" |
选择使用哪个目录
- 应用数据、设置、数据库:
Directory.Data - 临时文件和分享导出:
Directory.Cache - 可重新下载的大型媒体文件:
Directory.ExternalAndroid 端,Directory.LibraryNoCloud在 iOS 上 - 用户希望保存在下载或图片中的文件:通过 MediaStore
FileSharer.save() - 用户导入应用的文件:先选择,再复制到
Directory.Data - 用户希望应用持续写入的文件夹:带有持久化授权的 SAF 树 URI
如需完整的读写 API,请参阅 Capacitor 文件处理指南.
迁移现有应用
- 在您的 code 中搜索
Directory.Documents,Directory.ExternalStorage,/storage/emulated,以及requestLegacyExternalStorage. - 将应用拥有的文件移至
Directory.Data或Directory.External. 在新版本首次启动时,复制所有仍可读取的旧文件,然后删除它们。 - 将“保存到设备”功能替换为
FileSharer.save(). - 将路径输入框替换为选择器。
- 添加
android:maxSdkVersion迁移到旧版存储权限,并移除READ_MEDIA_*除非确实需要。 - 在 Android 10、11、13 以及 15 或 16 的设备或模拟器上测试。行为差异恰好出现在这些边界版本上。
preserveLegacyExternalStorage="true" ,详见 <application> tag 会为从使用旧版存储的版本更新而来的应用保留旧版访问权限 requestLegacyExternalStorage,直到应用重新安装。它只在一次性迁移步骤中有效,不要依赖它。
故障排除
EACCES (Permission denied) 写入 /storage/emulated/0/...:你正在直接写入共享存储。请使用 MediaStore 或应用目录。
文件已通过以下方式保存 Directory.Documents 重新安装后已丢失:应用已失去对该文件的所有权。用户文件请使用 MediaStore。
FileNotFoundException 读取 content:// URI 稍后:临时授权已过期。请在选择文件时复制该文件,或对树 URI 持久化授权。
Play Console 关于照片和视频权限的警告:某个插件添加了 READ_MEDIA_IMAGES 或 READ_MEDIA_VIDEO。使用 tools:node="remove" 移除它们(如果不需要)。
requestPermissions() 权限已授予,但写入仍然失败: 在 Android 11+ 上,旧版存储权限虽然会被授予,但对共享存储已无意义。
保持原生与 Web 端同步
存储规则属于原生配置,因此修改 manifest 或插件需要走应用商店发版,而这 Capgo 构建 可以在云端生成。围绕它们的文件逻辑(路径、文件夹名称、导出流程)都是 JavaScript,您可以通过 Live Update 进行更新 Capgo 实时更新.