移动设备上的外部存储是一种可移动数据介质,任何具有相应权限的应用程序都可以访问它。根据 Android Open Source Project, 2026,从 Android 10 开始,Google 引入了 Scoped Storage——一种限制直接访问文件系统并要求使用 MediaStore API 处理公共媒体文件的模型。这种方法提高了用户数据的安全性,并防止了应用程序之间的信息泄漏。
要点
READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE 对于 Android 9 及更低版本是必需的外部存储是不属于应用程序受保护沙箱且其他程序可以访问的内存区域。在 Android 上,这个术语通常指 SD 卡,但外部存储也包括通过 OTG 适配器连接的 USB 驱动器以及在文件系统级别集成的云盘。与内部存储的主要区别在于缺乏隔离:任何具有相应权限的应用程序都可以读取和修改外部存储中的文件。
在 Android 上,外部存储以两种主要模式呈现:便携式(portable)和内部式(adoptable)。当 SD 卡格式化为便携式存储时,它保持可移动性,并格式化为 FAT32 或 exFAT。在 adoptable storage 模式下,卡格式化为 ext4 并使用 AES-128 加密,之后系统将其视为内部存储的扩展。Adoptable 模式从 Android 6.0 Marshmallow 开始可用,但设备制造商经常在其固件中禁用此功能。根据 Google Play Console 统计数据,只有约 12% 的 Android 设备支持 adoptable storage,因此不建议仅依赖通过 SD 卡扩展内部存储作为唯一方案。
在 iOS 上没有传统的 SD 卡。Apple 使用封闭架构,所有存储都建立在内部闪存和 iCloud 上。外部驱动器可通过 Lightning 或 USB-C 端口支持,但对它们的访问限于系统文件管理器 Files 和与 Document Picker 集成的应用程序。用户自行决定从外部源打开哪些文件——应用程序未经明确同意不能扫描连接的驱动器。
外部介质的选择取决于对速度、容量和便携性的要求。不同类型的介质在移动应用程序中具有不同的速度特性、文件系统和使用场景。
| 介质类型 | 最大容量 | 读取速度 | 文件系统 |
|---|---|---|---|
| SD UHS-I | 2 TB | 最高 104 MB/s | exFAT / FAT32 |
| SD UHS-II | 2 TB | 最高 312 MB/s | exFAT / FAT32 |
| USB 3.0 OTG | 2 TB | 最高 400 MB/s | exFAT / NTFS / FAT32 |
| USB-C SSD | 4 TB | 最高 1000 MB/s | exFAT / APFS / NTFS |
SD 卡按写入速度分类:Class 10 (10 MB/s)、U1 (10 MB/s)、U3 (30 MB/s)、V30 (30 MB/s) 和 V90 (90 MB/s)。录制 4K 视频需要至少 U3 或 V30 级别的卡。对于处理大型媒体文件(照片处理、视频编辑)的移动应用程序,建议使用 U3 及以上级别的卡。卡的速度直接影响应用程序的性能:在 Class 10 卡上录制 4K 视频可能因带宽不足而导致丢帧。为应用程序选择 SD 卡时,不仅要关注速度等级,还要关注 Application Performance Class 评级:A1 和 A2 表示随机读取和写入的最低性能,这对数据库和应用程序缓存的运行至关重要。
USB-OTG(On-The-Go)允许通过适配器将外部 USB 驱动器连接到移动设备。Android 从 3.1 版本开始支持 OTG,但挂载驱动器需要内核支持文件系统。FAT32 和 exFAT 原生支持,NTFS 仅在有自定义内核的设备上或通过 Paragon 库支持。在 iOS 上,USB 驱动器支持随 iOS 13 与 Files 应用程序一起出现。应用程序必须使用 UIDocumentPickerViewController 来与驱动器交互,该组件仅提供对用户所选文件的访问——iOS 不提供对驱动器文件系统的完全访问。
云服务——iCloud、Google Drive、Dropbox——可以通过系统提供程序集成到设备的文件系统中。在 iOS 上,iCloud Drive 是应用程序沙箱容器的一部分,文件在用户的设备之间自动同步。在 Android 上,Google Drive 提供用于读写文件的 API,但不会直接挂载到文件系统中。对于处理大量数据的移动应用程序,云存储可以作为 SD 卡的替代方案,特别是在没有扩展插槽的设备上。
在 Android 上使用外部存储取决于操作系统版本和 Scoped Storage 模型。文件访问的过程在 Android 9 及更低版本、Android 10–12 和 Android 13+ 上有所不同。
从 Android 10 开始,访问公共媒体文件的主要方式是 MediaStore API。该 API 为在公共存储中读写图像、视频和音频文件提供统一接口。应用程序无需 READ_EXTERNAL_STORAGE 权限即可访问自己的文件,但读取其他应用程序的文件仍需要明确的用户许可。MediaStore 自动索引媒体文件并提供 ContentResolver 供查询。
val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME
)
val cursor = contentResolver.query(collection, projection, null, null, null)
执行查询后,您会收到一个包含文件 URI 的 Cursor。要读取文件,请通过 contentResolver.openInputStream(uri) 打开 InputStream。写入类似地通过 contentResolver.openOutputStream(uri) 完成。MediaStore 自动处理名称冲突,并提供通过 contentResolver.insert(uri, values) 插入新文件的选项,该选项返回所创建文件的 URI。
Storage Access Framework (SAF)——在 Android 10+ 上访问任意文件的推荐方式。SAF 提供系统文件选择对话框,用户通过它授予应用程序访问特定文件或目录的权限。应用程序获得一个访问 URI,该 URI 在设备重启之前一直有效,直到通过设置明确撤销权限。SAF 不需要 READ_EXTERNAL_STORAGE 或 WRITE_EXTERNAL_STORAGE 权限,这简化了在 Google Play 上的发布,并减少了权限请求的数量。
val requestCode = 42
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
startActivityForResult(intent, requestCode)
在 iOS 上,通过 UIDocumentPickerViewController 处理外部文件,用户打开它以选择特定文件或目录。应用程序无法直接访问外部驱动器的文件系统——只能访问用户明确选择的文件。选择文件后,应用程序在沙箱容器中获得一个临时 URL 地址,并可以通过 FileManager 处理文件。
UIDocumentPickerViewController 允许用户从任何可用源选择一个或多个文件:iCloud Drive、连接的 USB 驱动器、第三方云服务。选择后,控制器返回一个 URL 地址数组,应用程序可以在会话期间读取这些地址。要在应用程序关闭后保持访问,必须调用 startAccessingSecurityScopedResource() 并将安全范围书签保存到 UserDefaults。
let picker = UIDocumentPickerViewController(
forOpeningContentTypes: [.data, .image],
asCopy: true
)
picker.delegate = self
func documentPicker(
controller: UIDocumentPickerViewController,
didPickDocumentsAt urls: [URL]
) {
guard let url = urls.first else { return }
let data = try Data(contentsOf: url)
}
asCopy: true 参数意味着 iOS 会将文件复制到应用程序的沙箱容器中,应用程序将获得独立于原始文件自己的副本。如果指定 asCopy: false,应用程序将获得对原始文件的引用,但需要调用 url.startAccessingSecurityScopedResource() 才能访问。不调用此方法将导致文件读取抛出异常。完成文件操作后,请务必调用 url.stopAccessingSecurityScopedResource() 以关闭资源并防止描述符泄漏。
外部存储的安全性低于内部存储,因为文件对其他应用程序可见。任何具有 READ_EXTERNAL_STORAGE 权限的应用程序都可以读取 SD 卡上的所有文件,包括您的应用程序为临时存储敏感数据而创建的文件。恶意应用程序可以扫描 SD 卡并提取用户数据(如果数据未加密)。因此,强烈不建议在外部存储上以未加密形式存储身份验证令牌、密码或个人用户数据。
在 Android 10+ 上,Scoped Storage 显著限制了应用程序访问公共文件系统的能力。应用程序无需权限即可读取仅由自身创建的文件,要访问其他应用程序的文件,需要用户通过 SAF 对话框明确同意。但是,此限制不适用于目标 SDK 版本低于 29 的应用程序——它们继续按旧模型运行,这对没有更新应用程序的用户构成风险。Google Play 自 2021 年起要求所有新应用和更新应用的 targetSdkVersion 为 29+。
格式化和加密——另一个重要的安全方面。格式化为便携式存储的 SD 卡默认情况下不被系统加密。即使使用 adoptable storage,加密也只有当设备制造商在固件中启用此功能时才激活。要保护存储在外部存储上的敏感数据,请使用 AndroidX Security 中的 EncryptedFile 库,它使用 AES256-GCM 独立于系统加密单独加密每个文件。
外部存储的高效使用需要在数据可用性和安全性之间取得平衡。请遵循以下规则以确保用户文件的可靠存储。
Environment.getExternalStorageState() 检查 SD 卡的状态。卡可能已被移除、损坏或不可写重要的是要记住,外部存储不能保证发生故障时的数据安全。SD 卡的写入周期有限,特别是廉价的 Class 4 和 Class 6 型号。在密集写入时(例如日志记录或流数据缓存),卡可能在几个月内出现故障。对于此类场景,请使用内部存储或云存储,在外部存储上仅保存用户认为可以接受丢失的文件。定期通过 StatFs 检查 SD 卡状态,并告知用户文件系统的严重错误。
常见问题
在 Android 13+ 上,READ_EXTERNAL_STORAGE 权限已被更细粒度的权限取代:READ_MEDIA_IMAGES、READ_MEDIA_VIDEO 和 READ_MEDIA_AUDIO。要访问其他类型的文件,请使用 SAF。
可以,adoptable storage 功能在 Android 6.0+ 上可用。卡格式化为 ext4 并采用 AES-128 加密。但是,许多制造商禁用此选项,约 88% 的设备不支持 adoptable storage。
使用 Environment.getExternalStorageState() 方法,该方法返回状态字符串。MEDIA_MOUNTED 值表示卡可用于读写。其他值表示错误或卡不存在。
Scoped Storage 将应用程序限制在其自己的容器和共享 MediaStore 媒体库中。完全访问允许读取设备上的任何文件。Scoped Storage 提高了用户数据的安全性,并且是强制性的新应用程序。
使用 ACTION_OPEN_DOCUMENT_TREE 操作启动 Intent,这将打开系统目录选择对话框。选择后,您将获得一个在重启前有效的 URI 权限。将此 URI 保存到 SharedPreferences 中以备后续使用。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。