Android 入门教程
文件存储
本教程共 100 篇 · 第 66 篇 · 更新于 2026-07-28 · 约 9 分钟阅读
AndroidAndroid 入门教程文件存储内部存储外部存储分区存储FileProvider
66. 文件存储
本节目标:掌握 Android 内部存储和外部存储的文件读写,理解分区存储机制,学会用 FileProvider 安全分享文件。
内部存储 vs 外部存储
| 特性 | 内部存储 | 外部存储 |
|---|---|---|
| 空间 | 小 | 大 |
| 可用性 | 始终可用 | 可能不可用(SD 卡拔出) |
| 权限 | 不需要 | 应用专属目录不需要,其他需要 |
| 卸载删除 | 是 | 应用专属目录是 |
| 路径 | /data/data/<包名>/ | /sdcard/Android/data/<包名>/ |
Note
- 别被名字误导:「内部」和「外部」指的是存储卷,不是隐私级别。外部存储上的应用专属目录(
getExternalFilesDir())仍然是私有的,别的应用访问不了,不需要权限。
内部存储读写
写文件
fun writeToInternalFile(context: Context, filename: String, content: String) {
context.openFileOutput(filename, Context.MODE_PRIVATE).use { output ->
output.write(content.toByteArray())
}
}
openFileOutput 是 Context 的便捷方法,文件自动存在 filesDir 目录下。
读文件
fun readFromInternalFile(context: Context, filename: String): String {
return context.openFileInput(filename).bufferedReader().use { it.readText() }
}
直接用 File 对象
val file = File(context.filesDir, "data.txt")
// 写
file.writeText("内容")
// 读
val content = file.readText()
// 追加
file.appendText("追加内容")
缓存目录
val cacheFile = File(context.cacheDir, "temp.data")
// 缓存文件系统内存不足时可能自动清除
Tip
- 缓存目录存临时文件,系统在存储空间不足时会自动清理。别把重要数据放缓存目录,它可能被删。手动管理缓存大小是好习惯。
外部存储读写
检查可用性
fun isExternalStorageWritable(): Boolean {
return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
}
应用专属目录(不需要权限)
// 获取外部应用专属目录
val dir = context.getExternalFilesDir(null) // /sdcard/Android/data/<包名>/files/
val cacheDir = context.getExternalCacheDir() // /sdcard/Android/data/<包名>/cache/
// 指定子目录(系统会自动创建)
val imageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
// 写文件
val file = File(dir, "photo.jpg")
FileOutputStream(file).use { output ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, output)
}
Note
getExternalFilesDir()返回的是应用专属目录,不需要任何存储权限,卸载应用时自动删除。这是存应用产生的文件(图片、音频、文档)的首选位置。
分区存储(Scoped Storage)
Android 10 引入分区存储,限制应用对外部存储的访问。
核心规则
- 应用只能访问自己的专属目录(
getExternalFilesDir()) - 访问公共媒体文件(相册里的图片)用 MediaStore API
- 访问其他类型文件用存储访问框架(SAF,系统文件选择器)
WRITE_EXTERNAL_STORAGE权限在 Android 11+ 完全失效
MediaStore 保存图片到相册
suspend fun saveImageToGallery(context: Context, bitmap: Bitmap): Uri? {
val resolver = context.contentResolver
val imageCollection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "photo_${System.currentTimeMillis()}.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
put(MediaStore.Images.Media.IS_PENDING, 1)
}
}
val uri = resolver.insert(imageCollection, contentValues) ?: return null
resolver.openOutputStream(uri)?.use { output ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, output)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentValues.clear()
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
resolver.update(uri, contentValues, null, null)
}
return uri
}
Warning
- Android 10+ 写入自己的应用专属目录不需要权限。但写入公共相册(MediaStore)在 Android 10 需要
WRITE_EXTERNAL_STORAGE(可在 manifest 中用requestLegacyExternalStorage临时兼容),Android 11+ 不需要权限。读取其他应用的媒体文件需要READ_MEDIA_IMAGES(Android 13+)或READ_EXTERNAL_STORAGE(旧版)。
存储访问框架(SAF)
让用户通过系统文件选择器选择文件:
// 打开文件选择器
val pickFile = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
uri?.let {
// 读取选中的文件
context.contentResolver.openInputStream(it)?.use { input ->
val content = input.bufferedReader().readText()
}
}
}
// 启动选择器(参数是可选择的 MIME 类型)
pickFile.launch(arrayOf("image/*", "application/pdf"))
Tip
- SAF 不需要任何存储权限,用户选了哪个文件你就获得那个文件的临时访问权。这是访问非媒体文件(如 PDF、文档)的推荐方式。
FileProvider
如果要把应用内部的文件分享给其他应用(比如用第三方应用打开 PDF),不能直接传文件路径—其他应用没权限读你的内部文件。用 FileProvider 生成一个 content:// URI,授权临时访问。
配置 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>
路径配置 res/xml/file_paths.xml:
<paths>
<files-path name="my_files" path="." />
<cache-path name="my_cache" path="." />
<external-files-path name="external_files" path="." />
</paths>
使用 FileProvider 分享文件
fun openFile(context: Context, file: File) {
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
file
)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "application/pdf")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(intent)
}
Warning
FLAG_GRANT_READ_URI_PERMISSION必须加,否则目标应用没有权限读这个 URI。FileProvider 是跨应用分享文件的安全方式,避免了直接暴露文件路径的风险。
完整示例:保存 JSON 配置文件
class ConfigManager(private val context: Context) {
private val configFile get() = File(context.filesDir, "config.json")
fun saveConfig(config: AppConfig) {
val json = Gson().toJson(config)
configFile.writeText(json)
}
fun loadConfig(): AppConfig? {
if (!configFile.exists()) return null
return try {
Gson().fromJson(configFile.readText(), AppConfig::class.java)
} catch (e: Exception) {
null
}
}
}
data class AppConfig(
val serverUrl: String,
val cacheSize: Int,
val features: List<String>
)
Note
- 文件操作应该在 IO 线程执行,不要在主线程读写大文件。用协程的
withContext(Dispatchers.IO)切换线程。
小结
内部存储存小而重要的文件,外部专属目录存大文件(不需要权限)。分区存储限制了对外部存储的访问:自己的目录随便用,公共媒体用 MediaStore,其他文件用 SAF。跨应用分享文件用 FileProvider 生成 content URI。文件读写放 IO 线程。下一节学习 SQLite 数据库。