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. 针对大文件操作进行性能优化
相关推荐
xq952714 小时前
lambda与匿名内部类 java和kotlin 对比
kotlin
蜀中廖化15 小时前
Android Studio 导入 opencv
android·opencv·android studio
奋斗的小鹰15 小时前
ASM Bytecode Viewer 插件查看kotlin和java文件的字节码
android·kotlin·asm
欢喜躲在眉梢里17 小时前
mysql中的日志
android·运维·数据库·mysql·adb·日志·mysql日志
路上^_^18 小时前
安卓基础组件019-引导页布局001
android·安卓
梦终剧19 小时前
【Android之路】UI消息循环机制
android·ui
zh_xuan19 小时前
Android android.util.LruCache源码阅读
android·源码阅读·lrucache
梦终剧19 小时前
【Android之路】安卓资源与编译初步
android
mykrecording21 小时前
launch Activity流程
android·app
xq952721 小时前
kotlin 基础语法
kotlin