Skip to content

文件存储:Android 文件 I/O 完整指南

目录

  1. 文件存储概述
  2. 内部存储
  3. 外部存储
  4. 文件权限管理
  5. 缓存管理
  6. 高级文件操作
  7. 最佳实践
  8. 面试考点

1. 文件存储概述

1.1 文件存储类型对比

Android 提供多种文件存储方式:

类型路径生命周期可访问性适用场景
内部存储/data/data/<package>/files/应用存在仅应用配置文件、私有数据
内部缓存/data/data/<package>/cache/可被系统清除仅应用临时文件
外部存储/storage/emulated/0/用户可删除所有应用(部分)媒体文件、共享文件
外部应用目录/storage/emulated/0/Android/data/<package>/应用卸载清除应用 + 系统应用专用大文件
公共目录/storage/emulated/0/Pictures/用户控制所有应用照片、视频、文档

1.2 存储路径详解

完整存储结构:
/data/data/com.example.app/
├── files/           # 内部存储(私有)
├── cache/           # 内部缓存
├── code_cache/      # 代码缓存
├── databases/       # SQLite 数据库
├── shared_prefs/    # SharedPreferences
└── no_backup/       # 不备份的目录

/storage/emulated/0/
├── Download/        # 下载目录
├── DCIM/           # 相机目录
├── Pictures/        # 图片目录
├── Music/           # 音乐目录
├── Movies/          # 视频目录
├── Documents/       # 文档目录
└── Android/
    ├── data/        # 应用专用目录
    ├── obb/         # 大型游戏资源
    └── media/       # 媒体文件

1.3 IO 流基础

kotlin
// Android 文件 IO 主要方式
class FileIOBasics(context: Context) {
    private val file = File(context.filesDir, "test.txt")
    
    // 1. Java IO
    fun javaIO() {
        FileInputStream(file).use { input ->
            FileOutputStream(file).use { output ->
                // 读写操作
            }
        }
    }
    
    // 2. Kotlin IO
    fun kotlinIO() {
        file.readText()    // 读取文本
        file.writeText("内容")  // 写入文本
        file.readBytes()   // 读取字节
        file.writeBytes(byteArray)  // 写入字节
    }
    
    // 3. Android Assets
    fun assetsIO() {
        context.assets.open("file.txt").use { input ->
            // 读取资源文件
        }
    }
    
    // 4. Android Resources
    fun resourcesIO() {
        context.resources.openRawResource(R.raw.data).use { input ->
            // 读取 raw 资源
        }
    }
}

2. 内部存储

2.1 基本操作

kotlin
class InternalStorage(context: Context) {
    private val filesDir = context.filesDir
    
    // 写入文件
    fun writeToFile(fileName: String, content: String) {
        val file = File(filesDir, fileName)
        file.writeText(content)
        
        // 或使用 FileOutputStream
        FileOutputStream(file).use { output ->
            output.write(content.toByteArray())
        }
    }
    
    // 读取文件
    fun readFromFile(fileName: String): String {
        val file = File(filesDir, fileName)
        return if (file.exists()) {
            file.readText()
        } else {
            ""
        }
    }
    
    // 读取为字节数组
    fun readBytes(fileName: String): ByteArray {
        val file = File(filesDir, fileName)
        return file.readBytes()
    }
    
    // 写入字节数组
    fun writeBytes(fileName: String, bytes: ByteArray) {
        val file = File(filesDir, fileName)
        file.writeBytes(bytes)
    }
    
    // 检查文件是否存在
    fun fileExists(fileName: String): Boolean {
        return File(filesDir, fileName).exists()
    }
    
    // 删除文件
    fun deleteFile(fileName: String): Boolean {
        val file = File(filesDir, fileName)
        return file.delete()
    }
    
    // 获取文件大小
    fun getFileSize(fileName: String): Long {
        val file = File(filesDir, fileName)
        return if (file.exists()) file.length() else 0L
    }
}

2.2 子目录操作

kotlin
class SubDirectoryStorage(context: Context) {
    private val baseDir = context.filesDir
    
    // 创建子目录
    fun createSubDirectory(dirName: String): File {
        val dir = File(baseDir, dirName)
        if (!dir.exists()) {
            dir.mkdirs()
        }
        return dir
    }
    
    // 列出子目录
    fun listSubDirectories(): List<String> {
        return baseDir.listFiles(File::isDirectory)?.map { it.name } ?: emptyList()
    }
    
    // 列出文件
    fun listFiles(directory: String = ""): List<File> {
        val dir = if (directory.isEmpty()) baseDir else File(baseDir, directory)
        return dir.listFiles()?.toList() ?: emptyList()
    }
    
