从Android 10开始,系统对外部存储的访问模型发生了根本变化,引入了分区存储(Scoped Storage)机制。在此之前,应用只要在Manifest中声明了READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限,就能通过File API任意读写外部存储的公共目录,例如/sdcard/DCIM、/sdcard/Download等。这种宽松的设计虽然方便了开发者,但也带来了隐私和垃圾文件问题。分区存储将应用可访问的空间严格限制在自身专属目录以及通过系统API授权访问的媒体集合中,未适配的应用在Android 10及以上系统上会遇到FileNotFoundException、权限被拒、文件写入后无法被其他应用看到等问题。

分区存储并没有完全禁止访问公共目录,而是将访问方式分成了三类:一是应用专属目录,位于外部存储的Android/data/包名/下,应用无需任何权限即可读写;二是系统定义的媒体集合,例如图片、视频、音频、下载文件,应用通过MediaStore API进行增删改查,且需要相应的读写权限;三是使用存储访问框架(Storage Access Framework,SAF),通过系统文件选择器让用户授权访问任意文件或目录。理解这三类访问方式是适配工作的基础。对于大多数应用,优先使用应用专属目录保存私有数据,媒体文件则切换至MediaStore,其他文件类型则通过SAF交由用户选择。
分区存储的权限模型变化
在Android 10之前,读写外部存储公共目录通常只需要在Manifest中声明两个权限:READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE。用户安装应用时如果授予了这两个权限,代码里就能用File API操作任意路径。Android 10之后,系统对权限做了更细致的拆分:READ_EXTERNAL_STORAGE仍然需要申请,但作用范围已经缩小,仅允许读取其他应用创建的媒体文件(图片、音频、视频),并且从Android 11开始,读取权限进一步细化为READ_MEDIA_IMAGES、READ_MEDIA_VIDEO、READ_MEDIA_AUDIO;WRITE_EXTERNAL_STORAGE在Android 10上只要声明并授权就可以向媒体集合中写入,但从Android 11开始,该权限被废弃,系统会忽略该权限,应用通过MediaStore写入媒体文件不再需要任何写入权限,因为写入操作被系统接管。
这意味着适配工作不能只改代码,还得同步调整运行时权限申请逻辑。对于仅需要保存应用私有文件的应用,可以直接使用getExternalFilesDir()或getExternalCacheDir(),这些目录不需要任何权限,也不会被其他应用访问。对于需要将图片保存到系统相册的需求,在Android 10及以上应使用MediaStore.Images.Media.EXTERNAL_CONTENT_URI插入数据,系统会负责实际的文件存储位置,应用不需要申请WRITE_EXTERNAL_STORAGE权限。以下是权限声明的示例,注意Android 10和Android 11的差异:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Android 10及以上仍然需要声明读权限,写权限在Android 10有效,Android 11后被忽略 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<!-- 针对Android 13及以上,读取媒体文件需要细分权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
</manifest>
如果项目的最低版本低于Android 10,还需要在代码中根据Build.VERSION.SDK_INT进行判断,动态决定申请哪些权限。同时,为了让旧版本应用在Android 10上能够继续使用传统文件路径,可以在Manifest的application标签中设置android:requestLegacyExternalStorage="true",但该属性在Android 11上被忽略,只能作为临时过渡方案,不建议长期依赖。
适配核心:应用专属目录与MediaStore
应用专属目录是分区存储中最简单可靠的存储位置。调用Context.getExternalFilesDir(String type)可以获取外部存储上属于当前应用的目录,例如传入Environment.DIRECTORY_PICTURES会返回类似/storage/emulated/0/Android/data/com.example.app/files/Pictures的路径。该目录在应用卸载时会被系统删除,不需要任何权限,也不受分区存储限制。对于需要缓存的数据,使用getExternalCacheDir()。如果应用需要保存配置、下载的临时文件、数据库备份等,这些目录是首选。以下代码展示了如何将私有文件写入专属目录:
File privateDir = getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
if (privateDir != null && !privateDir.exists()) {
privateDir.mkdirs();
}
File outputFile = new File(privateDir, "report.txt");
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
fos.write("分区存储适配示例".getBytes(StandardCharsets.UTF_8));
// 写入成功
} catch (IOException e) {
e.printStackTrace();
}
MediaStore是处理公共媒体文件的标准方式。以保存一张图片到系统相册为例,Android 10及以上推荐使用MediaStore.Images.Media.EXTERNAL_CONTENT_URI配合ContentResolver插入一条新记录,系统会分配一个Uri并将文件写入合适的目录。插入时可以通过ContentValues设置DISPLAY_NAME、MIME_TYPE等元数据。从Android 10开始,插入后返回的Uri可以直接用于写入文件流,不需要事先知道实际路径。下面是一个完整的保存图片到相册的示例:
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "photo_" + System.currentTimeMillis() + ".jpg");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/MyApp");
} else {
// Android 9及以下使用传统文件路径,需要先创建目录再写入
}
Uri collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Uri itemUri = getContentResolver().insert(collection, values);
if (itemUri != null) {
try (OutputStream os = getContentResolver().openOutputStream(itemUri)) {
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.sample);
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os);
} catch (IOException e) {
e.printStackTrace();
}
}
除了插入新文件,MediaStore也支持查询、更新和删除。例如查询所有属于自己应用的图片可以使用MediaStore.Images.Media.EXTERNAL_CONTENT_URI配合selection条件。需要注意的是,通过MediaStore查询到的文件Uri可能无法直接转换为文件路径,也不应该依赖路径来操作文件,始终使用ContentResolver.openInputStream()或openOutputStream()读写数据。如果确实需要路径(例如传给某些不支持Uri的第三方库),可以先将数据复制到应用专属目录中临时使用。对于非媒体文件(如PDF、文档),MediaStore也有MediaStore.Downloads或MediaStore.Files表,但用法类似,只是查询和插入的Uri不同。
存储访问框架(SAF)的使用场景
有些应用需要让用户选择任意位置的文件或目录,例如导入数据库备份、导出报表、选择下载位置等。这类需求无法用MediaStore完全覆盖,分区存储也禁止直接使用File API访问其他应用的专属目录或公共目录的任意子目录。此时应该使用存储访问框架,通过触发Intent让用户参与授权,系统会返回一个可读写的Uri,并授予应用对该Uri对应文件或目录的临时访问权限。常见的Intent有ACTION_OPEN_DOCUMENT(打开单个文件)、ACTION_CREATE_DOCUMENT(创建新文件)、ACTION_OPEN_DOCUMENT_TREE(选择整个目录)等。
以导出报告文件为例,应用可以启动ACTION_CREATE_DOCUMENT,指定初始文件名和MIME类型,用户在系统文件管理器中确认位置后,应用获得写入Uri,然后通过ContentResolver打开输出流写入内容。整个过程不需要任何存储权限。以下代码展示了如何启动SAF并处理返回结果:
private static final int CREATE_FILE_REQUEST = 1;
private void createReport() {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TITLE, "report.txt");
startActivityForResult(intent, CREATE_FILE_REQUEST);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CREATE_FILE_REQUEST && resultCode == RESULT_OK) {
Uri uri = data.getData();
if (uri != null) {
try (OutputStream os = getContentResolver().openOutputStream(uri)) {
os.write("导出的报告内容".getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
SAF返回的Uri通常是content://协议,并且具有时效性。如果应用希望长期保留对某个文件或目录的访问权限,需要在获得Uri后调用ContentResolver.takePersistableUriPermission(),并传入Intent.FLAG_GRANT_READ_URI_PERMISSION或FLAG_GRANT_WRITE_URI_PERMISSION。这样即使应用重启或设备重启,仍然可以访问该Uri。对于目录选择,还可以通过DocumentFile类方便地管理文件操作,它封装了SAF的树形Uri操作。虽然SAF的用户体验稍显繁琐,但它是分区存储下访问任意文件的标准方式,符合系统安全模型。
常见适配问题与排查建议
在实际适配过程中,最常见的问题是直接使用Environment.getExternalStorageDirectory()或拼接固定路径如/sdcard/DCIM/xxx.jpg,这些代码在Android 10及以上会直接失败或返回不可访问的路径。排查时可以检查Logcat中是否有FileNotFoundException或Permission Denied异常,并确认目标文件是否位于公共目录中。如果应用确实需要跨应用共享文件,应优先使用FileProvider或MediaStore,而不是直接暴露文件路径。另一个常见坑是权限申请:Android 11之后即便申请了WRITE_EXTERNAL_STORAGE也不会生效,代码里如果依赖该权限判断是否可写,可能会导致逻辑错误,应当改为检查系统版本并使用合适的API。
还有一类问题是文件写入后无法立即在系统相册或其他应用中看到。MediaStore插入或更新后,系统媒体扫描通常会自动进行,但某些设备或场景下可能需要触发媒体扫描。对于Android 10及以上,可以在插入后使用MediaScannerConnection.scanFile()通知系统扫描,或者直接依赖系统自动处理。另外,使用MediaStore查询时要注意部分字段在Android 10上才可用,例如RELATIVE_PATH,在低版本上需要回退到传统路径操作。为了避免兼容性问题,建议将存储操作封装成工具类,内部根据SDK版本分派到不同实现。
为了提前发现未适配的问题,可以使用Android Studio的Lint检查或手动在Android 10模拟器上运行应用。重点测试文件上传下载、拍照保存、附件打开、数据导入导出等功能。如果应用原来使用了requestLegacyExternalStorage="true"来临时绕过限制,在Android 11设备上测试时必须移除该属性,否则问题会再次暴露。最后,适配分区存储不仅是权限和API的替换,还需要重新审视产品的数据存储策略:哪些数据属于私有,哪些需要公开共享,哪些必须由用户显式选择位置。规划清楚后再动手修改代码,能减少反复调整的代价。
Android 10分区存储Scoped Storage存储适配修改时间:2026-10-04 14:31:19