Android - 分区存储 MediaStore、SAF
一、概念
分区存储(Scoped Storage)的推出是针对 APP 访问外部存储的行为(乱建乱获取文件和文件夹)进行规范和限制,以减少混乱使得用户能更好的控制自己的文件。
公有目录被分为两大类:媒体文件(图片、音频、视频)的访问使用 MediaStore,其它文件通过系统的文件选择器访问 Storage Access Framework(简称SAF)。
二、MediaStore
跳转ContentProvider
| class MediaStore.Images | 所有图片内容的类。 |
| class MediaStore.Video | 所有视频内容的类。 |
| class MediaStore.Audio | 所有音频内容的类。 |
| class MediaStore.Files | 文件储存库中所有文件的索引,包括非媒体文件和媒体文件类。 |
| interface MediaStore.MediaColumns | 文件储存库中表的公共字段(文件的各种信息)。 |
2.1 获取 Uri
使用 Context 获取到 ContentResolver 对象,通过 Uri 即可获取各种媒体库的 ContentProvider,从而对媒体文件进行操作。
| 文件类型 | MediaStore 常量 | Uri 地址 |
| 图片 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI | content://media/external/images/media |
| 视频 | MediaStore.Video.Media.EXTERNAL_CONTENT_URI | content://media/external/video/media |
| 音频 | MediaStore.Audio.Media.EXTERNAL_CONTENT_URI | content://media/external/audio/media |
| 非媒体文件 | MediaStore.Downloads.Media.EXTERNAL_CONTENT_URI | content://media/external/downloads |
val uri1 = Uri.parse("content://media/external/images/media")
val uri2 = MediaStore.Images.Media.getContentUri("external")
val uri3 = MediaStore.Images.Media.EXTERNAL_CONTENT_URI //推荐
2.2 读取媒体文件
列名(文件信息)可以在 MediaStore.MediaColumns 取公共常量字段,也可以根据文件类型的不同在具体内部类中取值。
| 文件类型 | MediaStore 常量(常用列名) | 说明 |
| 图片 | MediaStore.Images.Media._ID | 磁盘上文件的路径 |
| MediaStore.Images.Media.DATA | 磁盘上文件的路径 | |
| MediaStore.Images.Media.DATE_ADDED | 文件添加到media provider的时间(单位秒) | |
| MediaStore.Images.Media.DATE_MODIFIED | 文件最后一次修改单元的时间 | |
| MediaStore.Images.Media.DISPLAY_NAME | 文件的显示名称 | |
| MediaStore.Images.Media.HEIGHT | 图像/视频的高度,以像素为单位 | |
| MediaStore.Images.Media.MIME_TYPE | 文件的 MIME 类型 | |
| MediaStore.Images.Media.SIZE | 文件的字节大小 | |
| MediaStore.Images.Media.TITLE | 标题 | |
| MediaStore.Images.Media.WIDTH | 图像/视频的宽度,以像素为单位 | |
| 视频 | MediaStore.Video.Media.TITLE | 名称 |
| MediaStore.Video.Media.DURATION | 总时长 | |
| MediaStore.Video.Media.DATA | 地址 | |
| MediaStore.Video.Media.SIZE | 大小 | |
| MediaStore.Video.Media.WIDTH | 视频的宽度,以像素为单位 | |
| MediaStore.Video.Media.HEIGHT | 视频的高度,以像素为单 | |
| 音频 | MediaStore.Audio.Media.TITLE | 歌名 |
| MediaStore.Audio.Media.ARTIST | 歌手 | |
| MediaStore.Audio.Media.DURATION | 总时长 | |
| MediaStore.Audio.Media.DATA | 地址 | |
| MediaStore.Audio.Media.SIZ | 大小 |
| public final Cursor query ( Uri uri, //要查询的 ContentProvider 的 Uri String[] projection, //要查询的字段(列Column),用 null 表示返回所有字段内容。 String selection, //查询条件,相当于SQL语句中的where,用 null 表示不进行筛选。 String[] selectionArgs, //如果 selection 里有?符号这里可以以实际值代替。没有的话可以为null。 String sortOrder //对结果进行排序,相当于SQL语句中的Order by,升序 asc /降序 desc,null为默认排序。 ) 返回的是一个封装了结果集的游标对象 Cursor ,资源用完需要调用 close() 关闭。 |
//获取图片类型的Uri
val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
//要获取的信息(列名)
val projection = arrayOf(MediaStore.Images.Media._ID, //获取IDMediaStore.Images.Media.MIME_TYPE, //获取MIME_TYPEMediaStore.Images.Media.DISPLAY_NAME //获取DISPLAY_NAME
)
//筛选条件(png格式的图片)
val selection = "${MediaStore.Images.Media.DISPLAY_NAME}='.png'" // ='xx.png' 改成 =?
//筛选条件的参数
val selectionArgs = arrayOf(".png") //替换筛选条件语句中?部分
//对结果的排序方式
val sortOrder = "${ContactsContract.Contacts._ID} DESC" //注意:desc前有空格
//开始查询(返回的是一个封装了结果集的游标对象,资源用完需要关闭使用use函数)
contentResolver.query(uri, projection, selection, selectionArgs, sortOrder)?.use { cursor ->//表都是通过行和列定位到具体的位置然后数据将其取出cursor.run {//获取字段在第几列(查询什么才能取出什么,否则空指针异常)val idIndex = getColumnIndexOrThrow(MediaStore.Images.Media._ID)val mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.MIME_TYPE)val displayNameIndex = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)//循环取出每一行对应字段的数据while (moveToNext()) {val id = getLong(idIndex)val mineType = getString(mimeTypeIndex)val displayName = getString(displayNameIndex)//合成图片的UriContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)//TODO...}}
}
//获取到的 Uri 可以通过 Glide 显示
Glide.with(context).load(uri).into(imageView)
//手动解析成图片的话
contentResolver.openFileDescriptor(uri, "")?.use {val bitmap = BitmapFactory.decodeFileDescriptor(it.fileDescriptor)imageView.setImageBitmap(bitmap)
}
2.3 写入媒体文件
通过 MediaStore 创建文件会保存到对应类型的默认目录中,也可以指定存放到其它同类型的公有目录或子文件夹中。如果存放到不同类型的公有目录中会报错 IllegalArgumentException(但是三种都可以存到Download中)。
| 文件类型 | mimeType 文件类型 | 默认存储目录(其它允许存储目录) |
| 图片 | image/* | Pictures(DICM) |
| 视频 | video/* | Movies(DICM) |
| 音频 | audio/* | Music(Alarms、Notifications、Podcasts、Ringtones) |
| 文件 | file/* | Download |
| public final Uri insert( Uri url, ContentValues values) 构造一个 ContentValues 对象通过 ContentResolver.insert 插入到对应的目录中,对返回的 Uri 对象进行文件流写入即可。 |
val values = ContentValues().apply {//指定 MimeTypeput(MediaStore.Images.Media.MIME_TYPE,"image/png")//指定文件名put(MediaStore.Images.Media.DISPLAY_NAME,"${System.currentTimeMillis()}.png")//指定保存的文件目录(如果不设置这个值,则会被默认保存到对应的媒体类型的文件夹下)if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {//Android 10中新增了一个RELATIVE_PATH常量,表示文件存储的相对路径,可选值有DIRECTORY_DCIM、DIRECTORY_PICTURES、DIRECTORY_MOVIES、DIRECTORY_MUSICput(MediaStore.Images.Media.RELATIVE_PATH, "${Environment.DIRECTORY_PICTURES}/DemoPicture")} else {//之前的系统版本中并没有RELATIVE_PATH,所以要使用 DATA 并拼装出一个文件存储的绝对路径才行put(MediaStore.MediaColumns.DATA, "${Environment.getExternalStorageDirectory().path}${File.separator}${Environment.DIRECTORY_DCIM}${File.separator}${System.currentTimeMillis()}.png")}
}
//插入文件数据库并获取到文件的Uri
val uri= contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
//对Uri进行文件流写入
uri?.let {//通过outputStream将本地图片bitmap或网络图片输入流写入UrlcontentResolver.openOutputStream(it)?.use { outputStream ->//TODO...//bitmap.compress(Bitmap.CompressFormat.PNG,100, outputStream)}
}
2.4 下载文件到Download目录
方式和上面的写入一样,将网络获取的输入流写入。
- 注意:MediaStore.Downloads是Android 10中新增的API,Android 9及以下的系统版本仍然使用之前的代码来进行文件下载。
val inputStream = XXX.inputStream
val bis = BufferedInputStream(inputStream)
val buffer = ByteArray(1024)//对Uri进行文件流写入
insertUri?.let {//通过outputStream将本地bitmap或网络输入流写入UrlcontentResolver.openOutputStream(it)?.use { outputStream ->BufferedOutputStream(outputStream).use { bos ->var bytes = bis.read(buffer)while (bytes >= 0) {bos.write(buffer, 0, bytes)bos.flush()bytes = bis.read(buffer)}}}
}
三、使用文件选择器 SAF
对于非媒体文件,无法像之前那样手写一个文件浏览器,而是必须使用系统提供的内置文件选择器。通过 Intent 启动系统的文件选择器,然后在 onActivityResult() 中获取到用户选中文件的 Uri 通过ContentResolver打开文件输入流来进行读取就可以了。
const val PICK_FILE = 1private fun pickFile() {val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)intent.addCategory(Intent.CATEGORY_OPENABLE)intent.type = "*/*"startActivityForResult(intent, PICK_FILE)
}override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {super.onActivityResult(requestCode, resultCode, data)when (requestCode) {PICK_FILE -> {if (resultCode == Activity.RESULT_OK && data != null) {val uri = data.dataif (uri != null) {val inputStream = contentResolver.openInputStream(uri)// 执行文件读取操作}}}}
}
四、第三方库不支持的解决办法
编写一个文件复制功能,将Uri对象所对应的文件复制到应用程序的关联目录下,然后再将关联目录下这个文件的绝对路径传递给第三方SDK,这样就可以完美进行适配了。
fun copyUriToExternalFilesDir(uri: Uri, fileName: String) {val inputStream = contentResolver.openInputStream(uri)val tempDir = getExternalFilesDir("temp")if (inputStream != null && tempDir != null) {val file = File("$tempDir/$fileName")val fos = FileOutputStream(file)val bis = BufferedInputStream(inputStream)val bos = BufferedOutputStream(fos)val byteArray = ByteArray(1024)var bytes = bis.read(byteArray)while (bytes > 0) {bos.write(byteArray, 0, bytes)bos.flush()bytes = bis.read(byteArray)}bos.close()fos.close()}
}
五、管理设备上所有的文件(公有目录 + 自定义目录)
绝大部分的应用程序都不应该申请这个权限,仅适用于文件浏览器、病毒查杀类APP,需要跳转到系统页面让用户手动授权,Play商店上架也会更严格。即便得到授权也只能访问 公有目录 + 自定义目录,依然无法访问私有目录。
5.1 权限声明
//不加 ignore 属性 AndroidStudio 会用警告提醒。
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"tools:ignore="ScopedStorage" />
5.2 跳转系统页面授权
//系统低于11或者方法返回true说明已经拥有整个SD卡管理权限
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || Environment.isExternalStorageManager()) {Toast.makeText(this, "已获得访问所有文件权限", Toast.LENGTH_SHORT).show()
} else {//否则弹窗告知申请原因并跳转到系统授权界面让用户手动授权val builder = AlertDialog.Builder(this).setMessage("本程序需要您同意允许访问所有文件权限").setPositiveButton("确定") { _, _ ->val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)startActivity(intent)}builder.show()
}
六、修改其它APP贡献的文件
修改其它APP贡献的文件是不安全的行为,默认情况下会抛异常,需要跳转到系统页面让用户手动授权,仅适用于美图秀秀类APP。在 Android 10 中每次跳转授权只能操作一张图片,如果一个程序需要修改很多张图片会很麻烦,在 Android 11 中提供了 Batch Operations 从而一次性对多个文件的操作权限进行申请。
- 由于10 之前没有分区存储,10 和 11以后是两套处理方案,专门针对 10 一个版本去写处理方案会很麻烦,由于 10 不是强制启用分区存储,可以在 AndroidManifest 中配置 requestLegacyExternalStorage 来禁用。
| createWriteRequest() 请求对多个文件的写入权限。 |
| createFavoriteRequest() 请求将多个文件加入到Favorite(收藏)的权限。 |
| createTrashRequest() 请求将多个文件移至回收站的权限。 |
| createDeleteRequest() 请求将多个文件删除的权限。 |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {/创建了一个集合用于存放所有要批量申请权限的文件Urival urisToModify = listOf(uri1, uri2, uri3, uri4)//创建一个PendingIntentval editPendingIntent = MediaStore.createWriteRequest(contentResolver, urisToModify)//进行权限申请startIntentSenderForResult(editPendingIntent.intentSender, EDIT_REQUEST_CODE, null, 0, 0, 0)
}override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {super.onActivityResult(requestCode, resultCode, data)when (requestCode) {EDIT_REQUEST_CODE -> {if (resultCode == Activity.RESULT_OK) {Toast.makeText(this, "用户已授权", Toast.LENGTH_SHORT).show()} else {Toast.makeText(this, "用户没有授权", Toast.LENGTH_SHORT).show()}}}
}相关文章:
Android - 分区存储 MediaStore、SAF
官方页面 参考文章 一、概念 分区存储(Scoped Storage)的推出是针对 APP 访问外部存储的行为(乱建乱获取文件和文件夹)进行规范和限制,以减少混乱使得用户能更好的控制自己的文件。 公有目录被分为两大类:…...
Shiro框架权限控制
首先去通过配置类的用户认证,在用户认证完成后,进行用户授权,用户通过授权之后再跳转其他的界面时,会进行一个验证,当前账号是否有权限。 前端权限控制显示的原理 在前端中,通常使用用户的角色或权限信息来…...
centOS7 安装tailscale并启用子网路由
1、在centOS7上安装Tailscale客户端 #安装命令所在官网位置:https://tailscale.com/download/linux #具体命令为: curl -fsSL https://tailscale.com/install.sh | sh #命令执行后如下图所示2、设置允许IP转发和IP伪装。 安装后,您可以启动…...
spring 项目中如何处理跨越cors问题
1.使用 CrossOrigin 注解 作用于controller 方法上 示例如下 RestController RequestMapping("/account") public class AccountController {CrossOriginGetMapping("/{id}")public Account retrieve(PathVariable Long id) {// ...}DeleteMapping(&quo…...
importlib --- import 的实现
3.1 新版功能. 源代码 Lib/importlib/__init__.py 概述 importlib 包具有三重目标。 一是在 Python 源代码中提供 import 语句的实现(并且因此而扩展 __import__() 函数)。 这提供了一个可移植到任何 Python 解释器的 import 实现。 与使用 Python 以…...
【PyTorch】现代卷积神经网络
文章目录 1. 理论介绍1.1. 深度卷积神经网络(AlexNet)1.1.1. 概述1.1.2. 模型设计 1.2. 使用块的网络(VGG)1.3. 网络中的网络(NiN)1.4. 含并行连结的网络(GoogLeNet)1.5. 批量规范化…...
用python编写九九乘法表
1 问题 我们在学习一门语言的过程中,都会练习到编写九九乘法表这个代码,下面介绍如何编写九九乘法表的流程。 2 方法 (1)打开pycharm集成开发环境,创建一个python文件,并编写第一行代码,主要构建…...
Google Gemini 模型本地可视化
Google近期发布了Gemini模型,而且开放了Gemini Pro API,Gemini Pro 可免费使用! Gemini Pro支持全球180个国家的38种语言,目前接受文本、图片作为输入并生成文本作为输出。 Gemini Pro的表现超越了其他同类模型,当前版…...
数据修复:.BlackBit勒索病毒来袭,安全应对方法解析
导言: 黑色数字罪犯的新玩具——.BlackBit勒索病毒,近来成为网络安全领域的头号威胁。这种恶意软件以其高度隐秘性和毁灭性而引起广泛关注。下面是关于.BlackBit勒索病毒的详细介绍,如不幸感染这个勒索病毒,您可添加我们的技术服…...
拓扑排序实现循环依赖判断 | 京东云技术团队
本文记录如何通过拓扑排序,实现循环依赖判断 前言 一般提到循环依赖,首先想到的就是Spring框架提供的Bean的循环依赖检测,相关文档可参考: https://blog.csdn.net/cristianoxm/article/details/113246104 本文方案脱离Spring Be…...
Java的NIO工作机制
文章目录 1. 问题引入2. NIO的工作方式3. Buffer的工作方式4. NIO数据访问方式 1. 问题引入 在网络通信中,当连接已经建立成功,服务端和客户端都会拥有一个Socket实例,每个Socket实例都有一个InputStream和OutputStream,并通过这…...
一个简单的光线追踪渲染器
前言 本文参照自raytracing in one weekend教程,地址为:https://raytracing.github.io/books/RayTracingInOneWeekend.html 什么是光线追踪? 光线追踪模拟现实中的成像原理,通过模拟一条条直线在场景内反射折射,最终…...
C++学习笔记(十二)------is_a关系(继承关系)
你好,这里是争做图书馆扫地僧的小白。 个人主页:争做图书馆扫地僧的小白_-CSDN博客 目标:希望通过学习技术,期待着改变世界。 提示:以下是本篇文章正文内容,下面案例可供参考 文章目录 前言 一、继承关系…...
DC电源模块的设计与制造技术创新
BOSHIDA DC电源模块的设计与制造技术创新 DC电源模块的设计与制造技术创新主要涉及以下几个方面: 1. 高效率设计:传统的DC电源模块存在能量转换损耗较大的问题,技术创新可通过采用高效率的电路拓扑结构、使用高性能的功率开关器件和优化控制…...
Sketch for Mac:实现你的创意绘图梦想的矢量绘图软件
随着数字时代的到来,矢量绘图软件成为了广告设计、插画创作和UI设计等领域中必不可少的工具。在众多矢量绘图软件中,Sketch for Mac(矢量绘图软件)以其强大的功能和简洁的界面脱颖而出,成为了众多设计师的首选。 Sket…...
ReactNative0.73发布,架构升级与更好的调试体验
这次更新包含了多种提升开发体验的改进,包括: 更流畅的调试体验: 通过 Hermes 引擎调试支持、控制台日志历史记录和实验性调试器,让调试过程更加高效顺畅。稳定的符号链接支持: 简化您的开发工作流程,轻松将文件或目录链接到其他…...
SVN忽略文件的两种方式
当使用版本管理工具时,提交到代码库的文档我们不希望存在把一些临时文件也推送到仓库中,这样就需要用到忽略文件。SVN的忽略相比于GIT稍显麻烦,GIT只需要在.gitignore添加忽略规则即可。而SVN有两种忽略方式,一个是全局设置&#…...
手写VUE后台管理系统10 - 封装Axios实现异常统一处理
目录 前后端交互约定安装创建Axios实例拦截器封装请求方法业务异常处理 axios 是一个易用、简洁且高效的http库 axios 中文文档:http://www.axios-js.com/zh-cn/docs/ 前后端交互约定 在本项目中,前后端交互统一使用 application/json;charsetUTF-8 的请…...
JavaScript装饰者模式
JavaScript装饰者模式 1 什么是装饰者模式2 模拟装饰者模式3 JavaScript的装饰者4 装饰函数5 AOP装饰函数6 示例:数据统计上报 1 什么是装饰者模式 在程序开发中,许多时候都我们并不希望某个类天生就非常庞大,一次性包含许多职责。那么我们就…...
C++学习笔记01
01.C概述(了解) c语言在c语言的基础上添加了面向对象编程和泛型编程的支持。 02.第一个程序helloworld(掌握) #define _CRT_SECURE_NO_WARNINGS #include<iostream> using namespace std;//标准命名空间int main() {//co…...
[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解
突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 安全措施依赖问题 GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...
ES6从入门到精通:前言
ES6简介 ES6(ECMAScript 2015)是JavaScript语言的重大更新,引入了许多新特性,包括语法糖、新数据类型、模块化支持等,显著提升了开发效率和代码可维护性。 核心知识点概览 变量声明 let 和 const 取代 var…...
脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)
一、数据处理与分析实战 (一)实时滤波与参数调整 基础滤波操作 60Hz 工频滤波:勾选界面右侧 “60Hz” 复选框,可有效抑制电网干扰(适用于北美地区,欧洲用户可调整为 50Hz)。 平滑处理&…...
PHP和Node.js哪个更爽?
先说结论,rust完胜。 php:laravel,swoole,webman,最开始在苏宁的时候写了几年php,当时觉得php真的是世界上最好的语言,因为当初活在舒适圈里,不愿意跳出来,就好比当初活在…...
day52 ResNet18 CBAM
在深度学习的旅程中,我们不断探索如何提升模型的性能。今天,我将分享我在 ResNet18 模型中插入 CBAM(Convolutional Block Attention Module)模块,并采用分阶段微调策略的实践过程。通过这个过程,我不仅提升…...
Axios请求超时重发机制
Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式: 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...
python报错No module named ‘tensorflow.keras‘
是由于不同版本的tensorflow下的keras所在的路径不同,结合所安装的tensorflow的目录结构修改from语句即可。 原语句: from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense 修改后: from tensorflow.python.keras.lay…...
网站指纹识别
网站指纹识别 网站的最基本组成:服务器(操作系统)、中间件(web容器)、脚本语言、数据厍 为什么要了解这些?举个例子:发现了一个文件读取漏洞,我们需要读/etc/passwd,如…...
C++课设:简易日历程序(支持传统节假日 + 二十四节气 + 个人纪念日管理)
名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 专栏介绍:《编程项目实战》 目录 一、为什么要开发一个日历程序?1. 深入理解时间算法2. 练习面向对象设计3. 学习数据结构应用二、核心算法深度解析…...
4. TypeScript 类型推断与类型组合
一、类型推断 (一) 什么是类型推断 TypeScript 的类型推断会根据变量、函数返回值、对象和数组的赋值和使用方式,自动确定它们的类型。 这一特性减少了显式类型注解的需要,在保持类型安全的同时简化了代码。通过分析上下文和初始值,TypeSc…...
