一、旧时代的终结:为什么需要存储革命?
1. 隐私泄露的"黑暗时代"
-
案例: 某应用在用户授予存储权限后,扫描整个SD卡窃取照片和文档
-
系统缺陷:
READ_EXTERNAL_STORAGE权限如同"万能钥匙"- 应用可无限制访问所有用户文件(包括其他应用的文件)
-
用户代价: 私人照片、财务文档、聊天记录面临泄露风险
2. 存储空间的"垃圾场困境"
3. 混乱的访问方式
- 开发者过度依赖
Environment.getExternalStorageDirectory() - 媒体文件存储位置混乱(DCIM、Pictures、自定义目录混用)
- 文件命名冲突频繁("image.jpg"在多个目录重复出现)
4. 权限机制的失效
- 用户面临"全有或全无"的选择:要么授予全部存储权限,要么放弃使用应用
- 权限请求与功能不匹配(如计算器应用请求照片访问权限)
二、分区存储的核心设计哲学
1. 三大核心目标
2. 四大基本原则
原则1:应用私有沙箱(Sandboxing)
-
安全区: 每个应用拥有专属存储空间
-
位置:
- 内部存储:
/data/data/<package> - 外部存储:
/sdcard/Android/data/<package>和/sdcard/Android/media/<package>
- 内部存储:
-
特权: 自由读写,无需权限
-
生命周期: 卸载自动清理
原则2:公共资源标准化访问
| 资源类型 | 访问方式 | 技术实现 |
|---|---|---|
| 媒体文件 | 媒体库接口 | MediaStore API |
| 文档文件 | 用户选择 | 存储访问框架(SAF) |
| 下载文件 | 下载管理器 | DownloadManager |
原则3:文件归属透明化
- 元数据记录: 系统追踪文件创建者
- 卸载清理: 明确区分"应用文件"和"用户文件"
- 视觉标识: 文件管理器显示应用专属目录
原则4:限制直接路径访问
- 禁止: 直接使用
/sdcard/路径访问公共区域 - 替代: 使用
content://URI 替代file://路径 - 例外: 应用专属目录仍可直接访问
三、版本适配路线图
1. Android 10(API 29):温和过渡期
<!-- AndroidManifest.xml -->
<application
android:requestLegacyExternalStorage="true"
... >
-
作用: 临时禁用分区存储
-
限制: 仅对 targetSDK=29 有效
-
策略:
- 添加标志位保持旧行为
- 同时开发适配代码
- 测试两种模式下的行为
2. Android 11(API 30):强制执行期
-
重大变化:
requestLegacyExternalStorage失效- 引入
preserveLegacyStorage迁移标志
<application
android:preserveLegacyExternalStorage="true"
... >
- 迁移策略:
3. Android 12+(API 31+):优化完善期
-
精细化媒体权限:
READ_MEDIA_IMAGESREAD_MEDIA_VIDEOREAD_MEDIA_AUDIO
-
改进 SAF: 更好的目录访问体验
-
存储统计API: 让用户清楚应用存储占用
四、分区存储下的访问模式对比
1. 媒体文件访问(图片/视频/音频)
| 操作 | 旧模式 | 分区存储模式 |
|---|---|---|
| 查询 | File.listFiles() | ContentResolver.query() |
| 读取 | FileInputStream | ContentResolver.openInputStream |
| 插入 | new File(path).create() | MediaStore.createWriteRequest() |
| 删除 | File.delete() | MediaStore.createDeleteRequest() |
| 更新 | 直接覆盖文件 | 需用户授权或使用SAF |
2. 文档文件访问(PDF/DOC/XLS等)
存储访问框架(SAF)工作流程:
SAF 优势:
- 无需请求存储权限
- 可访问云存储文件
- 用户精确控制访问范围
五、开发者适配策略
1. 文件存储位置决策树
2. 适配四步法
-
迁移现有文件
// 将公共目录文件移动到应用专属目录 File oldFile = new File(Environment.getExternalStorageDirectory(), "old.jpg"); File newFile = new File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), "new.jpg"); oldFile.renameTo(newFile); -
替换文件访问API // 旧方式(废弃) val file = File("/sdcard/Pictures/photo.jpg")
// 新方式 val projection = arrayOf(MediaStore.Images.Media._ID) val selection = "${MediaStore.Images.Media.DISPLAY_NAME} = ?" val selectionArgs = arrayOf("photo.jpg") contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, selection, selectionArgs, null )?.use { cursor -> if (cursor.moveToFirst()) { val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) // 使用uri访问文件 } } -
处理URI转换
// 在AndroidManifest.xml中声明FileProvider <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider> // 创建file_paths.xml <paths> <external-files-path name="my_images" path="Pictures/" /> </paths> -
多版本兼容处理
fun saveImage(context: Context, bitmap: Bitmap) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // 使用MediaStore保存 saveViaMediaStore(context, bitmap) } else { // 旧方式保存到应用专属目录 saveToAppDirectory(context, bitmap) } }
六、特殊场景解决方案
1. 应用卸载后保留用户文件
方案:
2. 批量处理媒体文件
Android 11+ 解决方案:
// 创建待删除文件URI集合
List<Uri> urisToDelete = new ArrayList<>();
urisToDelete.add(uri1);
urisToDelete.add(uri2);
// 创建删除请求
PendingIntent deleteIntent = MediaStore.createDeleteRequest(
getContentResolver(),
urisToDelete
);
// 启动用户确认
startIntentSenderForResult(deleteIntent.getIntentSender(),
REQUEST_CODE_DELETE, null, 0, 0, 0);
3. 处理文件路径依赖的第三方库
适配策略:
-
封装层方案:
public Uri getFileUri(String filename) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // 使用MediaStore获取URI return getMediaStoreUri(filename); } else { // 返回file:// URI return Uri.fromFile(new File(getExternalDir(), filename)); } } -
存储访问框架方案: fun selectFolderForLibrary(context: Activity) { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION } context.startActivityForResult(intent, REQUEST_CODE_FOLDER) }
// 在onActivityResult中获取URI
总结
分区存储不是限制,而是 Android 存储生态的进化。
- 强化隐私保护(用户数据与应用数据隔离)
- 建立存储秩序(明确文件归属)
- 统一访问路径(MediaStore/SAF标准化接口)