Android 存储体系与分区存储-《Android深水区(十二)》

Android 存储体系与分区存储

前言

Android 存储最容易被误解成"有没有 SD 卡权限"。这个说法在今天已经不够准确:现代 Android 把存储拆成应用私有数据、应用专属外部目录、共享媒体、用户选择的文档 URI、数据库、偏好设置以及少数受严格政策约束的全文件访问能力。开发者真正要回答的问题不是"文件放哪里都能读写吗",而是:这份数据是谁的、是否要被其他应用看到、卸载后是否保留、用户是否明确选择、系统是否允许按路径访问。

分区存储(scoped storage)不是单个 API,而是一套把"共享外部存储"重新划边界的访问模型。它的目标是减少应用随意扫描、修改和泄露用户文件的机会,同时保留相机、相册、下载、文档编辑、文件管理器等真实场景的能力。本文从工程决策开始,串起私有目录、MediaStore、SAF、权限变化、源码路径和迁移策略,帮助你把"能跑的文件读写"升级成"符合当前 Android 隐私模型的存储设计"。

截至 2026-07-07,本文按 Android 16 / API 36 文档、本地 Android SDK API 36 framework sources 和 AOSP main 分支路径复查。存储、相册权限和 Google Play 全文件访问政策都属于高频变化区域,生产代码要以目标 SDK、设备版本和官方文档为准。

前置知识

建议先理解:

  • A023:Intent 与 PendingIntent,因为 SAF、照片选择器和系统设置页都通过 Intent 边界授予访问能力。
  • A027:Context 区别,因为 filesDircacheDirgetExternalFilesDir() 都从 Context 暴露。
  • A029:权限模型,因为 Android 13+ 媒体权限、Android 14 部分照片访问和 MANAGE_EXTERNAL_STORAGE 都建立在权限模型之上。

先记住一个实用判断表:

数据类型 首选方案 是否随卸载删除 是否需要存储权限 典型 API
登录态、配置、小型结构化数据 DataStore / Room / 内部文件 DataStoreRoomfilesDir
临时缓存 内部或外部 cache 是,且可能提前清理 cacheDirexternalCacheDir
应用独占的大文件 外部应用专属目录 Android 4.4+ 否 getExternalFilesDir()
用户希望在相册看到的图片/视频/音频 共享媒体集合 写入通常不需要,读取他人媒体按版本申请 MediaStore
PDF、Office、压缩包等用户选择文件 SAF 否,用户选择 URI ACTION_OPEN_DOCUMENTACTION_CREATE_DOCUMENT
文件管理器/备份/杀毒等核心全盘场景 全文件访问 需要特殊授权和政策理由 MANAGE_EXTERNAL_STORAGE

这张表比背"内部/外部存储"更有用。Android 官方文档也把应用专属文件、共享媒体、文档、偏好和数据库分别放在不同访问模型下,而不是鼓励所有数据都落到 /sdcard

核心概念

1. 内部存储不是"手机内置存储"的同义词

在 Android API 语义里,内部存储通常指应用私有目录,例如 context.filesDircontext.cacheDir。这些目录由系统按应用 UID 隔离,其他普通应用不能直接访问,适合 token、业务数据库、草稿、加密前的敏感数据和不应共享的业务文件。缺点是空间相对有限,且卸载应用时会随应用删除。

工程上要避免把"用户想导出的文件"长期塞进内部私有目录。如果文件要被用户带走或被其他应用打开,应通过 FileProvider 临时授权、MediaStore 写入共享媒体,或通过 SAF 让用户选择保存位置。

2. 外部存储不是"可插拔 SD 卡"的同义词

Android 设备上常见的 /sdcard 往往是 emulated external storage。它可能位于内部闪存上,但 API 语义仍属于外部存储。外部存储还包括可移除 SD 卡、USB OTG 等卷。外部应用专属目录通常形如:

text 复制代码
/storage/emulated/0/Android/data/<package>/files/
/storage/emulated/0/Android/data/<package>/cache/

这些目录适合应用独占的大文件、离线包、日志缓存、模型文件缓存等。Android 4.4 及以上访问自己的外部应用专属目录不需要存储权限,但文件不保证永远可用:可移除介质可能被拔出,用户或系统也可能清理空间。

3. 共享媒体应该走 MediaStore,而不是手写公共路径

