深入浅出 Android 存储(一):基石篇 - 核心概念与分区揭秘

688 阅读9分钟

想象一下,你的手机:

  1. 存储空间有限且多样:  有高速但容量较小的内置芯片(类似电脑 SSD),也可能有低速但容量可扩展的 SD 卡(如今越来越少)。
  2. 应用来自五湖四海:  无数开发者编写的应用需要读写文件。
  3. 用户隐私至关重要:  你的照片、文档、聊天记录不能被其他应用随意窥探。
  4. 设备碎片化严重:  不同厂商、不同 Android 版本对存储的实现可能有差异。

Android 如何应对?

Android 设计了一套基于权限和分区隔离的存储系统,核心目标在于:

  • 应用隔离 (Sandboxing):  每个应用默认拥有自己的私有存储空间,其他应用无法直接访问(除非明确授权)。
  • 用户数据保护:  限制应用随意访问用户的公共文件(如图片、音乐),需要用户授权。
  • 减少混乱:  避免应用在公共区域随意创建文件夹,造成用户存储空间混乱不堪。
  • 兼容性与灵活性:  既要支持内置存储,也要兼容(逐渐式微的)物理 SD 卡。

理解这套系统的第一步,就是认清存储分区

二、 核心分区详解:内部存储 vs. 外部存储

这是最容易产生混淆的地方!请务必记住:

  • 内部存储 (Internal Storage) 和 外部存储 (External Storage) 的划分,主要基于其物理位置和可移除性,而非“重要性”或“隐私性”。
  • 在现代 Android 设备(尤其无 SD 卡槽的设备)上,“外部存储”通常指的是设备内置的、用于用户数据的共享分区,它并不是一块物理上可拔出的卡!

1. 内部存储 (Internal Storage) - 应用的“闺房”

  • 位置:  /data/data/<你的应用包名>/ (例如 /data/data/com.example.myapp/)

  • 核心特点:

    • 高度私有 (Private):  这个目录及其所有子目录 (filescachedatabasesshared_prefs 等) 默认仅属于你的应用。其他应用(以及普通用户,如果没有 root)无法访问。系统通过 Linux 文件权限严格保证了这一点。
    • 空间较小但速度快:  通常使用设备上速度最快的存储芯片(类似 SSD),但容量相对较小,主要用于存放应用核心数据。
    • 卸载即清理:  当用户卸载你的应用时,系统会自动删除整个 /data/data/<package_name>/ 目录!这是保证用户存储空间整洁的关键机制。
    • 无需权限:  应用读写自己内部存储的文件不需要申请任何运行时存储权限 (READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 等)。
  • 关键 API 获取路径:

    • 存放普通文件:  Context.getFilesDir() -> 返回 /data/data/<package_name>/files/

    • 存放缓存文件:  Context.getCacheDir() -> 返回 /data/data/<package_name>/cache/ (系统可能在存储空间不足时清理这里,应用自身也应管理缓存)

    • 存放数据库:  Context.getDatabasePath("mydb.db") -> 返回 /data/data/<package_name>/databases/mydb.db

    • 读写文件流:

      • FileOutputStream fos = openFileOutput("myfile.txt", Context.MODE_PRIVATE); (写入 files/ 目录)
      • FileInputStream fis = openFileInput("myfile.txt"); (从 files/ 目录读取)
  • 总结:  内部存储是应用最安全、最私密的“自留地”,用于存放绝对不能丢失绝对不能泄露的核心数据(如用户登录 token、核心配置文件、小型数据库)。空间宝贵,请珍惜使用。

2. 外部存储 (External Storage) - 共享的“大仓库”与应用的“外院”

概念演变:  早期 Android 设备普遍支持物理 SD 卡,那时“外部存储”指的就是这张卡。现代设备(尤其主流旗舰机)大多取消了 SD 卡槽,“外部存储”转而指设备内置的一块专门用于存放用户数据(音乐、照片、下载文件等)的共享存储分区。  它在文件系统中通常挂载在 /storage/emulated/0/ (或用户看到的 /sdcard/)。这不再是物理卡,而是内置存储的一部分!