    // 递归列出所有文件
    fun listAllFilesRecursive(directory: File = baseDir): List<File> {
        val files = mutableListOf<File>()
        
        directory.listFiles()?.forEach { file ->
            if (file.isDirectory) {
                files.addAll(listAllFilesRecursive(file))
            } else {
                files.add(file)
            }
        }
        
        return files
    }
    
    // 删除目录及所有文件
    fun deleteDirectory(dirName: String): Boolean {
        val dir = File(baseDir, dirName)
        return deleteDirectoryRecursive(dir)
    }
    
    private fun deleteDirectoryRecursive(dir: File): Boolean {
        dir.listFiles()?.forEach { file ->
            if (file.isDirectory) {
                deleteDirectoryRecursive(file)
            } else {
                file.delete()
            }
        }
        return dir.delete()
    }
}

2.3 对象序列化

kotlin
class SerializableStorage(context: Context) {
    private val filesDir = context.filesDir
    
    // 序列化对象
    fun saveObject(name: String, obj: Any) {
        val file = File(filesDir, "$name.obj")
        ObjectOutputStream(FileOutputStream(file)).use { output ->
            output.writeObject(obj)
        }
    }
    
    // 反序列化对象
    fun loadObject(name: String, clazz: Class<out Any>): Any? {
        val file = File(filesDir, "$name.obj")
        return if (file.exists()) {
            ObjectInputStream(FileInputStream(file)).use { input ->
                input.readObject()
            }
        } else {
            null
        }
    }
}

// Parcelable 方式(推荐)
class ParcelableStorage(context: Context) {
    private val filesDir = context.filesDir
    
    // 使用 Gson 序列化(推荐)
    private val gson = Gson()
    
    fun saveUser(user: User) {
        val file = File(filesDir, "user.json")
        val json = gson.toJson(user)
        file.writeText(json)
    }
    
    fun loadUser(): User? {
        val file = File(filesDir, "user.json")
        return if (file.exists()) {
            val json = file.readText()
            gson.fromJson(json, User::class.java)
        } else {
            null
        }
    }
    
    // 使用 Kryo(高性能)
    private val kryo = Kryo()
    
    fun saveWithKryo(name: String, obj: Any) {
        val file = File(filesDir, "$name.kryo")
        FileOutputStream(file).use { output ->
            kryo.writeObject(output, obj)
        }
    }
}

data class User(
    val id: Int,
    val name: String,
    val email: String,
    val age: Int
)

2.4 压缩文件

kotlin
class CompressedStorage(context: Context) {
    private val filesDir = context.filesDir
    
    // 压缩文件
    fun compressFile(sourceFile: File, zipFile: File) {
        val buffer = ByteArray(1024)
        
        FileOutputStream(zipFile).use { fos ->
            ZipOutputStream(fos).use { zos ->
                FileInputStream(sourceFile).use { fis ->
                    val zipEntry = ZipEntry(sourceFile.name)
                    zos.putNextEntry(zipEntry)
                    
                    var length: Int
                    while (fis.read(buffer).also { length = it } > 0) {
                        zos.write(buffer, 0, length)
                    }
                    
                    zos.closeEntry()
                }
            }
        }
    }
    
    // 解压文件
    fun decompressFile(zipFile: File, destDir: File) {
        if (!destDir.exists()) {
            destDir.mkdirs()
        }
        
        FileInputStream(zipFile).use { fis ->
            ZipInputStream(fis).use { zin ->
                var zipEntry: ZipEntry?
                while (zin.nextEntry.also { zipEntry = it } != null) {
                    val outputFile = File(destDir, zipEntry!!.name)
                    
                    FileOutputStream(outputFile).use { fos ->
                        val buffer = ByteArray(1024)
                        var length: Int
                        while (zin.read(buffer).also { length = it } > 0) {
                            fos.write(buffer, 0, length)
                        }
                    }
                    
                    zin.closeEntry()
                }
            }
        }
    }
    
    // 压缩多个文件
    fun compressFiles(files: List<File>, zipFile: File) {
        val buffer = ByteArray(1024)
        
        FileOutputStream(zipFile).use { fos ->
            ZipOutputStream(fos).use { zos ->
                files.forEach { file ->
                    if (file.exists()) {
                        FileInputStream(file).use { fis ->
                            val zipEntry = ZipEntry(file.name)
                            zos.putNextEntry(zipEntry)
                            
                            var length: Int
                            while (fis.read(buffer).also { length = it } > 0) {
                                zos.write(buffer, 0, length)
                            }
                            
                            zos.closeEntry()
                        }
                    }
                }
            }
        }
    }
}

3. 外部存储

3.1 Android 10+ 分区存储(Scoped Storage)

从 Android 10 (API 29) 开始,Google 引入了分区存储:

