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

Android 10分区存储适配实战:从MediaStore到SAF的完整迁移指南

Android 10分区存储迁移实战:MediaStore与SAF的深度应用指南

当小米工程师在Redmi K30上测试新版相册应用时,发现一个诡异现象:通过传统File API保存的用户照片,在系统升级到Android 10后突然"消失"了。这背后正是Android 10引入的分区存储机制在发挥作用——一个让无数开发者又爱又恨的存储革命。本文将带你穿透概念迷雾,直击适配核心,用真实项目经验告诉你如何优雅跨越这道技术鸿沟。

1. 分区存储的本质与挑战

在Android 10之前,开发者可以像在自家后院一样随意访问整个外部存储空间。这种"自由"带来的代价是:用户相册里突然出现各种应用的缓存目录,卸载应用后残留大量垃圾文件,隐私数据被随意读取。Google的解决方案就是分区存储(Scoped Storage),它像给每个应用划分了专属"领地"。

关键变化点对比表

特性Android 9及之前Android 10分区存储模式
访问范围整个外部存储应用私有目录+受限公共媒体文件
权限要求需要READ/WRITE_EXTERNAL_STORAGE自建文件无需权限
文件持久性卸载后残留私有目录随应用卸载清除
访问方式直接文件路径主要依赖MediaStore/SAF

我在适配某相机应用时踩过的典型坑:

// 旧代码 - 在Pictures目录创建子目录 File galleryDir = new File(Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_PICTURES), "MyCamera"); if (!galleryDir.exists()) { galleryDir.mkdirs(); // Android 10上会失败! }

提示:判断是否启用分区存储的标准方法:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !Environment.isExternalStorageLegacy()) { // 分区存储模式逻辑 }

2. MediaStore API的完全指南

MediaStore就像个智能档案管理员,它知道所有媒体文件的元信息。但要用好它,需要掌握这些实战技巧:

2.1 文件创建的正确姿势

以保存拍摄的照片为例,注意这些细节:

ContentValues values = new ContentValues(); values.put(MediaStore.Images.Media.DISPLAY_NAME, "IMG_" + System.currentTimeMillis()); values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg"); values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/MyCamera"); // 关键:相对路径 Uri uri = getContentResolver().insert( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); try (OutputStream out = getContentResolver().openOutputStream(uri)) { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out); }

常见踩坑点

  • 硬编码绝对路径(如/sdcard/Pictures)将导致失败
  • 未指定RELATIVE_PATH时文件会保存到媒体类型根目录
  • 非媒体文件(如PDF)不能通过MediaStore保存

2.2 复杂查询的优化策略

当需要查询特定条件的媒体文件时:

