深入浅出 Android 存储(三):分区存储革命篇 - 理解变革与核心原则

212 阅读4分钟

一、旧时代的终结:为什么需要存储革命?

1. 隐私泄露的"黑暗时代"

  • 案例:  某应用在用户授予存储权限后,扫描整个SD卡窃取照片和文档

  • 系统缺陷:

    • READ_EXTERNAL_STORAGE 权限如同"万能钥匙"
    • 应用可无限制访问所有用户文件(包括其他应用的文件)
  • 用户代价:  私人照片、财务文档、聊天记录面临泄露风险

2. 存储空间的"垃圾场困境"

deepseek_mermaid_20250630_5fe2b2.png

3. 混乱的访问方式

  • 开发者过度依赖 Environment.getExternalStorageDirectory()
  • 媒体文件存储位置混乱(DCIM、Pictures、自定义目录混用)
  • 文件命名冲突频繁("image.jpg"在多个目录重复出现)

4. 权限机制的失效

  • 用户面临"全有或全无"的选择:要么授予全部存储权限,要么放弃使用应用
  • 权限请求与功能不匹配(如计算器应用请求照片访问权限)

二、分区存储的核心设计哲学

1. 三大核心目标

deepseek_mermaid_20250630_bb92e2.png

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 有效

  • 策略:

    1. 添加标志位保持旧行为
    2. 同时开发适配代码
    3. 测试两种模式下的行为

2. Android 11(API 30):强制执行期

  • 重大变化:

    • requestLegacyExternalStorage 失效
    • 引入 preserveLegacyStorage 迁移标志
<application
    android:preserveLegacyExternalStorage="true"
    ... >
  • 迁移策略:

deepseek_mermaid_20250630_a23871.png

3. Android 12+(API 31+):优化完善期

  • 精细化媒体权限:

    • READ_MEDIA_IMAGES
    • READ_MEDIA_VIDEO
    • READ_MEDIA_AUDIO
  • 改进 SAF:  更好的目录访问体验

  • 存储统计API:  让用户清楚应用存储占用

四、分区存储下的访问模式对比

1. 媒体文件访问(图片/视频/音频)

操作旧模式分区存储模式
查询File.listFiles()ContentResolver.query()
读取FileInputStreamContentResolver.openInputStream
插入new File(path).create()MediaStore.createWriteRequest()
删除File.delete()MediaStore.createDeleteRequest()
更新直接覆盖文件需用户授权或使用SAF

2. 文档文件访问(PDF/DOC/XLS等)

存储访问框架(SAF)工作流程:

deepseek_mermaid_20250630_a9e4ef.png SAF 优势:

  • 无需请求存储权限
  • 可访问云存储文件
  • 用户精确控制访问范围

五、开发者适配策略

1. 文件存储位置决策树

deepseek_mermaid_20250630_d03285.png

2. 适配四步法

  1. 迁移现有文件

    // 将公共目录文件移动到应用专属目录
    File oldFile = new File(Environment.getExternalStorageDirectory(), "old.jpg");
    File newFile = new File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), "new.jpg");
    oldFile.renameTo(newFile);
    
  2. 替换文件访问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访问文件
        }
    }
    
  3. 处理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>
    
  4. 多版本兼容处理

    fun saveImage(context: Context, bitmap: Bitmap) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            // 使用MediaStore保存
            saveViaMediaStore(context, bitmap)
        } else {
            // 旧方式保存到应用专属目录
            saveToAppDirectory(context, bitmap)
        }
    }
    

六、特殊场景解决方案

1. 应用卸载后保留用户文件

方案:

deepseek_mermaid_20250630_d636e0.png

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. 处理文件路径依赖的第三方库

适配策略:

  1. 封装层方案:

    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));
        }
    }
    
  2. 存储访问框架方案: 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标准化接口)