kotlin
class ScopedStorage(context: Context) {
    private val contextRef = context.applicationContext
    
    // 检查是否支持分区存储
    fun isScopedStorageEnabled(): Boolean {
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
    }
    
    // 访问公共目录(推荐)
    fun saveToPublicPictures(fileName: String, bitmap: Bitmap): Uri {
        val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
        
        val values = ContentValues().apply {
            put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
            put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
            put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/MyApp")
        }
        
        contextRef.contentResolver.insert(uri, values)?.let { uri ->
            contextRef.contentResolver.openOutputStream(uri)?.use { output ->
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output)
            }
        }
        
        return uri
    }
    
    // 访问公共下载目录
    fun saveToDownloads(fileName: String, data: ByteArray): Uri {
        val uri = MediaStore.Downloads.EXTERNAL_CONTENT_URI
        
        val values = ContentValues().apply {
            put(MediaStore.Downloads.DISPLAY_NAME, fileName)
            put(MediaStore.Downloads.MIME_TYPE, "application/octet-stream")
        }
        
        contextRef.contentResolver.insert(uri, values)?.let { uri ->
            contextRef.contentResolver.openOutputStream(uri)?.use { output ->
                output.write(data)
            }
        }
        
        return uri
    }
    
    // 从公共目录读取文件
    fun getFileInfo(uri: Uri): FileInfo? {
        return contextRef.contentResolver.query(
            uri,
            arrayOf(
                MediaStore.Images.Media.DISPLAY_NAME,
                MediaStore.Images.Media.SIZE,
                MediaStore.Images.Media.MIME_TYPE
            ),
            null,
            null,
            null
        )?.use { cursor ->
            if (cursor.moveToFirst()) {
                FileInfo(
                    name = cursor.getString(0),
                    size = cursor.getLong(1),
                    mimeType = cursor.getString(2)
                )
            } else {
                null
            }
        }
    }
    
    data class FileInfo(
        val name: String,
        val size: Long,
        val mimeType: String
    )
}

3.2 应用专用外部目录

kotlin
class AppExternalStorage(context: Context) {
    // 获取应用专用外部目录
    val appSpecificDirs: Array<File>
        get() = context.getExternalFilesDirs(null) ?: arrayOf(context.filesDir)
    
    // 主外部存储目录
    val primaryExternalDir: File
        get() = appSpecificDirs.firstOrNull() ?: context.filesDir
    
    // 写入到应用专用目录
    fun writeToExternalStorage(fileName: String, content: String): File? {
        return appSpecificDirs.firstOrNull { it.canWrite() }?.let { dir ->
            val file = File(dir, fileName)
            file.writeText(content)
            file
        }
    }
    
    // 从应用专用目录读取
    fun readFromExternalStorage(fileName: String): String? {
        return appSpecificDirs.firstOrNull { 
            File(it, fileName).exists() 
        }?.let { dir ->
            File(dir, fileName).readText()
        }
    }
    
    // 获取缓存目录
    val externalCacheDirs: Array<File>
        get() = context.externalCacheDirs ?: arrayOf(context.cacheDir)
    
    // 写入缓存文件
    fun writeToExternalCache(fileName: String, content: ByteArray): File? {
        return externalCacheDirs.firstOrNull { it.canWrite() }?.let { dir ->
            val file = File(dir, fileName)
            file.writeBytes(content)
            file
        }
    }
    
    // 获取obb目录(大型游戏资源)
    val externalObbDirs: Array<File>
        get() = context.externalObbDirs ?: emptyArray()
}

3.3 使用 Storage Access Framework (SAF)

kotlin
class StorageAccessFramework(context: Context) {
    private val contextRef = context.applicationContext
    
    // 打开文档选择器
    fun pickDocument() {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
            addCategory(Intent.CATEGORY_OPENABLE)
            type = "*/*"
        }
        
        // 限制文件类型
        // type = "image/*"
        // putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/jpeg", "image/png"))
        
        contextRef.startActivityForResult(intent, REQUEST_PICK_DOCUMENT)
    }
    
    // 保存文档
    fun saveDocument() {
        val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
            addCategory(Intent.CATEGORY_OPENABLE)
            type = "application/json"
            putExtra(Intent.EXTRA_TITLE, "export_data.json")
        }
        
        contextRef.startActivityForResult(intent, REQUEST_SAVE_DOCUMENT)
    }
    
    // 处理选中的文件
    fun handlePickedDocument(uri: Uri) {
        contextRef.contentResolver.openInputStream(uri)?.use { input ->
            // 读取文件内容
            val content = input.readText()
        }
    }
    
    // 持久化权限(保持对文件的访问)
    fun persistUriPermission(uri: Uri) {
        val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or 
                       Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        
        contextRef.contentResolver.takePersistableUriPermission(uri, takeFlags)
    }
    
    // 释放权限
    fun releaseUriPermission(uri: Uri) {
        contextRef.contentResolver.releasePersistableUriPermission(
            uri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION or 
            Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        )
    }
    
    // 获取持久化的 URI
    fun getPersistedUriPermissions(): List<Uri> {
        return contextRef.contentResolver.persistedUriPermissions.map {
            it.uri
        }
    }
}

