当前位置: 首页 > news >正文

从‘无法访问’到‘轻松保存’:一个Android相册保存功能的重构实战(TargetSdkVersion 30+)

从‘无法访问’到‘轻松保存’:Android相册保存功能的重构实战

最近在重构一个老项目的相册保存功能时,遇到了一个典型问题:在Android 10及以上版本中,用户保存的图片虽然在文件系统中确实存在,但在系统相册里却怎么也找不到。这让我意识到,传统的File API和绝对路径方式已经不再适用于新版本的Android系统。经过一番折腾,终于找到了解决方案,现在分享给大家这个完整的重构过程。

1. 理解Scoped Storage的核心变化

Android 10引入的Scoped Storage从根本上改变了应用访问外部存储的方式。过去,我们习惯使用Environment.getExternalStorageDirectory()获取根目录,然后拼接路径来操作文件。这种方式在新系统上会遇到两个主要问题:

  1. 访问限制:应用只能访问自己的App-specific目录和通过MediaStore授权的媒体文件
  2. 可见性问题:即使文件写入成功,系统相册可能不会立即更新显示

关键变化对比表

特性Android 9及以下Android 10+
存储访问模式自由访问分区存储
文件可见性直接可见需通过MediaStore
权限要求WRITE_EXTERNAL_STORAGE更细粒度的控制
路径访问允许绝对路径限制使用绝对路径

提示:从Android 11开始,即使获取了MANAGE_EXTERNAL_STORAGE权限,Google Play也会对使用此权限的应用进行严格审核,建议仅在绝对必要时使用。

2. 重构相册保存功能的关键步骤

2.1 权限声明调整

首先需要更新AndroidManifest.xml中的权限声明:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />

对于Android 10及以上版本,实际上WRITE_EXTERNAL_STORAGE权限已经不再需要(除非你针对的是Android 9及以下设备)。但为了兼容性,我们仍然声明它,并通过maxSdkVersion限制其在高版本上的使用。

2.2 使用MediaStore API保存图片

核心的保存逻辑需要改用MediaStore API:

fun saveImageToGallery(context: Context, bitmap: Bitmap): Uri? { val contentValues = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, "IMG_${System.currentTimeMillis()}.jpg") put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000) put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) } return try { val uri = context.contentResolver.insert( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues ) uri?.let { context.contentResolver.openOutputStream(it).use { outputStream -> if (bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)) { // 通知媒体扫描器刷新 val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri) context.sendBroadcast(mediaScanIntent) uri } else { context.contentResolver.delete(uri, null, null) null } } } } catch (e: Exception) { e.printStackTrace() null } }

2.3 处理Android 11的特殊情况

Android 11引入了更严格的存储访问策略。如果你的应用需要访问其他应用创建的文件,或者需要频繁访问大量媒体文件,可能需要申请MANAGE_EXTERNAL_STORAGE权限:

fun checkAndRequestManageStoragePermission(activity: Activity) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) intent.data = Uri.parse("package:${activity.packageName}") activity.startActivityForResult(intent, REQUEST_CODE_MANAGE_STORAGE) } }

注意:使用MANAGE_EXTERNAL_STORAGE权限的应用在提交到Google Play时需要进行额外的声明和审核。

3. 兼容性处理与边缘情况

3.1 多版本兼容方案

为了兼容不同Android版本,可以采用策略模式:

interface ImageSaver { fun saveImage(context: Context, bitmap: Bitmap): Uri? } class LegacyImageSaver : ImageSaver { override fun saveImage(context: Context, bitmap: Bitmap): Uri? { // 实现Android 9及以下的保存逻辑 } } class ScopedStorageImageSaver : ImageSaver { override fun saveImage(context: Context, bitmap: Bitmap): Uri? { // 实现Android 10+的保存逻辑 } } fun getImageSaver(): ImageSaver { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { ScopedStorageImageSaver() } else { LegacyImageSaver() } }

3.2 常见问题排查

在实际重构过程中,可能会遇到以下问题:

  1. 图片保存成功但相册不显示

    • 确保调用了媒体扫描Intent
    • 检查ContentValues中是否设置了正确的MIME类型
    • 确认文件确实写入了正确的存储位置
  2. 权限被拒绝

    • Android 10+上WRITE_EXTERNAL_STORAGE权限实际上不再有效
    • 对于媒体文件,只需要READ_EXTERNAL_STORAGE权限即可写入
  3. 文件重复问题