图片、视频和音频属于共享媒体。现代 Android 推荐通过 MediaStore.ImagesMediaStore.VideoMediaStore.Audio 插入和查询,而不是拼接 /sdcard/DCIM/xxx.jpg。写入新媒体时要使用 DISPLAY_NAMEMIME_TYPERELATIVE_PATHIS_PENDING 等列;官方文档明确提醒,创建或更新媒体文件时不要依赖 DATA 列,而应使用 DISPLAY_NAMERELATIVE_PATH

分区存储之后,MediaStore 实际扮演了"共享媒体索引 + 访问仲裁"的角色。应用能直接管理自己创建的媒体;要修改、删除不属于自己的媒体,则通常需要用户确认,或使用 MediaStore.createWriteRequest()createDeleteRequest() 这样的请求流。

4. 文档和任意文件优先 SAF

PDF、ZIP、日志导出、Office 文档这类"不属于相册媒体集合"的用户文件,首选 Storage Access Framework。它通过系统文件选择器让用户选择具体文件或目录,应用拿到的是 content:// URI,而不是永久全盘路径。用户参与选择后,应用无需申请传统存储权限;如果需要跨重启长期访问,要配合 takePersistableUriPermission() 保存 URI 授权。

SAF 的关键不是"文件选择器 UI",而是用户把某个具体资源授权给某个应用。这个模型比全盘扫描更符合隐私,也更适合云盘、文档提供者和跨设备文件系统。

5. 分区存储改变的是共享外部存储边界

Android 10 引入分区存储,Android 11 开始对 target 30+ 忽略 requestLegacyExternalStorage。这意味着历史上"申请 READ/WRITE_EXTERNAL_STORAGE 后遍历 /sdcard"的模式不能作为新应用的默认设计。今天的正确模型是:

  • 应用私有数据放内部目录或应用专属目录。
  • 用户可见媒体通过 MediaStore 写入或读取。
  • 非媒体文档通过 SAF 由用户选择。
  • 全文件访问只给文件管理器、备份、杀毒等核心场景,并受 Google Play 政策约束。

整体架构