3.4 MediaStore 完整示例

kotlin
class MediaStoreHelper(context: Context) {
    private val contextRef = context.applicationContext
    private val contentResolver = contextRef.contentResolver
    
    // 保存图片
    suspend fun saveImage(bitmap: Bitmap, fileName: String): Uri {
        return withContext(Dispatchers.IO) {
            val values = ContentValues().apply {
                put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
                put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
                put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/MyApp")
                put(MediaStore.Images.Media.IS_PENDING, 1) // 标记为待处理
            }
            
            val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
                ?: throw Exception("Failed to insert image")
            
            contentResolver.openOutputStream(uri)?.use { output ->
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output)
                
                // 完成写入,移除待处理标记
                values.clear()
                values.put(MediaStore.Images.Media.IS_PENDING, 0)
                contentResolver.update(uri, values, null, null)
            }
            
            uri
        }
    }
    
    // 保存视频
    suspend fun saveVideo(videoPath: String, title: String): Uri {
        return withContext(Dispatchers.IO) {
            val values = ContentValues().apply {
                put(MediaStore.Video.Media.DISPLAY_NAME, File(videoPath).name)
                put(MediaStore.Video.Media.MIME_TYPE, "video/mp4")
                put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/MyApp")
                put(MediaStore.Video.Media.TITLE, title)
                put(MediaStore.Video.Media.IS_PENDING, 1)
            }
            
            val uri = contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values)
                ?: throw Exception("Failed to insert video")
            
            val videoFile = File(videoPath)
            contentResolver.openOutputStream(uri)?.use { output ->
                FileInputStream(videoFile).copyTo(output)
                
                values.clear()
                values.put(MediaStore.Video.Media.IS_PENDING, 0)
                contentResolver.update(uri, values, null, null)
            }
            
            uri
        }
    }
    
    // 删除文件
    suspend fun deleteFile(uri: Uri) {
        withContext(Dispatchers.IO) {
            contentResolver.delete(uri, null, null)
        }
    }
    
    // 查询图片
    fun queryImages(limit: Int = 20): Flow<List<ImageInfo>> {
        return flow {
            val projection = arrayOf(
                MediaStore.Images.Media._ID,
                MediaStore.Images.Media.DISPLAY_NAME,
                MediaStore.Images.Media.SIZE,
                MediaStore.Images.Media.DATE_ADDED
            )
            
            val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC LIMIT $limit"
            
            contentResolver.query(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                projection,
                null,
                null,
                sortOrder
            )?.use { cursor ->
                val idIndex = cursor.getColumnIndex(MediaStore.Images.Media._ID)
                val nameIndex = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
                val sizeIndex = cursor.getColumnIndex(MediaStore.Images.Media.SIZE)
                val dateIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED)
                
                while (cursor.moveToNext()) {
                    emit(ImageInfo(
                        id = cursor.getLong(idIndex),
                        name = cursor.getString(nameIndex),
                        size = cursor.getLong(sizeIndex),
                        dateAdded = cursor.getLong(dateIndex)
                    ))
                }
            }
        }.asFlow()
    }
    
    data class ImageInfo(
        val id: Long,
        val name: String,
        val size: Long,
        val dateAdded: Long
    )
}

4. 文件权限管理

4.1 运行时权限

kotlin
class FilePermissionManager(private val activity: Activity) {
    private val context = activity.applicationContext
    
