Scoped Storage — 是一种文件系统访问模型,引入于Android 10(API 29),它限制了应用程序对设备共享存储的随意访问。根据Google Android Developer Documentation(2024),Scoped Storage取代了旧的READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限模型,允许应用程序仅访问其隔离沙箱中的自有文件以及通过MediaStore访问共享集合。此模型提高了用户数据的隐私性,并防止应用程序未经授权收集信息。
要点
Scoped Storage — 是Android 10(API 29)中的一项架构更改,从根本上改变了应用程序访问设备文件系统的方式。应用程序不再完全访问共享存储,而只能访问自己的沙箱以及通过MediaStore访问某些媒体文件集合。
在Scoped Storage之前,任何拥有READ_EXTERNAL_STORAGE权限的应用程序都可以读取共享存储中的所有文件——照片、文档、下载内容、其他应用程序的数据。这造成了隐私风险:不诚实的应用程序可以在用户不知情的情况下收集用户信息。
根据Google I/O 2019的数据,超过65%的Android用户认为数据隐私是选择应用程序时的关键因素。Scoped Storage直接回应了这一需求:每个应用程序在隔离环境中运行,并且只有在用户明确同意的情况下才能访问其他人的数据。
重要提示:Scoped Storage在Android 10中是可选的(应用程序可以通过requestLegacyExternalStorage禁用它)。从Android 11(API 30)开始,Scoped Storage对所有应用程序成为强制性要求,无论target SDK如何。
Google引入Scoped Storage是为了解决旧文件访问模型的三个基本问题:隐私、权限管理和残留文件清理。
在旧模型中,拥有READ_EXTERNAL_STORAGE权限的应用程序可以扫描整个共享存储并收集用户文件的元数据——照片的地理坐标、文档名称、目录结构。Scoped Storage消除了这种可能性:即使拥有权限,应用程序也只能看到自己创建的文件以及用户通过SAF明确选择的文件。
在Scoped Storage之前,应用程序卸载后可能会在共享存储中留下文件。随着时间的推移,会积累垃圾目录。Scoped Storage解决了这个问题:应用程序沙箱中的所有文件随应用程序一起删除,而通过MediaStore的文件有所有者,可以由系统清理。
| 方面 | 旧模型(Legacy) | Scoped Storage |
|---|---|---|
| 访问共享存储 | 完全(有权限) | 仅自己的沙箱 |
| 媒体文件 | 直接文件路径 | 通过MediaStore URI |
| 他人文件 | 始终可用 | 仅通过SAF |
| 卸载时清理 | 文件保留 | 沙箱被删除 |
| 用户控制 | 最低限度 | 每个文件明确同意 |
Scoped Storage是Google在Android中加强隐私的总体战略的一部分,该战略还包括“仅在使用时”权限、摄像头和麦克风访问指示器以及Privacy Dashboard。
每个Android应用程序都有自己的私有目录,可通过Context.getFilesDir()和context.getCacheDir()访问。要访问外部(共享)私有目录,请使用Context.getExternalFilesDir()。
在Scoped Storage中,应用程序无需任何权限即可完全访问自己的外部私有目录。这是存储不应被其他应用程序访问或应用程序为自己使用而创建的文件的
import android.os.Environment
import java.io.File
import java.io.IOException
class FileManager {
fun saveToAppStorage(context: Context, fileName: String, data: ByteArray) {
val appDir = context.getExternalFilesDir(null)
?: return
val file = File(appDir, fileName)
file.writeBytes(data)
}
fun readFromAppStorage(context: Context, fileName: String): ByteArray? {
val appDir = context.getExternalFilesDir(null)
?: return null
val file = File(appDir, fileName)
return file.takeIf { it.exists() }?.readBytes()
}
}
外部私有目录路径:/storage/emulated/0/Android/data/{packageName}/files/。从Android 11开始,直接文件路径不可用——只能通过API。这是Scoped Storage安全性的另一项增强。
MediaStore — 是Scoped Storage中访问共享媒体文件(图像、视频、音频)的主要API。应用程序不通过文件路径工作,而是通过MediaStore提供者提供的content URIs工作。
MediaStore分为三个主要集合:Images、Video和Audio。每个集合通过ContentResolver支持CRUD操作。要向Images集合插入新文件,使用MediaStore.Images.Media;要查询现有文件,使用相应URI进行查询。
import android.content.ContentValues
import android.provider.MediaStore
import android.os.Environment
import java.io.OutputStream
fun saveImageToGallery(context: Context, bitmap: Bitmap, title: String) {
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "$title.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(
MediaStore.Images.Media.RELATIVE_PATH,
"${Environment.DIRECTORY_PICTURES}/MyApp"
)
}
val uri = context.contentResolver
.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
?: return
context.contentResolver.openOutputStream(uri)?.use { output: OutputStream ->
bitmap.compress(
Bitmap.CompressFormat.JPEG,
95,
output
)
}
}
重要提示:MediaStore不支持任意文件路径——仅content URIs。尝试通过MediaStore.Files.getContentUri()从URI获取文件路径,在Android 11+上不会给出直接的文件路径。相反,请使用ContentResolver.openInputStream()和openOutputStream()来处理内容。
Storage Access Framework(SAF)——是用于访问应用程序沙箱之外的任意文件和目录的API。SAF为用户提供文件或目录选择界面,之后应用程序获得带有临时访问权限的content URI。
SAF用于MediaStore未覆盖的场景:处理任意文档(PDF、ZIP、APK)、访问SD卡上的目录、从其他应用程序导入和导出文件。用户通过系统文件选择器明确选择文件——这保证了他对访问的同意。
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.provider.DocumentsContract
const val REQUEST_CODE_PICK_DIR = 1001
fun pickDirectory(activity: Activity) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
activity.startActivityForResult(intent, REQUEST_CODE_PICK_DIR)
}
fun handlePickResult(requestCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_PICK_DIR && data != null) {
val treeUri: Uri = data.data ?: return
// 获取持久URI权限
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(treeUri, flags)
}
}
SAF允许应用程序访问用户选择的目录,直到设备重启(当使用takePersistableUriPermission时)。这是在Android 11+上获取共享存储中任意文件访问权限的唯一方法。
将现有Android应用程序迁移到Scoped Storage需要在几个关键领域进行更改。Google建议分阶段进行,并在Android 11+上进行测试。
将共享存储中所有直接的File操作替换为ContentResolver.openInputStream / openOutputStream。对于应用程序沙箱(getExternalFilesDir)中的文件,文件路径仍然有效。
如果特定场景不需要(例如,文件管理——有专门的MANAGE_EXTERNAL_STORAGE权限),请删除READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE。
| 场景 | 旧方法 | Scoped Storage方法 |
|---|---|---|
| 保存照片 | File(path).writeBytes() | MediaStore + ContentResolver |
| 读取PDF | File(path).inputStream() | SAF ACTION_OPEN_DOCUMENT |
| 自己的文件 | Environment.getExternalStorageDirectory() | context.getExternalFilesDir() |
| 缓存 | File(cacheDir).writeBytes() | context.cacheDir(不变) |
迁移后,在Android 11+(API 30)上测试应用程序,在build.gradle中设置targetSdk = 30或更高。确保共享存储中的所有文件操作通过MediaStore或SAF进行,而不是通过直接文件路径。
import android.os.Build
fun isScopedStorage(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
}
fun compatReadFile(context: Context, uri: Uri): ByteArray? {
return if (isScopedStorage()) {
context.contentResolver
.openInputStream(uri)?.readBytes()
} else {
File(uri.path ?: return null)
.takeIf { it.exists() }
?.readBytes()
}
}
根据Google Play Console(2024)的数据,超过78%的活跃Android设备在Android 10+上运行Scoped Storage。迁移是在Google Play中发布更新的必要步骤:新应用程序必须target API 31+,更新——API 30+。
常见问题
不可以,从Android 11(API 30)开始,Scoped Storage对所有应用程序都是强制性的。在Android 10中可用的requestLegacyExternalStorage标志在API 30+上不起作用。获得对文件系统广泛访问的唯一方法是MANAGE_EXTERNAL_STORAGE权限,但它仅适用于文件管理器和防病毒软件。
MANAGE_EXTERNAL_STORAGE——对于需要完全访问文件系统的应用程序(文件管理器、备份工具)的特殊权限。请求权限会打开系统屏幕,并带有用户警告。在Google Play发布时,需要填写关于此权限必要性的Declaration表格。
在Android 11+的Scoped Storage中,无法从content URI获取直接文件路径。相反,使用ContentResolver.openInputStream()进行读取,使用openOutputStream()进行写入。如果为了与第三方库兼容而需要文件路径——在getCacheDir()中创建文件的副本并使用副本。
FileProvider继续正常工作——它用于通过content URIs向其他应用程序提供来自应用程序沙箱的文件访问。Scoped Storage不影响FileProvider,因为FileProvider在content URIs级别工作,而不是直接文件路径。
检查Build.VERSION.SDK_INT:如果>= Build.VERSION_CODES.Q(29),设备支持Scoped Storage。但在Android 10中,Scoped Storage可以通过requestLegacyExternalStorage禁用。在Android 11+上,检查manifest中是否设置了requestLegacyExternalStorage标志——如果没有,则Scoped Storage处于活动状态。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。