flowchart TD App[&#34;应用业务层&#34;] Private[&#34;应用私有数据\nfilesDir / cacheDir / Room / DataStore&#34;] AppSpecific[&#34;外部应用专属目录\ngetExternalFilesDir&#34;] Media[&#34;共享媒体\nMediaStore&#34;] Docs[&#34;用户选择文档\nSAF content URI&#34;] AllFiles[&#34;全文件访问\nMANAGE_EXTERNAL_STORAGE&#34;] Framework[&#34;Framework API\nContext / ContentResolver / Environment&#34;] Providers[&#34;系统 Provider\nMediaProvider / DocumentsProvider&#34;] StorageSvc[&#34;系统服务与存储栈\nStorageManagerService / vold / FUSE&#34;] User[&#34;用户确认或系统权限&#34;] App --> Private App --> AppSpecific App --> Media App --> Docs App --> AllFiles Private --> Framework AppSpecific --> Framework Media --> Framework Docs --> Framework AllFiles --> Framework Framework --> Providers Framework --> StorageSvc User --> Media User --> Docs User --> AllFiles

可以把 Android 存储看成三道门:

  1. 应用沙箱门:私有目录和数据库默认只服务当前 UID。
  2. Provider 门:共享媒体和文档通过 ContentResolverMediaProviderDocumentsProvider 仲裁。
  3. 权限/用户确认门:读取他人媒体、编辑他人媒体、选择文件、全文件访问,都需要用户或系统策略参与。

工作流程

写入一张用户可见图片

sequenceDiagram participant App as App participant CR as ContentResolver participant MS as MediaStore participant MP as MediaProvider participant FS as Shared Storage App->>CR: insert(collection, DISPLAY_NAME/MIME_TYPE/RELATIVE_PATH/IS_PENDING=1) CR->>MS: route to media URI MS->>MP: create pending media row MP-->>App: content://media/.../id App->>CR: openOutputStream(uri) App->>FS: write bytes App->>CR: update(IS_PENDING=0) MP->>FS: expose media to other apps/index

这里有两个容易忽略的点:

  • 插入新媒体不等于立即对外可见。IS_PENDING=1 期间适合写入未完成文件,写完后再改为 0。
  • 对媒体使用 content:// URI 是正常路径,不要为了"拿真实路径"而绕回 DATA 列。

读取用户选择的文档

SAF 流程更像"拿授权票据":

  1. 应用发起 ACTION_OPEN_DOCUMENTACTION_CREATE_DOCUMENT
  2. 用户在系统 UI 中选择文件或保存位置。
  3. 应用收到 content:// URI。
  4. 应用通过 ContentResolver.openInputStream()openOutputStream() 读写。
  5. 如果需要长期访问,保存 URI 并调用 takePersistableUriPermission()

这个流程的权限粒度是 URI,不是目录路径。即使底层文件来自本地、云盘或企业文档系统,应用也只应该通过 URI 协议访问。

迁移 legacy 外部存储

老项目升级 target SDK 时,迁移不要从"把权限补齐"开始,而要先做资产盘点:

旧路径/旧行为 迁移方向 说明
/sdcard/<app>/cache externalCacheDir 可清理缓存,不承诺长期存在
/sdcard/<app>/files getExternalFilesDir() 应用独占、随卸载删除
/sdcard/DCIM 手写图片 MediaStore.Images 用户可见媒体,使用 RELATIVE_PATH
/sdcard/Download/*.pdf SAF 或 MediaStore.Downloads 根据是否由用户选择和平台版本决策
扫描全盘找文件 SAF 目录授权或业务侧导入 不要默认全盘扫描
文件管理核心能力 MANAGE_EXTERNAL_STORAGE 只在确为核心功能且符合政策时申请

API 使用

1. 写入私有文件

kotlin 复制代码
fun savePrivateDraft(context: Context, articleId: String, body: String) {
    val file = File(context.filesDir, "draft-$articleId.md")
    file.parentFile?.mkdirs()
    file.writeText(body, Charsets.UTF_8)
}

fun readPrivateDraft(context: Context, articleId: String): String? {
    val file = File(context.filesDir, "draft-$articleId.md")
    return file.takeIf { it.exists() }?.readText(Charsets.UTF_8)
}

私有文件适合业务内部状态,但不适合直接把 File 暴露给其他应用。要分享内部文件,应通过 FileProvider 生成临时 content:// URI,并加上 FLAG_GRANT_READ_URI_PERMISSION

2. 写入共享图片到相册

kotlin 复制代码
suspend fun savePngToPictures(
    context: Context,
    displayName: String,
    bytes: ByteArray,
): Uri = withContext(Dispatchers.IO) {
    val resolver = context.contentResolver
    val values = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, displayName)
        put(MediaStore.Images.Media.MIME_TYPE, "image/png")
        put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/StorageDemo")
        put(MediaStore.Images.Media.IS_PENDING, 1)
    }

    val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
    val uri = resolver.insert(collection, values)
        ?: error("Create MediaStore row failed")

    try {
        resolver.openOutputStream(uri, "w")?.use { output ->
            output.write(bytes)
        } ?: error("Open output stream failed")

        values.clear()
        values.put(MediaStore.Images.Media.IS_PENDING, 0)
        resolver.update(uri, values, null, null)
        uri
    } catch (t: Throwable) {
        resolver.delete(uri, null, null)
        throw t
    }
}

这个示例有三个生产要点:

  • I/O 放到 Dispatchers.IO,避免阻塞主线程。
  • 先设置 IS_PENDING=1,写入成功后再公开。
  • 写入失败要删除半成品记录,避免相册里出现损坏条目。

Android 10 及以上,如果应用只是向共享存储添加文件,通常不需要申请存储权限。只有读取其他应用已有媒体、管理他人媒体或访问更宽范围时,才进入权限和用户确认流程。

3. 使用 Photo Picker 获取图片

kotlin 复制代码
class AvatarActivity : ComponentActivity() {
    private val pickImage = registerForActivityResult(
        ActivityResultContracts.PickVisualMedia()
    ) { uri: Uri? ->
        if (uri != null) {
            renderAvatar(uri)
        }
    }

    fun onChangeAvatarClicked() {
        pickImage.launch(
            PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
        )
    }
}

如果需求只是让用户选一张或几张照片,Photo Picker 往往比申请媒体读取权限更好。它让用户明确选择资源,应用无需持有整个相册的读取权限。

4. 使用 SAF 打开文档

kotlin 复制代码
class ImportActivity : ComponentActivity() {
    private val openDocument = registerForActivityResult(
        ActivityResultContracts.OpenDocument()
    ) { uri: Uri? ->
        if (uri != null) {
            contentResolver.takePersistableUriPermission(
                uri,
                Intent.FLAG_GRANT_READ_URI_PERMISSION,
            )
            importDocument(uri)
        }
    }

    fun onImportClicked() {
        openDocument.launch(arrayOf("application/pdf", "text/plain"))
    }

    private fun importDocument(uri: Uri) {
        contentResolver.openInputStream(uri)?.use { input ->
            // 解析业务文件,注意不要假设它来自本地路径。
        }
    }
}

SAF 代码里不要把 Uri.path 当作文件系统路径。正确做法是通过 ContentResolver 读写流,并把 URI 当成长期资源标识。

5. Android 13/14 媒体读取权限

kotlin 复制代码
fun mediaReadPermissions(): Array<String> = when {
    Build.VERSION.SDK_INT >= 34 -> arrayOf(
        Manifest.permission.READ_MEDIA_IMAGES,
        Manifest.permission.READ_MEDIA_VIDEO,
        Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED,
    )
    Build.VERSION.SDK_INT >= 33 -> arrayOf(
        Manifest.permission.READ_MEDIA_IMAGES,
        Manifest.permission.READ_MEDIA_VIDEO,
    )
    else -> arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
}

Android 13 把图片、视频、音频读取拆成更细的 READ_MEDIA_* 权限;Android 14 引入照片/视频部分访问。如果应用自己维护相册选择器,就要处理"全量、部分、拒绝"三种状态;如果只是选图,优先用 Photo Picker,减少权限面。

源码分析

Framework API 层

本地 Android SDK API 36 sources 中可以看到几类入口:

源码路径 关注点 代表 API/字段
frameworks/base/core/java/android/content/Context.java 暴露应用目录入口 getFilesDir()getCacheDir()getExternalFilesDir()getExternalFilesDirs()
frameworks/base/core/java/android/os/Environment.java 定义公共目录、外部存储状态和全文件访问检查 DIRECTORY_DOCUMENTSgetExternalStoragePublicDirectory()isExternalStorageManager()
frameworks/base/core/java/android/provider/MediaStore.java 共享媒体集合和写入请求 RELATIVE_PATHIS_PENDINGcreateWriteRequest()createDeleteRequest()

Context 是应用拿目录的门面;Environment 保留了很多历史公共目录 API,但它的注释和官方文档都倾向引导应用使用应用专属目录、MediaStore 或 SAF;MediaStore 则把共享媒体从"路径集合"提升为"带元数据和权限仲裁的 Provider 表"。

MediaStore 与 MediaProvider

MediaStore 位于 framework API 层,开发者通过它构建 URI、插入元数据、打开流、发起用户确认请求。真正维护媒体数据库和文件映射的实现位于 AOSP packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java

典型写入链路可以概括为:

text 复制代码
App -> ContentResolver.insert() -> MediaStore collection URI
    -> MediaProvider 插入媒体行 -> 返回 content URI
    -> ContentResolver.openOutputStream() 写入文件
    -> update(IS_PENDING=0) 公开媒体

这个链路解释了为什么 IS_PENDING 是媒体写入的关键列:它让应用在文件尚未完整写入时先占位,避免其他应用索引到半成品。RELATIVE_PATH 则表达媒体在共享集合中的相对位置,替代旧式绝对路径拼接。

SAF 与 DocumentsProvider

SAF 的 framework 入口主要是 Intent action、DocumentsContractContentResolver。系统文件选择器背后接入多个 DocumentsProvider:本机外部存储、Downloads、云盘、企业文档提供者都可以成为 provider。应用拿到的 URI 是 provider 授权的能力,而不是底层文件路径。

这也解释了两个常见现象:

  • 同一个 URI 可能没有真实本地路径,或者真实路径对应用不可见。
  • 读写性能取决于 provider,可能是本地文件,也可能是网络或加密存储。

StorageManagerService 与底层存储栈

frameworks/base/services/core/java/com/android/server/StorageManagerService.java 负责更底层的卷、挂载、用户存储和系统服务协调。普通应用不直接调用这个服务,而是通过 ContextStorageManagerContentResolverMediaStore、SAF 等稳定 API 间接使用存储能力。

在 Android 10 之后,外部存储访问还涉及 FUSE/sdcardfs 演进、UID 隔离、AppOps、Provider 检查等多个层面。对应用开发者来说,重要的是不要把底层挂载细节当成业务契约;路径能在一台设备上访问,不代表在目标 SDK、厂商 ROM 或未来 Android 版本上仍可访问。

线程模型

存储 I/O 不属于主线程工作。无论是 FileContentResolver.openInputStream()MediaStore 写入,还是 SAF provider 读写,都可能触发磁盘、数据库、跨进程甚至网络访问。推荐原则:

  • ViewModel 或 Repository 中用 Dispatchers.IO 包裹阻塞 I/O。
  • UI 只持有 URI、加载状态和业务状态,不直接在 Composable 中读大文件。
  • 大文件复制要支持取消、进度和失败清理。
  • content:// 流不要假设可重复读取,必要时复制到应用缓存再处理。

实战案例

场景:图片编辑器保存作品

需求:用户从相册选一张图,应用编辑后保存结果,并允许在系统相册看到。

推荐设计:

  1. 用 Photo Picker 让用户选择源图,避免申请全量相册读取权限。
  2. 将中间编辑状态保存到内部 cache 或 externalCacheDir
  3. 用户点击保存时,通过 MediaStore.Images 写入 Pictures/<AppName>
  4. 写入时使用 IS_PENDING,失败时删除半成品。
  5. 如果要覆盖原图,不直接写原 URI;使用 MediaStore.createWriteRequest() 让用户确认,或另存为新文件。

这个设计的优点是权限面小、失败边界清晰、作品能被相册看到,也不会把中间缓存暴露给其他应用。

场景:企业应用导入 PDF

需求:用户从本机或云盘导入合同 PDF,应用解析并保存业务记录。

推荐设计:

  1. ACTION_OPEN_DOCUMENTActivityResultContracts.OpenDocument() 选择 PDF。
  2. 调用 takePersistableUriPermission() 保存读取授权。
  3. 解析时通过 ContentResolver.openInputStream() 读取。
  4. 若业务需要离线长期处理,复制一份到内部私有目录,并在数据库记录原始 URI 和导入时间。
  5. 不申请 MANAGE_EXTERNAL_STORAGE,不扫描 /sdcard/Download

这个设计尊重用户选择,也兼容云盘和企业文档 provider。

场景:旧应用升级 target 30+

迁移顺序建议:

  1. 扫描代码里的硬编码 /sdcardEnvironment.getExternalStorageDirectory()DATA 列依赖。
  2. 给每类文件标注归属:私有、缓存、共享媒体、用户文档、全文件核心能力。
  3. 先迁移写入路径,再迁移读取路径。
  4. 对历史文件做一次性搬迁,搬迁完成后记录版本标记,避免每次启动扫描。
  5. 目标 SDK 升级前用真实设备覆盖 Android 10、11、13、14+ 权限和 URI 行为。

性能优化

1. 少做全盘扫描

全盘扫描在分区存储下既不稳定,也很慢。即便拿到了宽权限,遍历大量文件也会消耗 I/O、触发 provider 查询和电量成本。更好的做法是:

  • 媒体用 MediaStore 条件查询,只取需要列。
  • 文档让用户通过 SAF 选择。
  • 业务文件维护自己的索引表,而不是每次启动扫目录。
  • 大目录分页加载,避免一次性把所有 URI 和缩略图塞进内存。

2. 对 MediaStore 查询做投影和排序

查询共享媒体时,不要 projection = null。只取 _IDDISPLAY_NAMEMIME_TYPEDATE_ADDEDSIZE 等需要字段。缩略图交给图片加载库通过 URI 懒加载,不要在查询循环中同步解码大图。

kotlin 复制代码
val projection = arrayOf(
    MediaStore.Images.Media._ID,
    MediaStore.Images.Media.DISPLAY_NAME,
    MediaStore.Images.Media.DATE_ADDED,
)
val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"
context.contentResolver.query(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    projection,
    null,
    null,
    sortOrder,
)?.use { cursor ->
    // 分批映射到轻量模型,UI 层再按需加载缩略图。
}

3. 写入大文件要可恢复

大文件写入共享存储时,至少处理:

  • 写入失败删除 pending 项。
  • 低空间错误提示用户清理空间。
  • 复制过程支持取消。
  • 进程死亡后能识别未完成任务并清理或重试。

4. 缓存有上限

cacheDirexternalCacheDir 都不是永久存储。系统可能在空间不足时清理缓存,应用也应该主动维护 LRU 或总大小限制。把关键业务数据放 cache 是线上丢数据的常见根因。

常见问题

Q1:Android 10 之后还需要 WRITE_EXTERNAL_STORAGE 吗?

大多数新写入共享媒体的场景不需要。Android 10 及以上向共享存储添加文件通常可以通过 MediaStore 完成,不应为了保存图片或 PDF 默认申请传统写权限。读取其他应用已有媒体则要按版本处理读取权限或使用 Photo Picker。

Q2:为什么 File(uri.path) 读不到 SAF 选中的文件?

因为 SAF 返回的是 content:// URI,path 不是你的应用可访问的本地文件路径。正确方式是用 ContentResolver.openInputStream(uri)openFileDescriptor(uri, mode)

Q3:getExternalFilesDir() 里的文件会被相册看到吗?

通常不应把它当作用户共享媒体目录。外部应用专属目录服务当前应用,卸载后会删除。用户希望长期保留并在相册看到的内容,应写入 MediaStore 的图片、视频或音频集合。

Q4:什么情况下才考虑 MANAGE_EXTERNAL_STORAGE

只有当应用核心功能必须管理大量共享文件,且 SAF、MediaStore 等隐私友好 API 无法满足时才考虑,例如文件管理器、备份恢复、杀毒、安全审计等。即使系统允许申请,Google Play 也会按政策审核使用场景。

Q5:能不能继续用 requestLegacyExternalStorage

不能把它作为长期方案。Android 11 起,当应用 target Android 11/API 30 及以上时,系统会忽略该标记。它只适合作为 Android 10 兼容迁移阶段的临时缓冲。

Q6:MediaStore 的 DATA 列还能用吗?

读取已有媒体时某些设备上仍可能返回路径,但不应把它作为创建、更新或跨版本兼容的基础。官方文档明确建议创建或更新媒体时使用 DISPLAY_NAMERELATIVE_PATH

Q7:部分照片访问会影响相册列表吗?

会。Android 14 的 Selected Photos Access 允许用户只授权部分图片/视频。应用不能把"查不到"直接解释为相册为空,应该展示当前授权范围,并提供重新选择入口。

面试考点

  1. Android 内部存储和外部存储在 API 语义上分别指什么?它们和物理内置存储、SD 卡是什么关系?
  2. filesDircacheDirgetExternalFilesDir()MediaStore、SAF 分别适合什么数据?
  3. 分区存储解决了什么问题?为什么不能再依赖 /sdcard 全路径扫描?
  4. Android 11 对 requestLegacyExternalStorage 的处理是什么?迁移 legacy 存储时怎么做资产盘点?
  5. 向相册保存图片时为什么建议使用 IS_PENDING?失败时如何清理?
  6. MediaStoreRELATIVE_PATH 和旧式 DATA 列有什么区别?
  7. Photo Picker、媒体读取权限和 SAF 的使用边界是什么?
  8. Android 13/14 对图片视频读取权限有哪些变化?部分照片访问要如何设计 UI?
  9. MANAGE_EXTERNAL_STORAGE 能做什么?为什么仍不应该作为普通业务功能的默认方案?
  10. 为什么所有文件 I/O 都不应该放在主线程?content:// URI 还可能带来哪些性能不确定性?

总结

Android 存储设计的核心不是"找一个还能写的路径",而是给每类数据选择正确边界:

  • 私有业务数据放内部目录、Room 或 DataStore。
  • 应用独占的大文件放外部应用专属目录。
  • 用户可见媒体通过 MediaStore
  • 用户选择的文档通过 SAF。
  • 选图优先 Photo Picker。
  • 全文件访问只留给少数核心场景。

分区存储把历史上的公共外部存储从"应用共享硬盘"变成"按数据类型、用户选择和系统策略授权的资源集合"。当你按数据归属建模,权限弹窗会更少,兼容性会更好,迁移 target SDK 时也不会被路径假设拖住。

扩展阅读

相关推荐
会编程的吕洞宾1 小时前
DeepAgents In Action学习(Second)
android·java·学习
太子釢2 小时前
用 AI 完整实现 Github 客户端:基于 KMP + SwiftUI + Compose
android·人工智能·ios
汪海游龙2 小时前
本地源码还是 Maven 版本?Gradle composite build 双轨依赖的正确接法
android·ci/cd·kotlin
k3x1n2 小时前
三星安卓系统BUG排查记录:云闪付、铁路12306、个人所得税、中国移动等App,长期闪退的根本原因分析
android·逆向
网安蟹佬霸4 小时前
CTF Web方向解题全攻略:从信息收集到getshell(附实战payload与脚本)
android·前端·网络·安全·web安全·网络安全·网安
又见情义4 小时前
Android 13 Settings 设置界面选项裁剪(RK3568平台)
android
人才瘾大5 小时前
Android录屏内部音频录制完全指南:原理、限制与方案选型
android·音视频
网安蟹佬霸6 小时前
Webshell免杀与检测实战:从一句话木马到加密流量
android·安全·web安全·网络安全·黑客·网安·渗透测试·
阿巴斯甜8 小时前
Android SharedMemory 详细使用
android