Android存储访问框架(SAF)

在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最佳实践与性能优化

  1. 正确处理URI生命周期

    kotlin 复制代码
    // 保存URI字符串而非URI对象
    val uriString = uri.toString()
    // 恢复时
    val uri = Uri.parse(uriString)
  2. 大文件处理优化

    kotlin 复制代码
    // 使用FileDescriptor提高性能
    contentResolver.openFileDescriptor(uri, "r")?.use { pfd ->
        FileInputStream(pfd.fileDescriptor).use { stream ->
            // 处理大文件...
        }
    }
  3. 错误处理增强

    kotlin 复制代码
    try {
        // SAF操作...
    } catch (e: FileNotFoundException) {
        // 文件不存在或被移动
    } catch (e: SecurityException) {
        // 权限丢失
    } catch (e: IOException) {
        // IO错误
    }
  4. 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
    }
}

七、关键点总结

  1. 启动流程

    graph TD A[创建Intent] --> B[设置类型/过滤器] B --> C[启动Activity] C --> D[用户选择文件] D --> E[处理返回的URI] E --> F[获取持久化权限] F --> G[通过ContentResolver操作文件]
  2. 核心要点

    • 始终使用ContentResolver而非File
    • 通过takePersistableUriPermission获取持久化权限
    • 存储URI字符串而非URI对象
    • 处理SecurityException等异常情况
  3. 版本适配

    kotlin 复制代码
    // 检查SAF可用性
    fun isSAFAvailable(): Boolean {
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
    }
    
    // 兼容处理
    if (isSAFAvailable()) {
        // 使用SAF
    } else {
        // 传统方式(仅支持旧设备)
    }

结语

SAF作为Android现代存储解决方案,不仅解决了权限和安全问题,还提供了统一的文件访问体验。通过本文的详细指南和完整代码示例,相信你已经掌握了SAF的核心用法。在实际开发中,建议:

  1. 优先使用SAF替代传统存储访问
  2. 为用户提供清晰的文件类型过滤
  3. 妥善处理URI权限生命周期
  4. 针对大文件操作进行性能优化
相关推荐
androidwork1 分钟前
Kotlinx序列化多平台兼容性详解
android·java·kotlin
移动开发者1号3 小时前
深入理解文件存储沙盒机制
android·kotlin
zhifanxu8 小时前
Kotlin 中ArrayList、listOf、arrayListOf 和 mutableListOf区别
kotlin
云博客-资源宝12 小时前
Android Manifest 权限描述大全
android·开发语言·php
xzkyd outpaper12 小时前
Android DataBinding 与 MVVM
android·计算机八股
zzq199613 小时前
Android 14.0 framework默认将三按钮的导航栏修改为手势导航。
android
ii_best13 小时前
[按键精灵安卓/ios脚本插件开发] 遍历获取LuaAuxLib函数库命令辅助工具
android·ios
峥嵘life15 小时前
Android Java语言转Kotlin语言学习指导实用攻略
android·java·kotlin
bubiyoushang88816 小时前
Kotlin中快速实现MVI架构
android·开发语言·kotlin