Appearance
文件存储:Android 文件 I/O 完整指南
目录
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 访问公共目录
✅ 合理管理缓存
✅ 注意权限管理
学习建议:
- 掌握基本文件 IO 操作
- 理解分区存储机制
- 学会使用 MediaStore 和 SAF
- 实现合理的缓存策略
进阶方向: 文件存储 → 网络缓存 → 数据库 → 完整存储方案
本文档涵盖 Android 文件存储的核心知识点,建议配合实际项目练习以加深理解。