文件存储
问题
Android 文件存储有哪些方式?Scoped Storage 带来了什么变化?
答案
存储区域分类
| 存储区域 | 路径 | 权限 | 卸载清除 | 适用 |
|---|---|---|---|---|
| 内部存储 | /data/data/<pkg>/files/ | 无需权限 | ✅ | 配置、缓存 |
| 内部缓存 | /data/data/<pkg>/cache/ | 无需权限 | ✅ | 临时文件 |
| 外部应用专属 | /sdcard/Android/data/<pkg>/ | 无需权限 | ✅ | 大文件 |
| 共享存储 | /sdcard/Pictures/ 等 | 需权限 | ❌ | 媒体文件 |
Scoped Storage(分区存储)
Android 10 引入、Android 11 强制执行。限制 App 直接访问外部共享存储:
使用 MediaStore 保存图片
suspend fun saveImage(context: Context, bitmap: Bitmap): Uri? {
return withContext(Dispatchers.IO) {
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 = context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues
) ?: return@withContext null
context.contentResolver.openOutputStream(uri)?.use { stream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream)
}
// 标记写入完成
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentValues.clear()
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
context.contentResolver.update(uri, contentValues, null, null)
}
uri
}
}
SAF 文件选择
// 注册 ActivityResultContract
val pickFile = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
uri?.let { readFileContent(it) }
}
// 打开文件选择器
pickFile.launch(arrayOf("application/pdf", "text/plain"))
private fun readFileContent(uri: Uri) {
// 持久化 URI 权限(跨生命周期)
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
contentResolver.openInputStream(uri)?.use { stream ->
// 读取文件内容
}
}
requestLegacyExternalStorage
android:requestLegacyExternalStorage="true" 仅在 targetSdk 29 时有效。targetSdk 30+ 必须使用 Scoped Storage API。不要依赖此属性。
常见面试问题
Q1: 内部存储和外部存储的区别?
答案:
- 内部存储:App 私有,其他应用无法访问,无需申请权限,卸载时自动清除。适合敏感数据。
- 外部应用专属存储:同样无需权限且卸载时清除,但在物理上位于外部存储(SD 卡或内置存储公共区域),空间更大。适合大文件如下载的视频。
- 外部共享存储:所有应用可通过 MediaStore 访问的公共区域,卸载后文件保留。
Q2: FileProvider 是什么?什么时候需要用?
答案:
Android 7.0+ 禁止通过 file:// URI 跨应用共享文件,必须使用 FileProvider 生成 content:// URI。常见场景:调用系统相机拍照后获取照片、安装 APK、分享文件给其他应用。
val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "image/jpeg")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}