String[] projection = { MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.DATE_TAKEN }; String selection = MediaStore.Images.Media.DATE_TAKEN + " > ?"; String[] args = { String.valueOf(startTimestamp) }; String sortOrder = MediaStore.Images.Media.DATE_TAKEN + " DESC"; try (Cursor cursor = getContentResolver().query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, selection, args, sortOrder)) { while (cursor.moveToNext()) { long id = cursor.getLong(0); Uri uri = ContentUris.withAppendedId( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id); // 处理文件URI } }

注意:查询结果只包含媒体文件元信息,实际文件访问仍需通过ContentResolver打开流

3. Storage Access Framework的进阶技巧

当需要处理非媒体文件或获取持久访问权限时,SAF是唯一选择。但它的使用远比看起来复杂:

3.1 目录授权的正确流程

获取文档树授权的完整示例:

// 启动目录选择器 Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); startActivityForResult(intent, REQUEST_CODE_DIRECTORY); // 处理返回结果 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_CODE_DIRECTORY && resultCode == RESULT_OK) { Uri treeUri = data.getData(); // 持久化保存权限 getContentResolver().takePersistableUriPermission( treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); // 使用DocumentFile操作文件 DocumentFile root = DocumentFile.fromTreeUri(this, treeUri); DocumentFile newFile = root.createFile("text/plain", "note.txt"); } }

关键注意事项

  • 每次应用重启后都需要检查权限是否仍然有效
  • 通过DocumentFile类进行文件操作而非传统File API
  • 用户随时可能撤销授权,代码需做好异常处理

3.2 特定文件类型处理技巧

处理PDF文件的典型场景:

Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("application/pdf"); startActivityForResult(intent, REQUEST_CODE_PDF); // 在onActivityResult中获取文件URI后 try (ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r")) { FileDescriptor fd = pfd.getFileDescriptor(); PDFRenderer renderer = new PDFRenderer(ParcelFileDescriptor.dup(fd)); // 渲染PDF页面 }

4. 混合存储架构的设计实践

在适配某云盘应用时,我们创新性地采用了分层存储策略:

4.1 存储策略决策树

是否媒体文件? ├── 是 → 使用MediaStore API │ ├── 需要长期共享? → 存入公共媒体目录 │ └── 仅应用使用? → 存入应用私有目录 └── 否 → 使用SAF或应用私有存储 ├── 用户需要选择? → SAF文档选择器 └── 应用私有文件 → Context.getExternalFilesDir()

4.2 性能优化方案

缓存管理策略

// 在私有缓存目录保存缩略图 File cacheDir = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), ".thumbnail_cache"); if (!cacheDir.exists()) { cacheDir.mkdirs(); } // 定期清理(如使用WorkManager) File[] files = cacheDir.listFiles(); long cutoff = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7); for (File file : files) { if (file.lastModified() < cutoff) { file.delete(); } }

批量操作优化

ArrayList<ContentProviderOperation> ops = new ArrayList<>(); for (ImageInfo image : imagesToAdd) { ContentValues values = new ContentValues(); values.put(MediaStore.Images.Media.DISPLAY_NAME, image.name); values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/MyApp"); ops.add(ContentProviderOperation.newInsert( MediaStore.Images.Media.EXTERNAL_CONTENT_URI) .withValues(values) .build()); } try { getContentResolver().applyBatch(MediaStore.AUTHORITY, ops); } catch (RemoteException | OperationApplicationException e) { // 处理异常 }

5. 疑难问题排查手册

在真实项目中遇到的典型问题及解决方案:

5.1 权限丢失问题

现象:用户反馈重启手机后无法访问之前授权的文件。

解决方案

// 启动时检查并恢复权限 List<UriPermission> perms = getContentResolver().getPersistedUriPermissions(); for (UriPermission perm : perms) { if (perm.isReadPermission()) { // 重新建立文档树访问 DocumentFile root = DocumentFile.fromTreeUri(this, perm.getUri()); // 验证是否仍然可访问 if (root.canRead()) { // 恢复业务逻辑 } } }

5.2 文件路径兼容方案

对于必须使用文件路径的第三方库(如某些图像处理SDK),可采用临时文件桥接:

// 从MediaStore URI获取临时文件 Uri mediaUri = ...; // 从MediaStore获取的URI try (InputStream in = getContentResolver().openInputStream(mediaUri); OutputStream out = new FileOutputStream(tempFile)) { byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); } } // 将tempFile路径传给第三方库 processImage(tempFile.getAbsolutePath());

在适配某金融应用时,我们发现其使用的PDF签名库必须接收文件路径。通过这种桥接方式,既满足了库的要求,又遵循了分区存储规范。

http://www.jsqmd.com/news/519262/

相关文章:

  • SZMS 2025 自招 T2
  • 基于Matlab的不确定性预测仿真之旅
  • 双向Buck-Boost变换器:电压外环与电流内环控制的平均电流管理技术,实现模式切换无过压过...
  • prometheus histogram
  • 《医学数据分析与挖掘》第三周课程笔记
  • 55 千瓦感应电机设计与仿真那些事儿
  • 2026年 上海招商办公楼实力推荐榜:聚焦核心商圈,解析优质办公空间选址策略与增值服务 - 品牌企业推荐师(官方)
  • 永磁同步电机PMSM参数辨识与SVPWM矢量控制仿真探索
  • 深入解析LeetCode 136:巧用异或运算,高效找出数组中唯一的“单身数字”
  • Whisper-Tiny 模型:轻量级语音识别的实时应用与优化
  • GDS Decompiler:Godot引擎逆向工程工具深度解析
  • AI编程时代,35岁以上程序员将何去何从?
  • Java基础 - 对象与类
  • 别再死记硬背了!一张图帮你理清FS、FT、DTFT、DFS、DFT的关系与区别
  • 北京上门收画哪家专业?丰宝斋资深团队,精准鉴定名家字画 - 品牌排行榜单
  • 汇川H3U 10 轴项目实战:电池自动上料机的奇妙之旅
  • 交换机堆叠与集群完全指南:从入门到实战,一篇搞定所有难题
  • Keil5编译报错解析:从Program Size参数到Target not created的解决之道
  • 探索光储直流微电网协调控制之直流电压分层优化控制
  • 从零到全网通:一个实验彻底搞懂VLAN、三层交换与静态路由(华为eNSP实战)
  • 《QGIS快速入门与应用基础》231:图例项目管理(添加/删除/排序)
  • 7车位立体车库组态王6.53仿真程序:急停功能解析
  • 人机协作的核心困局,终于被这篇顶会论文破解了
  • 少走弯路:9个AI论文工具全场景通用测评,开题报告+毕业论文高效写作推荐!
  • 用Bash脚本构建AI编码助手:learn-claude-code项目技术解析
  • 避坑指南:PostgreSQL MCP高可用集群配置中的5个常见错误与性能调优实战
  • STM32+LoRa模块实战:从环境搭建到数据传输完整指南(附避坑清单)
  • 拖延症福音 一键生成论文工具 千笔AI VS 灵感ai 全领域适配首选
  • 人-机交互是新文科与新理科融合的最佳窗口
  • 用STM32F103C8T6最小系统板驱动HC-SR04超声波模块,手把手教你做个简易测距仪(附完整代码)