    • 在生成文件名时加入时间戳或UUID确保唯一性
    • 可以使用MediaStore的IS_PENDING标志进行临时写入

4. 性能优化与用户体验提升

4.1 异步处理保存操作

图片保存可能是个耗时操作,应该放在后台线程执行:

fun saveImageWithCallback( context: Context, bitmap: Bitmap, callback: (Uri?) -> Unit ) { CoroutineScope(Dispatchers.IO).launch { val uri = saveImageToGallery(context, bitmap) withContext(Dispatchers.Main) { callback(uri) } } }

4.2 进度反馈与错误处理

为用户提供保存进度反馈:

sealed class SaveResult { data class Progress(val percent: Int) : SaveResult() data class Success(val uri: Uri) : SaveResult() data class Failure(val exception: Exception) : SaveResult() } fun saveImageWithProgress( context: Context, bitmap: Bitmap, onResult: (SaveResult) -> Unit ) { // 实现带进度回调的保存逻辑 }

4.3 批量保存优化

如果需要保存多张图片,可以考虑以下优化:

  1. 使用MediaStore的批量操作API
  2. 预先生成所有ContentValues
  3. 使用单一媒体扫描通知刷新整个目录
fun saveMultipleImages(context: Context, bitmaps: List<Bitmap>): List<Uri> { val uris = mutableListOf<Uri>() val resolver = context.contentResolver bitmaps.forEachIndexed { index, bitmap -> val values = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, "IMG_${System.currentTimeMillis()}_$index.jpg") // 其他字段... } val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) uri?.let { resolver.openOutputStream(it)?.use { stream -> bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream) uris.add(it) } } } // 只需发送一次广播刷新 if (uris.isNotEmpty()) { val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) mediaScanIntent.data = uris.first() context.sendBroadcast(mediaScanIntent) } return uris }

在实际项目中,重构后的相册保存功能不仅解决了兼容性问题,还提升了用户体验。通过合理使用MediaStore API和处理好各种边缘情况,我们的应用现在能够在所有Android版本上可靠地保存图片到相册。

http://www.cnnetsun.cn/news/2218047.html

相关文章:

  • 华硕笔记本终极控制神器GHelper:免费轻量级性能优化完全指南
  • 观察taotoken在多地域访问下的路由优化与容灾表现
  • C++量子计算模拟框架深度对比(QPP、QCL、XACC三强实测报告)
  • MYC-YG2UL工业级SoM:异构计算与工业应用解析
  • 抖音批量下载完整指南:一键保存所有喜爱内容
  • 掌握Notepad--文件关联配置:打造你的专属文本编辑体验
  • 终极指南:如何用Firmware Extractor一键提取20+种Android固件格式
  • Taotoken 多模型聚合平台为数据分析工作流注入 AI 动力
  • 当科学发现遇上个人偏见:从光电效应看学术争议如何塑造物理学史
  • 如何用BilibiliDown高效下载B站视频?全面解析这款开源工具的实用技巧
  • 通过curl命令直接测试Taotoken聊天接口的完整步骤
  • Obsidian Excel插件技术解析:在知识库中实现结构化数据工作流
  • Tushare Pro接口保姆级入门:从注册Token到获取第一份股票日线数据
  • 哔咔漫画下载器:打造个人永久漫画库的完整解决方案
  • OpenCV玩转热力图:用applyColorMap给灰度图‘穿’上22套‘皮肤’,Python代码保姆级教程
  • 为什么Linux内核开发者集体反对C++27协程默认调度器?——嵌入式+实时OS场景下不可绕过的7个硬约束
  • 网盘直链解析技术全解析:突破下载限制的专业解决方案
  • LibreOffice Calc表格高手进阶:用Basic宏自动抓取网页数据并生成图表
  • Obsidian终极图表指南:三步搞定专业绘图,让笔记可视化升级
  • ESPi开发板双版本解析:硬件架构与物联网应用
  • OpenClaw实战:AI代理自动化系统的生产级架构与技能工厂设计
  • 终极指南:如何让Windows电脑变身苹果AirPlay接收器
  • 别再只查Body和URL了!Postman报400错误的5个隐蔽排查点(含Host问题详解)
  • 用Unity EventSystems打造高级UI拖拽:实现背包系统与装备栏交互(附完整C#脚本)
  • 别再只用gzip了!手把手教你为Vite+Vue项目配置Brotli压缩,打包体积再瘦身
  • 二刷 LeetCode:62. 不同路径 64. 最小路径和 复盘笔记
  • RKNN模型量化精度上不去?试试这招混合量化与精度分析工具
  • 终极指南:如何快速将网易云音乐NCM文件转换为MP3/FLAC格式
  • 在智能客服场景中利用 Taotoken 聚合多模型提升回答质量
  • 保姆级教程:用Kali和VMware从零搭建DC1靶场(附全套工具包下载)