首页 / Android 入门教程 / 文件存储

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 数据库。