在Android开发中,文件存储一直是开发者面临的重要挑战。随着Android 10作用域存储的引入,传统的文件访问方式已被淘汰。本文将深入解析Storage Access Framework(SAF)的使用方法,助你轻松实现安全合规的文件访问。
一、为什么需要SAF?
在Android 4.4之前,开发者通常直接使用文件路径访问存储空间,这种方式存在明显缺陷:
kotlin
// 传统方式 - 存在安全风险且不兼容新系统
val file = File(Environment.getExternalStorageDirectory(), "myfile.txt")
file.writeText("Hello World")
SAF的核心优势:
- 🛡️ 增强安全性:应用只能访问用户明确选择的文件
- 👥 用户控制:用户完全掌控文件访问权限
- ☁️ 云端集成:统一访问本地和云存储(Google Drive等)
- 📱 版本兼容:完美适配Android 10+的作用域存储
二、SAF核心使用场景及完整实现
场景1:打开文件(读取内容)
kotlin
// 启动文件选择器
private fun openFile() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*" // 支持所有文件类型
// 可选:指定特定类型
// type = "image/*"
// 或多种类型
// putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "application/pdf"))
}
startActivityForResult(intent, REQUEST_OPEN_FILE)
}
// 处理结果
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_OPEN_FILE && resultCode == RESULT_OK) {
data?.data?.let { uri ->
// 获取持久化权限
takePersistentUriPermission(uri)
// 读取文件内容
readFileContent(uri)
}
}
}
private fun readFileContent(uri: Uri) {
try {
contentResolver.openInputStream(uri)?.use { stream ->
val text = stream.bufferedReader().readText()
// 处理文件内容...
Log.d("SAF", "File content: $text")
}
} catch (e: Exception) {
Log.e("SAF", "Error reading file", e)
}
}
场景2:创建并保存文件
kotlin
private fun createFile() {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/plain" // MIME类型
putExtra(Intent.EXTRA_TITLE, "new_file.txt") // 默认文件名
// 可选:设置初始目录(API 26+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri)
}
}
startActivityForResult(intent, REQUEST_CREATE_FILE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// ...其他请求处理
if (requestCode == REQUEST_CREATE_FILE && resultCode == RESULT_OK) {
data?.data?.let { uri ->
saveContentToFile(uri, "Hello SAF World!")
}
}
}
private fun saveContentToFile(uri: Uri, content: String) {
try {
contentResolver.openOutputStream(uri)?.use { stream ->
stream.write(content.toByteArray())
Toast.makeText(this, "文件保存成功", Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
Log.e("SAF", "Error saving file", e)
}
}
场景3:获取目录访问权限(管理多个文件)
kotlin
private fun openDirectory() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
// 可选:设置初始目录
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri)
}
}
startActivityForResult(intent, REQUEST_OPEN_DIR)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// ...其他请求处理
if (requestCode == REQUEST_OPEN_DIR && resultCode == RESULT_OK) {
data?.data?.let { treeUri ->
// 获取持久化权限
val flags = data.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
contentResolver.takePersistableUriPermission(treeUri, flags)
// 保存URI供后续使用
saveDirectoryUri(treeUri)
// 现在可以操作目录中的文件
listFilesInDirectory(treeUri)
}
}
}
private fun listFilesInDirectory(treeUri: Uri) {
val docId = DocumentsContract.getTreeDocumentId(treeUri)
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, docId)
val projection = arrayOf(
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_MIME_TYPE,
DocumentsContract.Document.COLUMN_SIZE
)
contentResolver.query(childrenUri, projection, null, null, null)?.use { cursor ->
while (cursor.moveToNext()) {
val name = cursor.getString(0)
val type = cursor.getString(1)
val size = cursor.getLong(2)
Log.d("SAF", "File: $name ($type) - ${size}bytes")
}
}
}
三、关键机制:URI持久化权限处理
kotlin
// 获取持久化权限
private fun takePersistentUriPermission(uri: Uri) {
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(uri, flags)
}
// 检查权限状态
private fun checkUriPermission(uri: Uri): Boolean {
val permissions = contentResolver.persistedUriPermissions
return permissions.any { it.uri == uri }
}
// 释放权限
private fun releaseUriPermission(uri: Uri) {
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.releasePersistableUriPermission(uri, flags)
}
四、SAF与传统存储访问对比
特性 | 传统文件访问 | SAF存储访问框架 |
---|---|---|
权限要求 | 需要READ/WRITE_EXTERNAL_STORAGE | 不需要存储权限 |
用户控制 | 无 | 用户明确选择文件/目录 |
访问范围 | 整个存储空间 | 仅限于用户选择的项目 |
云端支持 | 不支持 | 支持Google Drive等云服务 |
Android 10+兼容性 | 不兼容作用域存储 | 完全兼容 |
长期访问 | 永久访问 | 需显式获取持久化权限 |
五、SAF最佳实践与性能优化
-
正确处理URI生命周期:
kotlin// 保存URI字符串而非URI对象 val uriString = uri.toString() // 恢复时 val uri = Uri.parse(uriString)
-
大文件处理优化:
kotlin// 使用FileDescriptor提高性能 contentResolver.openFileDescriptor(uri, "r")?.use { pfd -> FileInputStream(pfd.fileDescriptor).use { stream -> // 处理大文件... } }
-
错误处理增强:
kotlintry { // SAF操作... } catch (e: FileNotFoundException) { // 文件不存在或被移动 } catch (e: SecurityException) { // 权限丢失 } catch (e: IOException) { // IO错误 }
-
MIME类型过滤技巧:
kotlin// 同时支持图片和PDF intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf( "image/*", "application/pdf" ))
六、实战进阶:文件操作工具类
kotlin
object SAFUtil {
// 创建文件(简化版)
fun createFile(context: Context, mimeType: String, filename: String) {
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = mimeType
putExtra(Intent.EXTRA_TITLE, filename)
context.startActivity(this)
}
}
// 复制文件
fun copyFile(context: Context, srcUri: Uri, destUri: Uri) {
context.contentResolver.openInputStream(srcUri)?.use { input ->
context.contentResolver.openOutputStream(destUri)?.use { output ->
input.copyTo(output)
}
}
}
// 获取文件信息
data class FileInfo(val name: String, val size: Long, val mimeType: String)
fun getFileInfo(context: Context, uri: Uri): FileInfo? {
context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val nameIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
val sizeIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE)
val mimeIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE)
return FileInfo(
cursor.getString(nameIndex),
cursor.getLong(sizeIndex),
cursor.getString(mimeIndex)
)
}
}
return null
}
}
七、关键点总结
-
启动流程:
graph TD A[创建Intent] --> B[设置类型/过滤器] B --> C[启动Activity] C --> D[用户选择文件] D --> E[处理返回的URI] E --> F[获取持久化权限] F --> G[通过ContentResolver操作文件] -
核心要点:
- 始终使用
ContentResolver
而非File
类 - 通过
takePersistableUriPermission
获取持久化权限 - 存储URI字符串而非URI对象
- 处理
SecurityException
等异常情况
- 始终使用
-
版本适配:
kotlin// 检查SAF可用性 fun isSAFAvailable(): Boolean { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT } // 兼容处理 if (isSAFAvailable()) { // 使用SAF } else { // 传统方式(仅支持旧设备) }
结语
SAF作为Android现代存储解决方案,不仅解决了权限和安全问题,还提供了统一的文件访问体验。通过本文的详细指南和完整代码示例,相信你已经掌握了SAF的核心用法。在实际开发中,建议:
- 优先使用SAF替代传统存储访问
- 为用户提供清晰的文件类型过滤
- 妥善处理URI权限生命周期
- 针对大文件操作进行性能优化