在 Android 应用开发中,文件存储是数据持久化的核心方式之一,直接影响应用的性能、安全性和用户体验。从简单的配置保存到大型文件处理,合理的存储策略能显著提升应用质量。本文将系统讲解 Android 文件存储的底层机制、核心 API 使用、分区存储适配及优化策略,帮助开发者构建高效、安全的存储方案。
一、存储系统底层架构
Android 的存储系统基于 Linux 文件系统构建,但针对移动设备特性进行了特殊设计。理解其底层架构是掌握存储机制的基础。
1.1 存储介质与挂载点
Android 设备通常包含两种存储介质:
- 内部存储(Internal Storage):基于闪存的内置存储,不可移除,挂载点为/data/
- 外部存储(External Storage):可能是内置扩展分区或可移除 SD 卡,挂载点通常为/sdcard/(实际指向/storage/emulated/0/)
系统通过虚拟文件系统(VFS)统一管理这些存储介质,应用通过标准 Linux 文件操作接口访问,无需关心物理存储位置。
1.2 存储权限模型
Android 采用分层权限控制:
- 应用私有目录:无需权限即可访问,其他应用不可见
- 共享存储区域:需申请READ_EXTERNAL_STORAGE/WRITE_EXTERNAL_STORAGE权限(Android 13 + 细化为媒体类型权限)
- 特殊目录:如/system/、/proc/等系统目录,仅 root 权限可访问
权限模型在 Android 10(API 29)发生重大变化,引入分区存储(Scoped Storage)限制应用对外部存储的无序访问。
1.3 存储目录结构与权限说明
核心目录结构及权限说明如下:
目录路径 |
存储类型 |
访问权限 |
权限说明 |
/data/data/<包名>/files/ |
内部存储 |
rw------- |
仅应用自身可读写,其他应用无权限访问 |
/data/data/<包名>/cache/ |
内部存储 |
rw------- |
仅应用自身可读写,系统低存储空间时可能被清理 |
/data/data/<包名>/databases/ |
内部存储 |
rw------- |
数据库文件专用目录,权限同内部文件目录 |
/storage/emulated/0/Android/data/<包名>/files/ |
外部私有 |
rwx------ |
仅应用自身可访问,卸载时自动删除 |
/storage/emulated/0/Android/data/<包名>/cache/ |
外部私有 |
rwx------ |
外部缓存目录,权限同外部文件目录 |
/storage/emulated/0/DCIM/ |
外部公共 |
rwxr-xr-x |
Android 10 前需READ/WRITE_EXTERNAL_STORAGE;10 + 通过 MediaStore 访问 |
/storage/emulated/0/Pictures/ |
外部公共 |
rwxr-xr-x |
同上,共享图片存储目录 |
/storage/emulated/0/Download/ |
外部公共 |
rwxr-xr-x |
下载文件公共目录,访问权限同其他公共目录 |
/storage/<sdcard_id>/ |
物理 SD 卡 |
动态权限 |
需通过系统 API 申请访问权限,不同设备可能有差异 |
/system/ |
系统目录 |
r-xr-xr-x |
只读权限,普通应用无法写入 |
/proc/ |
系统进程目录 |
部分可读 |
仅 root 可写入,普通应用可读取部分进程信息 |
/data/ # 内部存储根目录(仅root可完全访问)
├── data/<包名>/ # 应用私有数据目录
│ ├── files/ # 持久化文件(getFilesDir())
│ ├── cache/ # 缓存文件(getCacheDir())
│ └── databases/ # 数据库文件
└── user/0/ # 用户数据区
/storage/ # 外部存储根目录
├── emulated/0/ # 模拟外部存储
│ ├── Android/data/<包名>/ # 应用外部私有目录(getExternalFilesDir())
│ ├── DCIM/ # 照片目录
│ ├── Pictures/ # 图片目录
│ └── Download/ # 下载目录
└── <sdcard_id>/ # 物理SD卡(若存在)
二、核心存储类型与访问方式
Android 提供多种存储方式,适用于不同场景。开发者需根据数据特性(持久性、大小、敏感性)选择合适的存储方案。
2.1 内部存储(Internal Storage)
特点:
- 应用私有,其他应用无法访问(除非通过 ContentProvider)
- 应用卸载时自动删除
- 空间通常有限(建议存储小文件)
- 无需权限即可读写
核心 API:
// 获取文件目录(/data/data/<包名>/files/)
val filesDir: File = context.filesDir
// 获取缓存目录(/data/data/<包名>/cache/)
val cacheDir: File = context.cacheDir
// 创建文件
val file = File(filesDir, "user_config.txt")
// 写入数据
file.writeText("username=android")
// 读取数据
val content = file.readText()
// 创建临时文件
val tempFile = File.createTempFile("temp_", ".txt", cacheDir)
适用场景:
- 应用配置文件(如用户设置)
- 敏感数据(如认证令牌)
- 小体积临时文件
2.2 外部存储(External Storage)
外部存储分为私有目录和公共目录,两者特性差异显著。
外部私有目录
特点:
- 逻辑上属于应用私有(/storage/emulated/0/Android/data/<包名>/)
- 应用卸载时自动删除
- 空间通常较大
- 无需权限即可访问
核心 API:
// 获取外部文件目录(默认目录)
val externalFilesDir: File? = context.getExternalFilesDir(null)
// 获取指定类型的外部文件目录(如图片)
val imageDir: File? = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
// 获取外部缓存目录
val externalCacheDir: File? = context.externalCacheDir
目录类型常量:
- DIRECTORY_PICTURES:图片
- DIRECTORY_MUSIC:音乐
- DIRECTORY_VIDEO:视频
- DIRECTORY_DOWNLOADS:下载内容
外部公共目录
特点:
- 所有应用可访问(需相应权限)
- 应用卸载后文件保留
- 适合共享数据(如照片、文档)
核心 API:
// 获取公共图片目录
val publicPicturesDir: File = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES
)
// 创建公共文件(Android 10+需通过MediaStore或SAF)
val publicFile = File(publicPicturesDir, "shared_image.jpg")
权限要求:
- Android 10 以下:需WRITE_EXTERNAL_STORAGE权限
- Android 10 及以上:通过MediaStore访问,无需权限(但需处理分区存储限制)
2.3 媒体存储(Media Storage)
对于图片、音频、视频等媒体文件,Android 提供MediaStore框架统一管理,支持跨应用访问。
读取媒体文件:
// 查询所有图片
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.SIZE
)
val cursor = context.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
null,
null,
"${MediaStore.Images.Media.DATE_ADDED} DESC"
)
cursor?.use {
while (it.moveToNext()) {
val id = it.getLong(it.getColumnIndexOrThrow(MediaStore.Images.Media._ID))
val name = it.getString(it.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME))
val size = it.getLong(it.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE))
// 通过ID构建内容URI
val uri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id
)
}
}
插入媒体文件:
// 插入图片到公共图库
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "my_image.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/MyApp") // Android 10+
}
val uri = context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
values
)
uri?.let {
context.contentResolver.openOutputStream(it).use { outputStream ->
// 写入图片数据
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
}
}
2.4 存储访问框架(SAF)
对于非媒体文件(如文档、压缩包),推荐使用存储访问框架(Storage Access Framework),通过系统文件选择器访问:
// 打开文件选择器
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/pdf" // 限制PDF文件
}
startActivityForResult(intent, READ_PDF_REQUEST_CODE)
// 处理选择结果
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == READ_PDF_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
// 读取文件内容
val inputStream = contentResolver.openInputStream(uri)
// 使用文件数据...
}
}
}
SAF 的优势在于:
- 无需申请WRITE_EXTERNAL_STORAGE权限
- 支持访问多种存储位置(包括 Google Drive 等云存储)
- 符合分区存储规范
三、分区存储(Scoped Storage)深度解析
Android 10 引入的分区存储是存储系统最重大的变革,旨在解决外部存储混乱、隐私泄露等问题。
3.1 核心变化
1.访问限制:
- 应用默认只能访问自身私有目录和媒体集合
- 无法直接访问其他应用的私有文件
- 禁止写入根目录和其他应用的目录
2.媒体文件管理:
- 通过MediaStore访问媒体文件
- 新增RELATIVE_PATH字段指定存储位置
- 编辑 / 删除其他应用创建的媒体文件需获得用户授权
3.文件元数据:
- 应用只能访问自身创建文件的全部元数据
- 访问其他应用创建的媒体文件时,部分元数据(如地理位置)被屏蔽
3.2 适配策略
Android 10 + 适配步骤:
1.声明兼容模式(可选,临时过渡方案):
<application
android:requestLegacyExternalStorage="true">
</application>
注意:Android 11 + 强制启用分区存储,此属性无效。
2.迁移到 MediaStore:
// 替代直接File操作
fun saveImageToGallery(bitmap: Bitmap, displayName: String) {
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "$displayName.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/MyApp")
}
val uri = context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
values
) ?: return
context.contentResolver.openOutputStream(uri).use { output ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, output)
}
}
3.处理文件编辑权限:
// 请求编辑其他应用创建的文件
val editIntent = Intent(Intent.ACTION_EDIT, uri)
startActivityForResult(editIntent, EDIT_REQUEST_CODE)
3.3 兼容旧版本
通过版本判断实现跨版本兼容:
fun getImageFile(context: Context, fileName: String): File {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10+使用MediaStore,返回缓存文件
File(context.externalCacheDir, fileName)
} else {
// 旧版本直接使用外部存储
File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), fileName)
}
}
四、存储优化策略
高效的存储管理能提升应用性能、减少用户投诉(如 "占用空间过大")。优化应从空间占用、读写性能、安全性三方面入手。
4.1 空间优化
1.缓存管理:
- 定期清理过期缓存:
fun cleanOldCache(cacheDir: File, maxAgeMillis: Long) {
val cutoffTime = System.currentTimeMillis() - maxAgeMillis
cacheDir.listFiles()?.forEach { file ->
if (file.lastModified() < cutoffTime) {
file.deleteRecursively()
}
}
}
- 限制缓存大小:
fun trimCache(cacheDir: File, maxSizeBytes: Long) {
var totalSize = 0L
val files = cacheDir.listFiles()?.sortedBy { it.lastModified() } ?: return
for (file in files) {
totalSize += file.length()
if (totalSize > maxSizeBytes) {
file.delete()
}
}
}
2.文件压缩:
- 对文本数据使用压缩流:
fun writeCompressedFile(file: File, content: String) {
GZIPOutputStream(FileOutputStream(file)).use { gzip ->
gzip.write(content.toByteArray())
}
}
fun readCompressedFile(file: File): String {
return GZIPInputStream(FileInputStream(file)).bufferedReader().readText()
}
- 图片压缩:根据显示需求调整分辨率和质量
3.智能存储选择:
- 小文件(<1MB):优先内部存储
- 大文件(>10MB):使用外部存储
- 临时文件:存放在cacheDir或externalCacheDir
4.2 性能优化
1.异步 IO 操作:
- 使用协程避免主线程阻塞:
suspend fun readFileAsync(file: File): String = withContext(Dispatchers.IO) {
file.readText()
}
2.缓冲流使用:
// 缓冲流比普通流快2-5倍
fun copyFile(src: File, dest: File) {
BufferedInputStream(FileInputStream(src)).use { input ->
BufferedOutputStream(FileOutputStream(dest)).use { output ->
input.copyTo(output)
}
}
}
3.内存映射文件:
对于大文件(>50MB),使用MappedByteBuffer提升读写速度:
fun readLargeFile(file: File): String {
FileInputStream(file).channel.use { channel ->
val buffer = channel.map(
FileChannel.MapMode.READ_ONLY,
0,
channel.size()
)
return Charsets.UTF_8.decode(buffer).toString()
}
}
4.避免频繁 IO:
- 批量写入代替多次单条写入
-
- 使用内存缓存减少重复读取
4.3 安全性优化
1.敏感数据加密:
// 使用AndroidKeyStore加密敏感文件
fun encryptFile(context: Context, plainFile: File, encryptedFile: File) {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
)
keyGenerator.init(KeyGenParameterSpec.Builder(
"my_encryption_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build())
val key = keyGenerator.generateKey()
// 加密文件内容
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, key)
val iv = cipher.iv // 保存IV用于解密
FileOutputStream(encryptedFile).use { output ->
output.write(iv) // 先写入IV
cipher.doFinal(plainFile.readBytes()).let {
output.write(it)
}
}
}
2.私有文件保护:
- 内部存储文件默认权限为-rw-------(仅应用可访问)
- 外部私有目录文件设置权限:
file.setReadable(false, false) // 禁止其他应用读取
file.setWritable(false, false) // 禁止其他应用写入
3.数据备份控制:
- 禁止敏感数据备份:
<application ...>
<meta-data
android:name="android.app.backup.BackupAgent"
android:value="com.example.NoBackupAgent" />
</application>
- 使用android:allowBackup="false"完全禁用备份(不推荐,影响用户体验)
五、最佳实践与常见问题
5.1 最佳实践清单
1.目录选择准则:
数据类型 |
推荐目录 |
生命周期 |
用户配置 |
filesDir |
应用生命周期 |
缓存图片 |
externalCacheDir |
临时,可能被系统清理 |
下载文件 |
getExternalFilesDir(DIRECTORY_DOWNLOADS) |
应用生命周期 |
共享图片 |
MediaStore.Images |
设备生命周期 |
数据库文件 |
内部存储databases/目录 |
应用生命周期 |
2.文件命名规范:
- 使用 UUID 作为文件名避免冲突
- 包含时间戳便于过期清理
- 统一扩展名便于识别
3.测试策略:
- 测试低存储空间场景(adb shell am set-internal-storage-limit 100M)
- 验证应用卸载后文件是否正确清理
- 测试分区存储在不同 Android 版本的兼容性
5.2 常见问题解决方案
1.存储空间不足:
fun checkStorageAvailable(requiredSpaceBytes: Long): Boolean {
val stat = StatFs(Environment.getExternalStorageDirectory().path)
val availableBytes = stat.availableBlocksLong * stat.blockSizeLong
return availableBytes >= requiredSpaceBytes
}
2.文件删除不生效:
-
- 对于MediaStore文件,需调用contentResolver.delete(uri, null, null)
-
- 确保拥有文件删除权限(特别是其他应用创建的文件)
3.分区存储下的文件迁移:
fun migrateLegacyFiles(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val legacyDir = File(Environment.getExternalStorageDirectory(), "MyApp")
if (legacyDir.exists()) {
// 迁移到外部私有目录
val newDir = context.getExternalFilesDir(null) ?: return
legacyDir.copyRecursively(newDir, overwrite = true)
legacyDir.deleteRecursively()
}
}
}
4.大文件处理 OOM:
- 使用流式处理代替一次性加载
- 图片处理使用inSampleSize降低内存占用
六、总结
Android 文件存储机制随着系统版本迭代不断演进,从早期的自由访问到现在的分区存储,安全性和规范性持续提升。开发者需要:
- 理解不同存储类型的特性与适用场景
- 严格遵循分区存储规范,做好版本兼容
- 实施空间、性能和安全性优化策略
- 建立完善的缓存管理和清理机制
合理的文件存储方案不仅能提升应用性能,还能减少用户投诉和卸载率。随着 Android 存储系统的不断完善,开发者应持续关注官方文档,及时适配新特性,打造更优质的用户体验。