FEATURED · 精选文章

Android存储权限演进与Operation not permitted异常全解析

发布时间 / 2026/8/12 16:36:08
来源 / 创域科博编辑部
栏目 / 资讯中心
Android存储权限演进与Operation not permitted异常全解析 1. 项目概述一个困扰安卓开发者多年的“顽疾”如果你是一名安卓开发者并且你的应用需要访问设备的外部存储也就是我们常说的SD卡那么你大概率遇到过这个令人头疼的异常java.io.IOException: Operation not permitted。这个错误就像一个幽灵从早期的安卓版本一直飘荡到最新的Android 13每次安卓系统权限收紧它就会以新的面貌出现让不少开发者尤其是刚入行的朋友调试到怀疑人生。简单来说这个错误就是系统告诉你“对不起你没有权限进行这个文件操作。”这个问题之所以复杂是因为它不是一个固定的“Bug”而是一系列随着安卓系统版本演进不断变化的“存储访问权限模型”所导致的结果。从最初的“只要声明权限就能随便读写在SD卡根目录”到Android 6.0引入的动态权限申请再到Android 10引入的“分区存储Scoped Storage”革命以及后续Android 11、12、13的持续调整和细化访问外部存储的规则发生了天翻地覆的变化。很多老代码、网上的旧教程在新的系统上直接运行就会报这个错而很多新开发者又对这套复杂的历史包袱缺乏系统性的了解。因此这个项目的核心目标就是彻底梳理从旧版本到Android 13Target SDK 33的SD卡读写兼容方案。它不是提供一个“万能代码块”而是为你构建一个清晰的认知地图和一套可落地的分层解决方案。让你明白在不同API等级targetSdkVersion和设备系统版本下应该用什么方法在哪里会碰壁以及如何优雅地绕过去。最终我们将得到一个能够“一次编写兼容各版本”的稳健工具类或策略。2. 安卓外部存储权限演进史理解“Operation not permitted”的根源要解决问题必须先理解问题是如何产生的。Operation not permitted的本质是权限不足而权限模型的变化是核心驱动力。我们可以把安卓的外部存储访问历史划分为几个关键时代。2.1 蛮荒时代Android 4.4及以前在这个时代访问外部存储相对“自由”。你只需要在AndroidManifest.xml文件中声明相应的权限即可!-- 写入外部存储 -- uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE/ !-- 读取外部存储Android 4.4前读权限隐含在写权限中 -- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE/声明之后你的应用几乎可以在SD卡的任何路径如/storage/emulated/0/或/sdcard/下自由创建、读写、删除文件和文件夹。Environment.getExternalStorageDirectory()返回的就是这个根路径。此时出现Operation not permitted多半是路径错误、文件被占用或存储介质本身出了问题如SD卡损坏、未挂载。注意虽然这个时代很“自由”但滥用会导致用户存储空间混乱应用随意创建文件夹这也是谷歌决心改革的重要原因。2.2 动态权限时代Android 6.0 - Android 9Android 6.0引入了运行时权限机制。即使你在Manifest中声明了WRITE_EXTERNAL_STORAGE在应用运行时你仍然需要在代码中向用户弹窗申请该权限。用户可以选择“允许”或“拒绝”。如果用户拒绝你尝试写文件时就会抛出Operation not permitted。关键变化权限申请需要使用ActivityCompat.requestPermissions在运行时申请。权限检查使用ContextCompat.checkSelfPermission检查是否已授权。READ_EXTERNAL_STORAGE在Android 6.0上如果你只声明了写权限你仍然可以写文件但读文件需要单独申请读权限。这有点反直觉需要注意。这个时代Environment.getExternalStorageDirectory()依然可用但前提是用户授予了权限。兼容方案的核心是处理好动态权限的申请逻辑和用户拒绝后的降级处理。2.3 分区存储时代Android 10及以后这是变革最大的一代。Android 10默认启用了“分区存储Scoped Storage”。其核心思想是应用只能无条件访问自己专属的沙盒目录Context.getExternalFilesDir()等以及通过系统文件选择器如ACTION_OPEN_DOCUMENT,ACTION_CREATE_DOCUMENT或MediaStore API访问的公共媒体文件图片、视频、音频。对于其他应用创建的任意非媒体文件以及SD卡根目录应用默认无权直接通过文件路径访问。这直接导致了Operation not permitted的高发如果你的targetSdkVersion设置为29Android 10或更高即使你拥有WRITE_EXTERNAL_STORAGE权限你也不再能通过FileAPI在SD卡根目录或其他应用的私有目录下随意创建文件。这个权限在Android 10上被极大地限制了。尝试使用new File(Environment.getExternalStorageDirectory(), “myApp/test.txt”)并调用file.createNewFile()在Android 10的设备上大概率会抛出java.io.IOException: Operation not permitted。Android 11和12进一步细化了规则权限自动重置如果用户长时间未使用应用系统会自动重置其已授予的运行时权限。更多权限细分引入了管理外部存储的权限MANAGE_EXTERNAL_STORAGE这是一个“特殊”权限需要应用上架到Google Play等商店并经过审核声明用于文件管理器、备份工具等特定用途才能申请。普通应用不应使用。过滤外部存储访问对MediaStore的访问也做了更严格的过滤。Android 13引入了新的媒体权限将READ_EXTERNAL_STORAGE拆分为独立的图片、视频、音频读取权限需要单独申请。但对我们讨论的“写入”和“非媒体文件访问”问题其根本性的分区存储限制与Android 10-12一脉相承。理解这段历史后我们就明白一个兼容方案必须能识别当前运行环境设备系统版本、应用targetSdkVersion并动态选择正确的API和路径进行文件操作。3. 分层兼容方案设计与核心思路拆解面对如此复杂的版本差异一个“if-else”满天飞的方案是难以维护的。我们的设计思路是分层和策略模式为不同的场景和版本提供不同的访问策略并通过一个统一的工具类进行封装对外提供简洁的接口。3.1 方案核心设计原则优先使用应用私有目录对于应用自己产生的、无需与其他应用共享的缓存文件、临时文件、配置文件无条件优先使用Context.getExternalFilesDir()或Context.getCacheDir()。这些路径永远可读写无需任何权限。这是解决存储问题的首要准则。公共文件使用MediaStore或SAF对于需要存入公共相册的图片、视频或让用户选择保存位置的文档使用MediaStoreAPI或系统文件选择器。兼容旧路径的降级策略对于某些必须访问旧路径如兼容老数据、使用某些特定库的场景我们需要一套降级策略在Android 10以下使用老方法在Android 10及以上尝试申请特殊权限或引导用户操作。明确权限申请逻辑清晰地区分哪些操作需要什么权限并在合适的时机如应用启动、用户触发保存动作时申请。3.2 整体架构图逻辑描述我们的工具类StorageCompatHelper将包含以下核心模块环境检测器用于判断当前设备SDK版本、是否启用了分区存储、是否拥有某些权限。路径解析器根据文件类型私有、公共图片、公共下载、自定义等和当前环境返回正确的可访问路径或Uri。文件操作引擎封装基本的创建、读、写、删除操作内部根据路径解析器的结果决定是使用传统的java.io.FileAPI还是使用ContentResolver和MediaStore或是通过DocumentFile进行操作。权限管理模块封装运行时权限的检查、申请和回调处理。当外部调用saveImageToPublic(bitmap, “myPhoto”)时流程如下环境检测器判断系统版本Android 10且targetSdk29。路径解析器返回策略应使用MediaStore.Images.Media插入内容提供器获取一个Uri。文件操作引擎通过ContentResolver.openOutputStream(uri)来写入图片数据。如果系统版本Android 10路径解析器返回策略使用Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)获取路径并使用FileAPI写入。这样调用者无需关心底层实现工具类自动处理了兼容性问题。4. 核心代码实现与分版本适配详解下面我们进入具体的代码实现环节。我们将构建一个简化的但功能核心完备的兼容工具类。4.1 基础环境判断与常量定义首先我们需要一些关键的判断条件。// 使用 Kotlin 示例Java版本逻辑类似 object StorageCompatHelper { // 判断是否运行在Android 10 (API 29) 及以上 val isAtLeastQ: Boolean Build.VERSION.SDK_INT Build.VERSION_CODES.Q // 判断targetSdkVersion是否 29 (影响分区存储的默认行为) // 注意需要在构建时从BuildConfig获取或者通过反射/其他方式判断。 // 更常见的做法是我们在开发时就知道targetSdk并据此编写条件代码。 // 这里我们假设我们知道我们的targetSdk是 29的所以分区存储规则生效。 // 实际上对于要兼容旧版APK在Android 10设备运行的情况还需要判断应用是否在清单中设置了requestLegacyExternalStorage。 private fun isScopedStorageEnabled(context: Context): Boolean { return if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { // Android 10 设备上如果targetSdk29且未设置android:requestLegacyExternalStorage”true”则启用分区存储。 // 这里简化处理通常我们开发时若targetSdk29就按分区存储来编码。 true // 假设我们面对的是启用分区存储的环境 } else { false } } // 常用目录类型 enum class DirType { APP_PRIVATE, // 应用私有目录无需权限 PUBLIC_PICTURES, // 公共图片目录 PUBLIC_DOWNLOADS, // 公共下载目录 PUBLIC_DOCUMENTS, // 公共文档目录 (Android 5.0) EXTERNAL_CACHE, // 外部缓存目录 // 可以添加更多... } }4.2 获取安全路径的策略实现这是最核心的部分。我们提供一个方法根据目录类型返回一个安全的、当前环境下可用的文件对象或Uri。fun getSafeDir(context: Context, dirType: DirType): File? { return when (dirType) { DirType.APP_PRIVATE - { // 应用私有文件目录永远可访问 // 通常位于 /storage/emulated/0/Android/data/package_name/files/ context.getExternalFilesDir(null) } DirType.EXTERNAL_CACHE - { // 应用外部缓存目录也永远可访问 context.externalCacheDir } DirType.PUBLIC_PICTURES, DirType.PUBLIC_DOWNLOADS, DirType.PUBLIC_DOCUMENTS - { // 对于公共目录需要分版本处理 if (!isScopedStorageEnabled(context)) { // 旧版本方式使用 Environment.getExternalStoragePublicDirectory val publicDirName when (dirType) { DirType.PUBLIC_PICTURES - Environment.DIRECTORY_PICTURES DirType.PUBLIC_DOWNLOADS - Environment.DIRECTORY_DOWNLOADS DirType.PUBLIC_DOCUMENTS - Environment.DIRECTORY_DOCUMENTS else - null } publicDirName?.let { Environment.getExternalStoragePublicDirectory(it) } } else { // Android 10 分区存储下不能直接使用上述方法获取File对象并写入。 // 对于媒体文件图片、视频、音频应通过MediaStore插入。 // 对于非媒体文件如下载的PDF应使用SAF或MediaStore.Downloads。 // 因此这个方法在分区存储下返回null意味着调用者需要使用其他API。 null } } } }可以看到对于公共目录在分区存储环境下我们返回了null。这意味着我们需要另一套方案。4.3 分区存储下的文件操作MediaStore与SAF场景一保存一张图片到公共相册。RequiresApi(Build.VERSION_CODES.Q) fun saveImageToMediaStore(context: Context, bitmap: Bitmap, displayName: String): Uri? { val resolver context.contentResolver // 1. 设置图片信息 val contentValues ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, “$displayName.jpg”) put(MediaStore.MediaColumns.MIME_TYPE, “image/jpeg”) // 指定保存到 Pictures 目录 put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES “/MyAppName/”) // Android Q以上可以设置IS_PENDING状态写入完成后再发布 if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { put(MediaStore.MediaColumns.IS_PENDING, 1) } } // 2. 插入一条记录获取Uri val imageUri resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) imageUri?.let { uri - try { resolver.openOutputStream(uri)?.use { outputStream - // 3. 将Bitmap写入到OutputStream bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream) } // 4. 写入完成更新IS_PENDING状态Q以上 if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { contentValues.clear() contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0) resolver.update(uri, contentValues, null, null) } return uri } catch (e: IOException) { Log.e(“StorageCompat”, “Failed to save image”, e) // 失败则删除已创建的条目 resolver.delete(uri, null, null) } } return null }实操心得使用IS_PENDING标志是个好习惯。在写入完成前其他应用看不到这个文件可以避免文件处于不完整状态时被其他应用访问。写入完成后将其置为0文件就正式发布了。场景二在公共目录如下载目录创建并写入一个自定义格式的文本文件。对于非媒体文件在Android 10上如果希望保存到Downloads等公共目录推荐使用系统文件选择器Storage Access Framework, SAF让用户自己选择保存位置。这不需要MANAGE_EXTERNAL_STORAGE权限也符合谷歌的设计规范。// 在Activity或Fragment中启动SAF创建文件 private val createFileLauncher registerForActivityResult(ActivityResultContracts.CreateDocument(“text/plain”)) { uri - uri?.let { saveContentToUri(it, “Hello, World!”) } } fun createTextFileViaSAF() { createFileLauncher.launch(“my_document.txt”) } private fun saveContentToUri(uri: Uri, content: String) { try { contentResolver.openOutputStream(uri)?.use { outputStream - outputStream.write(content.toByteArray()) } // 保存成功提示 } catch (e: Exception) { Log.e(“StorageCompat”, “Failed to write via SAF”, e) } }如果应用有文件管理器类的需求必须访问所有文件那就需要申请前面提到的MANAGE_EXTERNAL_STORAGE权限并使用Environment.isExternalStorageManager()进行检查跳转到系统设置页面让用户授权。但普通应用应尽量避免此路径。4.4 旧版本兼容与降级写入对于必须使用旧路径的极端情况例如需要读取旧版本存储在SD卡根目录下的数据我们可以提供一个降级方法并在Android 10上提示用户或尝试申请管理权限。fun writeToLegacyPath(context: Context, fileName: String, content: String): Boolean { val legacyDir Environment.getExternalStorageDirectory() val targetFile File(legacyDir, “${context.packageName}/$fileName”) return if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { // 旧版本正常写入需已授权WRITE_EXTERNAL_STORAGE writeFileWithTraditionalWay(targetFile, content) } else { // Android 10 if (Environment.isExternalStorageManager()) { // 用户已授予管理所有文件权限 writeFileWithTraditionalWay(targetFile, content) } else { // 没有权限无法写入。可以引导用户去设置。 // 例如showDialogToRequestManageStoragePermission(context) false } } } private fun writeFileWithTraditionalWay(file: File, content: String): Boolean { return try { file.parentFile?.mkdirs() file.writeText(content) true } catch (e: SecurityException) { Log.e(“StorageCompat”, “SecurityException - Permission denied”, e) false } catch (e: IOException) { Log.e(“StorageCompat”, “IOException”, e) false } }5. 权限申请的最佳实践与封装动态权限申请是兼容方案中不可或缺的一环。我们需要一个健壮且用户友好的权限申请流程。5.1 权限检查与申请封装我们封装一个权限请求工具处理检查、申请和回调。object PermissionHelper { // 定义需要的权限组 private val STORAGE_PERMISSIONS if (Build.VERSION.SDK_INT Build.VERSION_CODES.P) { // Android 9.0及以下需要读写权限 arrayOf( Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE ) } else if (Build.VERSION.SDK_INT Build.VERSION_CODES.R) { // Android 10-11写权限受限但申请流程仍需要用于MediaStore写入等 arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE) // WRITE_EXTERNAL_STORAGE在Android 10上对大多数路径无效但某些API如MediaStore写入可能仍会检查建议也申请。 // 实际测试中Android 10上只申请READ通常也够用但为了兼容性可以都申请。 } else { // Android 12 READ_EXTERNAL_STORAGE 仍然需要 arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE) } fun checkAndRequestStoragePermissions(activity: FragmentActivity, requestCode: Int): Boolean { val permissionsToRequest mutableListOfString() STORAGE_PERMISSIONS.forEach { permission - if (ContextCompat.checkSelfPermission(activity, permission) ! PackageManager.PERMISSION_GRANTED) { permissionsToRequest.add(permission) } } return if (permissionsToRequest.isNotEmpty()) { // 有权限未授予发起申请 activity.requestPermissions(permissionsToRequest.toTypedArray(), requestCode) false // 权限未就绪 } else { true // 所有权限已授予 } } // 处理权限申请结果 fun onRequestPermissionsResult(requestCode: Int, permissions: Arrayout String, grantResults: IntArray): Boolean { var allGranted true grantResults.forEach { result - if (result ! PackageManager.PERMISSION_GRANTED) { allGranted false } } return allGranted } }注意事项从Android 13开始READ_EXTERNAL_STORAGE被细分为READ_MEDIA_IMAGES,READ_MEDIA_VIDEO,READ_MEDIA_AUDIO。如果你的应用只需要访问图片可以只申请图片权限这样对用户更友好。我们的STORAGE_PERMISSIONS数组需要根据targetSdkVersion进一步细化。5.2 处理用户“拒绝且不再询问”当用户拒绝权限并勾选“不再询问”后下次调用requestPermissions将不会弹出对话框。此时我们应该引导用户去应用设置页面手动开启权限。fun shouldShowRequestPermissionRationale(activity: FragmentActivity, permission: String): Boolean { return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission) } fun openAppSettings(activity: Activity) { val intent Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { data Uri.fromParts(“package”, activity.packageName, null) } activity.startActivity(intent) }在申请权限前可以先判断shouldShowRequestPermissionRationale。如果返回false且权限未被授予说明用户可能选择了“不再询问”此时应该弹出一个自定义对话框解释为什么需要这个权限并提供一个按钮跳转到openAppSettings。6. 常见问题排查与实战避坑指南在实际开发中即使按照上述方案依然可能遇到各种诡异的Operation not permitted。下面是我踩过的一些坑和解决方案。6.1 问题排查清单问题现象可能原因排查步骤与解决方案Android 9以下设备正常Android 10报错分区存储导致。应用targetSdkVersion 29且未启用旧版存储模式。1. 检查AndroidManifest.xml中是否设置了android:requestLegacyExternalStorage”true”仅对targetSdk29有效targetSdk30时无效。2. 将文件操作迁移到应用私有目录或使用MediaStore/SAFAPI。已动态申请并授予权限仍报错1. 权限组理解有误Android 6.0-8.1。2. 在Android 10上试图用FileAPI写公共非媒体目录。3. 路径错误或父目录不存在。1. 确认申请的权限包含READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE针对旧版本。2. 在Android 10设备上使用adb shell appops get package_name命令检查MANAGE_EXTERNAL_STORAGE权限状态。确认是否在使用FileAPI访问受限路径。3. 打印目标文件路径检查其父目录File.mkdirs()是否成功。通过SAF或文件选择器获取的Uri后续无法写入获取的Uri权限是临时的。应用重启或系统清理后权限可能丢失。使用ContentResolver.takePersistableUriPermission()获取持久化权限。在onActivityResult中处理返回的Uri时调用contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)MediaStore插入图片后相册里不显示或延迟显示1. 未正确设置IS_PENDING标志Android Q。2. 媒体库扫描延迟。1. 确保在写入完成后将IS_PENDING更新为0。2. 插入后可以发送一个广播通知媒体扫描器context.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri))。但更推荐使用MediaScannerConnection.scanFile。在Android 11上即使有MANAGE_EXTERNAL_STORAGE权限访问某些路径仍报错权限作用范围限制。该权限并非真正的“Root”权限对Android/data等系统保护目录的访问依然受限。避免访问/Android/data/,/Android/obb/等受保护的应用私有目录。如果必须访问其他应用的数据几乎不可能这是系统设计的隐私保护。6.2 独家避坑技巧善用Context.getExternalFilesDir(String type)这个方法不仅可以获取私有文件根目录还可以传入Environment中的常量如DIRECTORY_PICTURES、DIRECTORY_DOCUMENTS等在私有目录下创建对应类型的子目录。系统文件管理器和一些备份工具会识别这些目录体验更好。val appPrivatePicturesDir context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) // 路径类似/storage/emulated/0/Android/data/package_name/files/Pictures这个目录无需任何权限且会在应用卸载时被清理非常适合存放应用生成的不需要与其他应用共享的图片、文档。谨慎处理File对象的listFiles()和exists()在分区存储下对没有权限的路径调用这些方法可能返回null或空数组而不会直接抛出异常。这可能导致逻辑错误。在操作前最好用Environment.getExternalStorageState()检查存储介质状态并用File.canRead()/File.canWrite()进行能力探测注意在分区存储下即使返回false也不一定是因为没权限可能是路径根本不可访问。使用DocumentFile替代File处理SAF Uri对于通过SAF获取的目录树UriIntent.ACTION_OPEN_DOCUMENT_TREE使用DocumentFile.fromTreeUri(context, uri)来构建一个DocumentFile对象。它的API与File类似listFiles(),createFile(),delete()等但它是操作SAF权限下文件的最佳实践能避免很多权限问题。为FileProvider配置好paths当需要在应用间共享文件如调用相机拍照后存储临时图片或分享文件给其他应用时必须使用FileProvider来生成content://Uri。确保在res/xml/file_paths.xml中正确配置了路径覆盖你所有需要共享的目录如外部缓存目录、私有文件目录下的特定子目录。Android 13的细粒度媒体权限如果你的targetSdkVersion即将或已经升级到33务必处理新的媒体权限。在申请前使用ActivityCompat.checkSelfPermission检查READ_MEDIA_IMAGES等权限。可以使用PermissionChecker类来帮助判断。对于只需要保存媒体文件而不需要读取其他应用媒体的应用可能只需要申请WRITE_EXTERNAL_STORAGE在Android 10上其作用已变化但保存媒体文件到MediaStore时仍可能需要或根本不需要这些权限如果只保存到私有目录。7. 完整工具类整合与使用示例最后我们将上述分散的模块整合成一个易于使用的工具类部分简化并展示一个完整的保存图片的场景。// StorageCompatHelper.kt object StorageCompatHelper { fun saveImage(context: Context, bitmap: Bitmap, fileName: String, isPublic: Boolean false): Uri? { return if (isPublic) { // 希望保存到公共相册 if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { // Android 10 使用MediaStore saveImageToMediaStore(context, bitmap, fileName) } else { // Android 9及以下使用旧方法需先检查权限 if (PermissionHelper.checkAndRequestStoragePermissions(context as FragmentActivity, REQUEST_CODE_SAVE_PUBLIC)) { saveImageToLegacyPublicDir(context, bitmap, fileName) } else { // 权限申请已发起等待回调 // 这里需要在实际的Activity/Fragment中处理权限回调 null } } } else { // 保存到应用私有目录最简单安全 saveImageToPrivateDir(context, bitmap, fileName) } } private fun saveImageToPrivateDir(context: Context, bitmap: Bitmap, fileName: String): Uri? { val privateDir context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) val outputFile File(privateDir, fileName) return try { FileOutputStream(outputFile).use { fos - bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos) } // 返回一个file:// Uri或者可以通过FileProvider生成content:// Uri供分享 Uri.fromFile(outputFile) } catch (e: Exception) { Log.e(“StorageCompat”, “Save to private failed”, e) null } } // ... 其他内部方法如 saveImageToMediaStore, saveImageToLegacyPublicDir 参考前面章节 } // 在Activity中的使用示例 class MainActivity : AppCompatActivity() { companion object { private const val REQUEST_CODE_SAVE_PUBLIC 1001 } fun onSaveButtonClicked() { // 假设有一个bitmap需要保存 val bitmap: Bitmap ... // 保存到私有目录无需权限询问 val privateUri StorageCompatHelper.saveImage(this, bitmap, “my_private_photo.jpg”, false) if (privateUri ! null) { Toast.makeText(this, “已保存到应用私有相册”, Toast.LENGTH_SHORT).show() } // 保存到公共相册可能需要权限申请 val publicUri StorageCompatHelper.saveImage(this, bitmap, “my_public_photo.jpg”, true) // 注意因为保存到公共相册在旧版本涉及异步权限申请所以可能不会立即返回Uri。 // 更好的做法是在onRequestPermissionsResult回调中处理保存逻辑。 } override fun onRequestPermissionsResult(requestCode: Int, permissions: Arrayout String, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode REQUEST_CODE_SAVE_PUBLIC) { if (PermissionHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)) { // 用户授予权限重新执行保存操作 onSaveButtonClicked() // 或者执行一个特定的保存任务 } else { Toast.makeText(this, “需要存储权限才能保存到公共相册”, Toast.LENGTH_LONG).show() } } } }这套方案从原理到实践覆盖了从旧版Android到Android 13的SD卡读写兼容性问题。核心思想是拥抱变化优先使用系统推荐的、无需权限或权限明确的API私有目录、MediaStore、SAF对于遗留代码或特殊需求做好版本判断和降级处理并清晰地向用户解释权限用途。理解背后的“为什么”远比复制一段“万能代码”更重要。在实际项目中你可以根据这个框架进一步封装成更适合自己业务需求的存储工具库。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