    // 请求存储权限(Android 9 及以下)
    fun requestStoragePermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            when (Build.VERSION.SDK_INT) {
                in Build.VERSION_CODES.M..Build.VERSION_CODES.P -> {
                    // Android 6-9: 请求 WRITE_EXTERNAL_STORAGE
                    ActivityCompat.requestPermissions(
                        activity,
                        arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
                        REQUEST_STORAGE_PERMISSION
                    )
                }
                Build.VERSION_CODES.Q -> {
                    // Android 10: 请求 MANAGE_EXTERNAL_STORAGE 或 使用 MediaStore
                    ActivityCompat.requestPermissions(
                        activity,
                        arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
                        REQUEST_STORAGE_PERMISSION
                    )
                }
                else -> {
                    // Android 11+: 使用分区存储,不需要权限
                    // 或请求 MANAGE_EXTERNAL_STORAGE
                }
            }
        }
    }
    
    // 请求管理所有文件权限(Android 11+)
    fun requestManageAllFilesPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            if (!Environment.isExternalStorageManager()) {
                val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
                activity.startActivity(intent)
            }
        }
    }
    
    // 检查权限
    fun hasStoragePermission(): Boolean {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            Environment.isExternalStorageManager() || 
            ContextCompat.checkSelfPermission(
                context,
                Manifest.permission.READ_EXTERNAL_STORAGE
            ) == PackageManager.PERMISSION_GRANTED
        } else {
            ContextCompat.checkSelfPermission(
                context,
                Manifest.permission.READ_EXTERNAL_STORAGE
            ) == PackageManager.PERMISSION_GRANTED
        }
    }
    
    // 检查写入权限
    fun hasWritePermission(): Boolean {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            Environment.isExternalStorageManager()
        } else {
            ContextCompat.checkSelfPermission(
                context,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
            ) == PackageManager.PERMISSION_GRANTED
        }
    }
    
    companion object {
        const val REQUEST_STORAGE_PERMISSION = 1001
        const val REQUEST_MANAGE_FILES_PERMISSION = 1002
    }
}

4.2 权限处理完整流程

kotlin
class PermissionHandlerActivity : AppCompatActivity() {
    private val permissionManager = FilePermissionManager(this)
    
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        
        when (requestCode) {
            FilePermissionManager.REQUEST_STORAGE_PERMISSION -> {
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    // 权限已授予
                    accessExternalStorage()
                } else {
                    // 权限被拒绝
                    if (shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                        // 解释为什么需要权限
                        showPermissionRationale()
                    } else {
                        // 用户选择"不再询问",引导到设置
                        showSettingsDialog()
                    }
                }
            }
        }
    }
    
    private fun showPermissionRationale() {
        AlertDialog.Builder(this)
            .setTitle("需要存储权限")
            .setMessage("应用需要访问存储来保存文件")
            .setPositiveButton("确定") { _, _ ->
                permissionManager.requestStoragePermission()
            }
            .setNegativeButton("取消", null)
            .show()
    }
    
    private fun showSettingsDialog() {
        AlertDialog.Builder(this)
            .setTitle("权限被拒绝")
            .setMessage("请在设置中手动授予存储权限")
            .setPositiveButton("去设置") { _, _ ->
                val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
                    data = Uri.fromParts("package", packageName, null)
                }
                startActivity(intent)
            }
            .setNegativeButton("取消", null)
            .show()
    }
    
    private fun accessExternalStorage() {
        // 执行需要权限的操作
    }
}

4.3 特殊目录权限

kotlin
class SpecialDirectoryAccess(context: Context) {
    private val contextRef = context.applicationContext
    
    // 访问 DCIM 目录(相机)
    fun accessDcimDirectory(): File? {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            // Android 10+ 使用 MediaStore
            null
        } else {
            Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
        }
    }
    
    // 访问 Download 目录
    fun accessDownloadsDirectory(): File? {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            null // 使用 MediaStore
        } else {
            Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
        }
    }
    
    // Android 10+ 使用 MediaStore 访问公共目录
    fun saveToDcimAndroid10Plus(bitmap: Bitmap, fileName: String): Uri {
        val values = ContentValues().apply {
            put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
            put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
            put(MediaStore.Images.Media.RELATIVE_PATH, "DCIM/MyApp")
        }
        
        val uri = contextRef.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
        
        uri?.let {
            contextRef.contentResolver.openOutputStream(it)?.use { output ->
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output)
            }
        }
        
        return uri
    }
}

5. 缓存管理

5.1 缓存策略

kotlin
class CacheManager(context: Context) {
    private val cacheDir = context.cacheDir
    private val externalCacheDir = context.externalCacheDir
    
    // 获取缓存目录大小
    fun getCacheSize(directory: File = cacheDir): Long {
        return directory.listFiles()?.sumOf { 
            if (it.isDirectory) getCacheSize(it) else it.length() 
        } ?: 0L
    }
    
    // 清理缓存
    fun clearCache(directory: File = cacheDir): Long {
        val size = getCacheSize(directory)
        
        directory.listFiles()?.forEach { file ->
            if (file.isDirectory) {
                clearCache(file)
            } else {
                file.delete()
            }
        }
        
        return size
    }
    
    // 清理超过大小限制的缓存
    fun trimCache(maxSize: Long, directory: File = cacheDir) {
        val currentSize = getCacheSize(directory)
        
        if (currentSize > maxSize) {
            // 按修改时间排序,删除最旧的
            val files = directory.listFiles()?.sortedBy { it.lastModified() } ?: emptyList()
            
            var freedSpace = 0L
            for (file in files) {
                if (freedSpace + file.length() > currentSize - maxSize) {
                    break
                }
                
                if (file.isDirectory) {
                    freedSpace += clearCache(file)
                } else {
                    file.delete()
                    freedSpace += file.length()
                }
            }
        }
    }
}

