Scoped Storage — 是什么、文件管理和存储访问

作者: IT Sectr 发布日期: 2026-07-10 阅读时间: 10 分钟

Scoped Storage — 是一种文件系统访问模型,引入于Android 10(API 29),它限制了应用程序对设备共享存储的随意访问。根据Google Android Developer Documentation(2024),Scoped Storage取代了旧的READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限模型,允许应用程序仅访问其隔离沙箱中的自有文件以及通过MediaStore访问共享集合。此模型提高了用户数据的隐私性,并防止应用程序未经授权收集信息。

要点

  • Scoped Storage — Android 10+中限制随意读取共享存储的文件访问模型。
  • 应用程序拥有自己的沙箱,无需额外权限即可完全访问。
  • 访问共享文件(照片、视频、音频)使用MediaStore API
  • 访问任意目录和文件使用Storage Access Framework(SAF)。
  • READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限在大多数场景中已失效。

Android中的Scoped Storage是什么?

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如何。

为什么需要Scoped Storage

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中,应用程序无需任何权限即可完全访问自己的外部私有目录。这是存储不应被其他应用程序访问或应用程序为自己使用而创建的文件的

kotlin
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 — 访问媒体文件

MediaStore — 是Scoped Storage中访问共享媒体文件(图像、视频、音频)的主要API。应用程序不通过文件路径工作,而是通过MediaStore提供者提供的content URIs工作。

MediaStore分为三个主要集合:ImagesVideoAudio。每个集合通过ContentResolver支持CRUD操作。要向Images集合插入新文件,使用MediaStore.Images.Media;要查询现有文件,使用相应URI进行查询。

kotlin
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)

Storage Access Framework(SAF)——是用于访问应用程序沙箱之外的任意文件和目录的API。SAF为用户提供文件或目录选择界面,之后应用程序获得带有临时访问权限的content URI。

SAF用于MediaStore未覆盖的场景:处理任意文档(PDF、ZIP、APK)、访问SD卡上的目录、从其他应用程序导入和导出文件。用户通过系统文件选择器明确选择文件——这保证了他对访问的同意。

kotlin
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+上获取共享存储中任意文件访问权限的唯一方法。

将应用程序迁移到Scoped Storage

将现有Android应用程序迁移到Scoped Storage需要在几个关键领域进行更改。Google建议分阶段进行,并在Android 11+上进行测试。

用content URIs替换文件路径

将共享存储中所有直接的File操作替换为ContentResolver.openInputStream / openOutputStream。对于应用程序沙箱(getExternalFilesDir)中的文件,文件路径仍然有效。

更新AndroidManifest

如果特定场景不需要(例如,文件管理——有专门的MANAGE_EXTERNAL_STORAGE权限),请删除READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE

场景旧方法Scoped Storage方法
保存照片File(path).writeBytes()MediaStore + ContentResolver
读取PDFFile(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进行,而不是通过直接文件路径。

kotlin
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中禁用Scoped Storage吗?

不可以,从Android 11(API 30)开始,Scoped Storage对所有应用程序都是强制性的。在Android 10中可用的requestLegacyExternalStorage标志在API 30+上不起作用。获得对文件系统广泛访问的唯一方法是MANAGE_EXTERNAL_STORAGE权限,但它仅适用于文件管理器和防病毒软件。

什么是MANAGE_EXTERNAL_STORAGE以及如何获得它?

MANAGE_EXTERNAL_STORAGE——对于需要完全访问文件系统的应用程序(文件管理器、备份工具)的特殊权限。请求权限会打开系统屏幕,并带有用户警告。在Google Play发布时,需要填写关于此权限必要性的Declaration表格。

如何在Scoped Storage中从content URI获取文件路径?

在Android 11+的Scoped Storage中,无法从content URI获取直接文件路径。相反,使用ContentResolver.openInputStream()进行读取,使用openOutputStream()进行写入。如果为了与第三方库兼容而需要文件路径——在getCacheDir()中创建文件的副本并使用副本。

Scoped Storage如何影响FileProvider?

FileProvider继续正常工作——它用于通过content URIs向其他应用程序提供来自应用程序沙箱的文件访问。Scoped Storage不影响FileProvider,因为FileProvider在content URIs级别工作,而不是直接文件路径。

如何检查设备是否支持Scoped Storage?

检查Build.VERSION.SDK_INT:如果>= Build.VERSION_CODES.Q(29),设备支持Scoped Storage。但在Android 10中,Scoped Storage可以通过requestLegacyExternalStorage禁用。在Android 11+上,检查manifest中是否设置了requestLegacyExternalStorage标志——如果没有,则Scoped Storage处于活动状态。

总结

  • Scoped Storage — Android 10+中限制随意读取共享存储的文件访问模型。
  • 应用程序无需额外权限即可完全访问其私有沙箱。
  • 访问共享媒体文件使用带有content URIs的MediaStore API
  • 访问任意文件和目录使用Storage Access Framework(SAF)。
  • 从Android 11开始,Scoped Storage对所有应用程序是强制性的
  • READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限在Android 11+上的大多数场景中不起作用。
  • 迁移时,将直接文件路径替换为ContentResolver.openInputStream / openOutputStream。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读