外部存储包含两个关键区域:

a. 公共区域 (Public Directories) - 共享的“广场”

  • 位置:  /storage/emulated/0/ (/sdcard/) 下的标准公共目录,例如:

    • Music/
    • Pictures/ (包含 DCIM/Screenshots/ 等子目录)
    • Downloads/
    • Documents/
    • Movies/
    • Ringtones/
    • Podcasts/
    • Alarms/
    • Notifications/
  • 核心特点:

    • 用户可见且共享 (Shared):  这些目录及其内容对所有应用(有权限时)和用户(通过文件管理器)都是可见和可访问的。用户期望在这里找到他们的照片、音乐等。

    • 持久化:  应用在公共区域创建的文件,默认在应用卸载后会被保留。这既是优点(用户数据不丢失)也是缺点(可能遗留垃圾文件)。

    • 访问受控 (分区存储):  Android 10 (API 29) 引入的分区存储 (Scoped Storage) 彻底改变了应用访问这些公共目录的方式:

      • 直接文件路径访问受限:  试图使用 new File("/sdcard/Pictures/myphoto.jpg") 或 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) (已弃用) 来读写其他应用或用户创建的文件,在 Android 10+ 上会受到严格限制,通常无法成功。
      • 官方访问途径:  必须通过 MediaStore API(访问媒体文件如图片、视频、音频)或 Storage Access Framework (SAF)(用户主动选择文件/目录)来访问公共区域。权限要求也随版本变化(详见后续权限篇)。
  • 总结:  公共区域是用户存放共享文件的“大广场”。应用若需读写这里的文件(尤其是非自身创建的文件),必须遵守分区存储规则(MediaStore / SAF)并申请相应权限。避免直接使用文件路径 (File API) 操作公共目录!

b. 应用专属外部存储 (App-Specific External Storage) - 应用的“私家花园”

  • 位置:

    • /storage/emulated/0/Android/data/<你的应用包名>/ (例如 /storage/emulated/0/Android/data/com.example.myapp/)
    • (Android 11+ 对于媒体文件) /storage/emulated/0/Android/media/<你的应用包名>/
  • 核心特点:

    • 应用私有 (Private):  虽然位于外部存储分区(/sdcard/下),但这个目录默认仅属于你的应用。其他应用无法直接访问(无权限)。系统同样通过文件权限隔离。

    • 用户可访问:  用户可以通过系统自带的文件管理器或第三方文件管理器,导航到 Android/data/ 或 Android/media/ 目录,看到并操作(复制、删除)你应用存放在这里的文件。这是它与内部存储 (/data/data/) 的一个重要区别!

    • 空间较大:  通常位于设备上容量更大的用户数据分区,适合存放应用需要的大文件(如离线地图、缓存视频、录音文件、非敏感日志等)。

    • 无需权限 (关键!):  在 Android 4.4 (API 19) 及更高版本上,应用读写自己专属外部存储目录 (Android/data/<package_name>/ 和 Android/media/<package_name>/) 中的文件,不需要申请任何运行时存储权限 (READ/WRITE_EXTERNAL_STORAGE)。  这是非常重要且常被误解的一点!

    • 卸载行为:

      • Android/data/<package_name>/  当用户卸载应用时,系统会自动删除整个此目录!
      • Android/media/<package_name>/  当用户卸载应用时,系统默认也会删除此目录。但是!  用户可以在系统设置(如 设置 -> 应用 -> [应用名] -> 存储 -> 清除数据)中选择 “保留媒体文件” ,这样 Android/media/<package_name>/ 下的文件就不会被删除。这是存放用户可能希望保留的媒体文件(如应用拍摄的照片/视频)的理想位置。
  • 关键 API 获取路径:

    • 存放普通文件:  Context.getExternalFilesDir(String type)type 可以是 null(根目录)或 Environment 常量如 DIRECTORY_PICTURESDIRECTORY_MUSIC 等(会在 Android/data/<package_name>/files/Pictures/ 下创建对应子目录)。返回路径示例:/storage/emulated/0/Android/data/com.example.myapp/files/Pictures/
    • 存放缓存文件:  Context.getExternalCacheDir() -> 返回 /storage/emulated/0/Android/data/com.example.myapp/cache/ (系统可能清理,应用也应管理)
    • 存放媒体文件 (推荐位置):  Context.getExternalMediaDirs() -> 返回一个 File[] 数组,通常第一个元素是 /storage/emulated/0/Android/media/com.example.myapp/特别适合存放希望被系统媒体扫描器发现(如图库、音乐播放器)且用户卸载时可能选择保留的媒体文件。
  • 总结:  应用专属外部存储是应用在“大仓库”里拥有的“私家花园”。它空间充裕,访问自由(无需额外权限),适合存放应用特定的大文件或媒体文件。用户可见但其他应用不可随意访问。注意其卸载清理规则与内部存储不同(特别是 media 目录的用户保留选项)。