5.2 LruCache 实现

kotlin
class ImageCache(context: Context) {
    // 计算缓存大小(内存的 1/8)
    private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
    private val cacheSize = maxMemory / 8
    
    // LruCache 实现
    private val memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
        override fun sizeOf(key: String, value: Bitmap): Int {
            // 返回 Bitmap 占用的内存(KB)
            return value.byteCount / 1024
        }
    }
    
    // 磁盘缓存
    private val diskCache: Cache
    
    init {
        val cacheDir = File(context.cacheDir, "image_cache")
        if (!cacheDir.exists()) {
            cacheDir.mkdirs()
        }
        
        diskCache = Cache(cacheDir, 100 * 1024 * 1024) // 100MB
    }
    
    // 获取图片
    fun getBitmap(key: String): Bitmap? {
        // 1. 先查内存缓存
        var bitmap = memoryCache.get(key)
        
        // 2. 内存未命中,查磁盘缓存
        if (bitmap == null) {
            try {
                val snapshot = diskCache.get(key)
                if (snapshot != null) {
                    bitmap = bitmapFromCache(snapshot)
                    if (bitmap != null) {
                        memoryCache.put(key, bitmap)
                    }
                    snapshot.close()
                }
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
        
        return bitmap
    }
    
    // 保存图片
    fun putBitmap(key: String, bitmap: Bitmap) {
        // 存入内存缓存
        memoryCache.put(key, bitmap)
        
        // 存入磁盘缓存
        try {
            val editor = diskCache.edit(key)
            outputStream(editor).use { output ->
                bitmap.compress(Bitmap.CompressFormat.PNG, 100, output)
            }
            editor.commit()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }
    
    private fun bitmapFromCache(snapshot: Snapshot): Bitmap? {
        return try {
            val `in` = inputStream(snapshot)
            BitmapFactory.decodeStream(`in`)
        } finally {
            snapshot.close()
        }
    }
    
    // 清理缓存
    fun clear() {
        memoryCache.evictAll()
        try {
            diskCache.delete()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }
}

5.3 Glide 缓存

kotlin
class GlideCacheManager(context: Context) {
    // 配置 Glide 缓存
    fun configureGlideCache() {
        Glide.with(context).clearMemory()
    }
    
    // 获取缓存大小
    fun getGlideCacheSize(): Long {
        val glide = Glide.with(context)
        return glide.context.cacheSize
    }
    
    // 清理 Glide 缓存
    fun clearGlideCache() {
        Glide.with(context).apply {
            clearMemory()
            
            // 清理磁盘缓存
            context.cacheDir.listFiles { file ->
                file.name.startsWith("glide") || file.name.startsWith("image_cache")
            }?.forEach { dir ->
                dir.deleteRecursively()
            }
        }
    }
    
    // 加载图片(自动缓存)
    fun loadImage(context: Context, url: String, imageView: ImageView) {
        Glide.with(context)
            .load(url)
            .centerCrop()
            .diskCacheStrategy(DiskCacheStrategy.ALL)  // 缓存原始和转换后的图片
            .memoryCacheStrategy(MemoryCacheStrategy.ALL)
            .into(imageView)
    }
    
    // 配置自定义缓存大小
    fun configureCacheSize() {
        val appGlideModule = AppGlideModule()
        
        // 设置内存缓存大小
        appGlideModule.memorySize = 100 * 1024 * 1024 // 100MB
        
        // 设置磁盘缓存大小
        appGlideModule.diskCache = LruCache(200 * 1024 * 1024) // 200MB
    }
}

class AppGlideModule : GlideModule {
    // Glide 配置
}

5.4 OkHttp 缓存

kotlin
class OkHttpCacheManager(context: Context) {
    private val cache: Cache
    private val client: OkHttpClient
    
    init {
        // 创建缓存目录
        val cacheDir = File(context.cacheDir, "http_cache")
        
        // 创建缓存(100MB)
        cache = Cache(cacheDir, 100 * 1024 * 1024)
        
        // 配置 OkHttpClient
        client = OkHttpClient.Builder()
            .cache(cache)
            .build()
    }
    
    // 获取缓存统计
    fun getCacheStats(): CacheStats {
        return cache.stats()
    }
    
    // 清理缓存
    fun clearCache() {
        cache.evictAll()
    }
    
    // 删除特定 URL 的缓存
    fun removeCache(url: String) {
        cache.directory().listFiles()?.forEach { file ->
            if (file.name.contains(url.hashCode().toString(16))) {
                file.delete()
            }
        }
    }
    
    // 配置缓存策略
    fun configureCacheControl() {
        // 响应头控制
        // Cache-Control: max-age=3600
        // Expires: Wed, 21 Oct 2015 07:28:00 GMT
        // ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
        // Last-Modified: Tue, 10 Oct 2015 07:28:00 GMT
    }
}

6. 高级文件操作

6.1 文件加密

kotlin
class EncryptedFileStorage(context: Context) {
    private val filesDir = context.filesDir
    private val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()
    
    // 加密写入
    fun writeEncrypted(fileName: String, content: String) {
        val encryptedFile = EncryptedFile.Builder(
            masterKey,
            context,
            File(filesDir, fileName),
            EncryptedFile.FileEncryptionScheme.AES256_GCM_HMACSHA384_AES256_GCM
        ).build()
        
        encryptedFile.openFileOutput().use { output ->
            output.write(content.toByteArray())
        }
    }
    
    // 加密读取
    fun readEncrypted(fileName: String): String {
        val encryptedFile = EncryptedFile.Builder(
            masterKey,
            context,
            File(filesDir, fileName),
            EncryptedFile.FileEncryptionScheme.AES256_GCM_HMACSHA384_AES256_GCM
        ).build()
        
        return encryptedFile.openFileInput().use { input ->
            input.readText()
        }
    }
}

6.2 文件监视

kotlin
class FileWatcher(context: Context) {
    private val watchService = WatchService.getInstance(context)
    private val watchDir = context.filesDir
    
    // 注册文件监听
    fun registerWatch(path: String, callback: (FileEvent) -> Unit) {
        watchService.watch(
            path,
            arrayOf(WatchService.CREATE, WatchService.MODIFY, WatchService.DELETE),
            object : WatchService.Watcher {
                override fun onEvent(event: FileEvent) {
                    callback(event)
                }
            }
        )
    }
    
    // 使用 FileObserver(API 较低)
    fun observeDirectory(path: String) {
        val observer = object : FileObserver(path, ALL_EVENTS) {
            override fun onEvent(event: Int, path: String?) {
                when (event) {
                    CREATE -> Log.d("FileWatcher", "File created: $path")
                    MODIFY -> Log.d("FileWatcher", "File modified: $path")
                    DELETE -> Log.d("FileWatcher", "File deleted: $path")
                }
            }
        }
        observer.startWatching()
    }
}

6.3 文件同步

kotlin
class FileSyncManager(context: Context) {
    private val localDir = context.filesDir
    private val remoteBaseUrl = "https://example.com/api/files"
    
    // 上传文件
    suspend fun uploadFile(file: File): Result<String> {
        return try {
            val body = MultipartBody.Part.createFormData(
                "file",
                file.name,
                RequestBody.create(MediaType.parse("application/octet-stream"), file)
            )
            
            val request = Request.Builder()
                .url("$remoteBaseUrl/upload")
                .post(body)
                .build()
            
            withContext(Dispatchers.IO) {
                val response = OkHttpClient().newCall(request).execute()
                if (response.isSuccessful) {
                    Result.success(response.body?.string() ?: "")
                } else {
                    Result.failure(Exception("Upload failed: ${response.code}"))
                }
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    // 下载文件
    suspend fun downloadFile(fileId: String, fileName: String): Result<File> {
        return try {
            val request = Request.Builder()
                .url("$remoteBaseUrl/download/$fileId")
                .build()
            
            withContext(Dispatchers.IO) {
                val response = OkHttpClient().newCall(request).execute()
                if (response.isSuccessful) {
                    val file = File(localDir, fileName)
                    response.body?.byteStream()?.use { input ->
                        FileOutputStream(file).use { output ->
                            input.copyTo(output)
                        }
                    }
                    Result.success(file)
                } else {
                    Result.failure(Exception("Download failed: ${response.code}"))
                }
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

7. 最佳实践

7.1 架构模式

kotlin
// Repository 模式
class FileRepository(context: Context) {
    private val internalStorage = InternalStorage(context)
    private val externalStorage = ScopedStorage(context)
    
    // 内部存储
    fun saveUserData(user: User) {
        val json = Gson().toJson(user)
        internalStorage.writeToFile("user.json", json)
    }
    
    fun loadUserData(): User? {
        val json = internalStorage.readFromFile("user.json")
        return Gson().fromJson(json, User::class.java)
    }
    
    // 外部存储
    suspend fun saveImageToGallery(bitmap: Bitmap): Uri {
        return externalStorage.saveToPublicPictures("image_${System.currentTimeMillis()}.jpg", bitmap)
    }
}

// UseCase 模式
class SaveFileUseCase(private val repository: FileRepository) {
    operator fun invoke(file: File, content: String) {
        repository.saveFile(file, content)
    }
}

7.2 异步处理

kotlin
class AsyncFileOperations(context: Context) {
    private val ioDispatcher = Dispatchers.IO
    
    // 协程读取文件
    suspend fun readFileAsync(fileName: String): String {
        return withContext(ioDispatcher) {
            File(context.filesDir, fileName).readText()
        }
    }
    
    // 协程写入文件
    suspend fun writeFileAsync(fileName: String, content: String) {
        withContext(ioDispatcher) {
            File(context.filesDir, fileName).writeText(content)
        }
    }
    
    // 批量操作
    suspend fun processFilesAsync(files: List<File>) {
        files.forEachConcurrent(5) { file ->
            withContext(ioDispatcher) {
                processFile(file)
            }
        }
    }
}

7.3 错误处理

kotlin
class SafeFileOperations(context: Context) {
    private val filesDir = context.filesDir
    
    fun safeReadFile(fileName: String): Result<String> {
        return try {
            val file = File(filesDir, fileName)
            if (!file.exists()) {
                return Result.failure(FileNotFoundException("File not found: $fileName"))
            }
            Result.success(file.readText())
        } catch (e: IOException) {
            Result.failure(e)
        } catch (e: SecurityException) {
            Result.failure(e)
        }
    }
    
    fun safeWriteFile(fileName: String, content: String): Result<Unit> {
        return try {
            val file = File(filesDir, fileName)
            file.writeText(content)
            Result.success(Unit)
        } catch (e: IOException) {
            Result.failure(e)
        } catch (e: SecurityException) {
            Result.failure(e)
        }
    }
}

8. 面试考点

考点 1:内部存储和外部存储的区别

问题: 内部存储和外部存储有什么区别?

答案:

  • 内部存储/data/data/<package>/files/,应用私有,卸载删除
  • 外部存储/storage/emulated/0/,可共享,用户可删除
  • 适用场景:内部存储存配置,外部存储存媒体文件

考点 2:Android 10+ 分区存储

问题: Android 10 引入的分区存储是什么?

答案:

  • 限制应用访问公共文件
  • 使用 MediaStore API 访问公共目录
  • 使用 SAF 让用户选择文件
  • 应用专用目录仍可直接访问

考点 3:文件权限

问题: 如何请求文件访问权限?

答案:

  • Android 6-9:WRITE_EXTERNAL_STORAGE
  • Android 10:分区存储,部分需要权限
  • Android 11+:使用 MediaStore 或 SAF

考点 4:缓存管理

问题: 如何管理文件缓存?

答案:

  • 使用 LruCache 实现内存缓存
  • 使用 Cache 类实现磁盘缓存
  • 设置最大缓存大小
  • 定期清理过期缓存

考点 5:文件序列化

问题: 如何保存对象到文件?

答案:

  • Serializable(性能较差)
  • Parcelable(Android 专用)
  • JSON(Gson/Moshi)
  • Protobuf(高性能)

考点 6:MediaStore 使用

问题: 如何使用 MediaStore 保存图片?

答案:

kotlin
val values = ContentValues().apply {
    put(MediaStore.Images.Media.DISPLAY_NAME, "image.jpg")
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
}
contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)

考点 7:SAF 框架

问题: 什么是 Storage Access Framework?

答案:

  • 统一的文件选择器
  • 支持云存储
  • 持久化 URI 权限
  • 跨应用共享文件

考点 8:文件加密

问题: 如何加密文件?

答案:

  • EncryptedFile(AndroidX Security)
  • 自定义 AES 加密
  • Android Keystore

考点 9:性能优化

问题: 文件 IO 如何优化性能?

答案:

  • 使用缓冲流
  • 异步 IO
  • 批量操作
  • 内存映射文件

考点 10:常见错误

问题: 文件操作常见错误有哪些?

答案:

  • SecurityException(权限不足)
  • FileNotFoundException(文件不存在)
  • IOException(IO 错误)
  • 内存不足(大文件)

总结

Android 文件存储是应用开发的基础:

✅ 内部存储用于私有数据
✅ 外部存储用于共享文件
✅ 使用 MediaStore 访问公共目录
✅ 合理管理缓存
✅ 注意权限管理

学习建议:

  1. 掌握基本文件 IO 操作
  2. 理解分区存储机制
  3. 学会使用 MediaStore 和 SAF
  4. 实现合理的缓存策略

进阶方向: 文件存储 → 网络缓存 → 数据库 → 完整存储方案


本文档涵盖 Android 文件存储的核心知识点,建议配合实际项目练习以加深理解。