三、 路径总结与对比表

特性内部存储 (/data/data/<包名>)应用专属外部存储 (/sdcard/Android/data或media/<包名>)外部存储公共区域 (/sdcard/Pictures/等)
物理位置高速内置存储芯片容量更大的内置用户数据分区同上
默认访问权限仅本应用仅本应用所有应用(需权限)和用户
用户文件管理器可见否(需root)
其他应用直接访问不可能不可能(无权限)可能(需权限且受分区存储限制)
卸载应用是否删除data/:是 media/:默认是(用户可选保留)
是否需要存储权限否(API≥19)是(需MediaStore/SAF)
适用场景敏感小数据、核心配置、小型数据库应用专属大文件/缓存/媒体文件(推荐media/)用户共享的媒体/文档/下载文件
关键API示例getFilesDir() getCacheDir()getExternalFilesDir() getExternalMediaDirs()MediaStore 存储访问框架(SAF)

四、 环境变量与路径获取 (了解即可,注意弃用)

  • Environment.getDataDirectory(): 返回内部存储的根目录 /data应用通常无法直接访问此路径下的内容。
  • Environment.getExternalStorageDirectory(): 已弃用 (Deprecated in API 29)!  它返回外部存储的根目录 /storage/emulated/0 (/sdcard)。在分区存储下,强烈反对使用此方法或其返回的路径直接访问公共文件。它容易导致兼容性问题和不正确的结果(尤其在多用户、多 SD 卡场景)。
  • 替代方案:  优先使用 Context 提供的方法 (getFilesDir()getExternalFilesDir() 等) 来获取应用有权限操作的路径。访问公共文件使用 MediaStore 或 SAF

五、 核心要点提炼 (Key Takeaways)

  1. 内部存储 (/data/data/):  私密、高速、空间小、卸载必删、无需权限

  2. 外部存储 (/sdcard/) 包含两部分:

    • 应用专属区 (Android/data|media/):  应用私有、用户可见、空间大、卸载可删(media 用户可选留)、无需权限 (API>=19) 。存放应用大文件和媒体首选。
    • 公共区 (Pictures/Downloads/ 等):  用户共享、卸载保留、访问受限(分区存储!)。必须用 MediaStore / SAF + 需要权限
  3. 分区存储 (Scoped Storage) 是革命:  它限制了应用对公共区域的随意访问(特别是直接文件路径),强制使用 MediaStore / SAF。理解它是适应现代 Android 开发的关键。

  4. 权限基石:  访问自己的私有目录(无论内外),默认都不需要任何存储权限!公共区域的访问才需要权限且方式已变。

  5. 路径获取:  优先使用 Context 方法 (getFilesDir()getExternalFilesDir()getExternalMediaDirs()),弃用 Environment.getExternalStorageDirectory()