Android 10(Q)/11(R) 分区存储适配
cac55 2025-09-02 13:50 12 浏览 0 评论
作者:连续三届村草
Android 10(Q)/11(R) 分区存储适配
大部分应用都会请求 ( READ_EXTERNAL_STORAGE ) ( WRITE_EXTERNAL_STORAGE ) 存储权限,来做一些诸如在 SD 卡中存储文件或者读取多媒体文件等常规操作。这些应用可能会在磁盘中存储大量文件,即使应用被卸载了还会依然存在。另外,这些应用还可能会读取其他应用的一些敏感文件数据。
为此,Google 终于下定决心在 Android 10 中引入了分区存储,对权限进行场景的细分,按需索取,并在 Android 11 中进行了进一步的调整。
Android 存储分区情况
Android 中存储可以分为两大类:私有存储和共享存储
- 私有存储 (Private Storage) : 每个应用在都拥有自己的私有目录,其它应用看不到,彼此也无法访问到该目录:内部存储私有目录 (/data/data/packageName) ;外部存储私有目录 (/sdcard/Android/data/packageName),
- 共享存储 (Shared Storage) : 存储其他应用可访问文件, 包含媒体文件、文档文件以及其他文件,对应设备DCIM、Pictures、Alarms、Music、Notifications、Podcasts、Ringtones、Movies、Download等目录。
Android 10(Q) :
Android 10 中主要对共享目录进行了权限详细的划分,不再能通过绝对路径访问。
受影响的接口:
访问不同分区的方式:
- 私有目录:和以前的版本一致,可通过 File() API 访问,无需申请权限。
- 共享目录:需要通过MediaStore和Storage Access Framework API 访问,视具体情况申请权限,下面详细介绍。
其中,对共享目录的权限进行了细分:
- 无需申请权限的操作:
通过 MediaStore API对媒体集、文件集进行媒体/文件的添加、对 自身APP 创建的 媒体/文件 进行查询、修改、删除的操作。 - 需要申请READ_EXTERNAL_STORAGE 权限:
通过 MediaStore API对所有的媒体集进行查询、修改、删除的操作。 - 调用 Storage Access Framework API :
会启动系统的文件选择器向用户申请操作指定的文件
新的访问方式:
Android 11 (R):
Android 11 (R) 在 Android 10 (Q) 中分区存储的基础上进行了调整
1. 新增执行批量操作
为实现各种设备之间的一致性并增加用户便利性,Android 11 向 MediaStore API 中添加了多种方法。对于希望简化特定媒体文件更改流程(例如在原位置编辑照片)的应用而言,这些方法尤为有用。
MediaStore API 新增的方法
系统在调用以上任何一个方法后,会构建一个 PendingIntent 对象。应用调用此 intent 后,用户会看到一个对话框,请求用户同意应用更新或删除指定的媒体文件。
2. 使用直接文件路径和原生库访问文件
为了帮助您的应用更顺畅地使用第三方媒体库,Android 11 允许您使用除 MediaStore API 之外的 API 访问共享存储空间中的媒体文件。不过,您也可以转而选择使用以下任一 API 直接访问媒体文件:
File API。
原生库,例如 fopen()。
简单来说就是,可以通过 File() 等API 访问有权限访问的媒体集了。
性能:
通过 File () 等直接通过路径访问的 API 实际上也会映射为MediaStore API 。
按文件路径顺序读取的时候性能相当;随机读取和写入的时候则会更慢,所以还是推荐直接使用 MediaStoreAPI。
3. 新增权限
MANAGE_EXTERNAL_STORAGE : 类似以前的 READ_EXTERNAL_STORAGE + WRITE_EXTERNAL_STORAGE ,除了应用专有目录都可以访问。
应用可通过执行以下操作向用户请求名为所有文件访问权限的特殊应用访问权限:
- 在清单中声明 MANAGE_EXTERNAL_STORAGE 权限。
- 使用 ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION intent 操作将用户引导至一个系统设置页面,在该页面上,用户可以为您的应用启用以下选项:授予所有文件的管理权限。
- 在 Google Play 上架的话,需要提交使用此权限的说明,只有指定的几种类型的 APP 才能使用。
Sample
- 使用 MediaStore 增删改查媒体集
- 使用 Storage Access Framework 访问文件集
1. 媒体集
1) 查询媒体集(需要 READ_EXTERNAL_STORAGE 权限)
实际上 MediaStore 是以前就有的 API ,不同的是过去主要通过
MediaStore.Video.Media._DATA这个 colum 请求原始数据,可以得到绝对Uri ,现在需要请求
MediaStore.Video.Media._ID来得到相对Uri再进行处理。
// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your
// app didn't create.
// Container for information about each video.
data class Video(
val uri: Uri,
val name: String,
val duration: Int,
val size: Int
)
val videoList = mutableListOf<Video>()
val projection = arrayOf(
MediaStore.Video.Media._ID,
MediaStore.Video.Media.DISPLAY_NAME,
MediaStore.Video.Media.DURATION,
MediaStore.Video.Media.SIZE
)
// Show only videos that are at least 5 minutes in duration.
val selection = "${MediaStore.Video.Media.DURATION} >= ?"
val selectionArgs = arrayOf(
TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES).toString()
)
// Display videos in alphabetical order based on their display name.
val sortOrder = "${MediaStore.Video.Media.DISPLAY_NAME} ASC"
val query = ContentResolver.query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder
)
query?.use { cursor ->
// Cache column indices.
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
val nameColumn =
cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
val durationColumn =
cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION)
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)
while (cursor.moveToNext()) {
// Get values of columns for a given video.
val id = cursor.getLong(idColumn)
val name = cursor.getString(nameColumn)
val duration = cursor.getInt(durationColumn)
val size = cursor.getInt(sizeColumn)
val contentUri: Uri = ContentUris.withAppendedId(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
id
)
// Stores column values and the contentUri in a local object
// that represents the media file.
videoList += Video(contentUri, name, duration, size)
}
}
2)插入媒体集(无需权限)
// Add a media item that other apps shouldn't see until the item is
// fully written to the media store.
val resolver = applicationContext.contentResolver
// Find all audio files on the primary external storage device.
// On API <= 28, use VOLUME_EXTERNAL instead.
val audioCollection = MediaStore.Audio.Media
.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val songDetails = ContentValues().apply {
put(MediaStore.Audio.Media.DISPLAY_NAME, "My Workout Playlist.mp3")
put(MediaStore.Audio.Media.IS_PENDING, 1)
}
val songContentUri = resolver.insert(audioCollection, songDetails)
resolver.openFileDescriptor(songContentUri, "w", null).use { pfd ->
// Write data into the pending audio file.
}
// Now that we're finished, release the "pending" status, and allow other apps
// to play the audio track.
songDetails.clear()
songDetails.put(MediaStore.Audio.Media.IS_PENDING, 0)
resolver.update(songContentUri, songDetails, null, null)
3)更新自己创建的媒体集(无需权限)
删除类似
// Updates an existing media item.
val mediaId = // MediaStore.Audio.Media._ID of item to update.
val resolver = applicationContext.contentResolver
// When performing a single item update, prefer using the ID
val selection = "${MediaStore.Audio.Media._ID} = ?"
// By using selection + args we protect against improper escaping of // values.
val selectionArgs = arrayOf(mediaId.toString())
// Update an existing song.
val updatedSongDetails = ContentValues().apply {
put(MediaStore.Audio.Media.DISPLAY_NAME, "My Favorite Song.mp3")
}
// Use the individual song's URI to represent the collection that's
// updated.
val numSongsUpdated = resolver.update(
myFavoriteSongUri,
updatedSongDetails,
selection,
selectionArgs)
4)更新/删除其它媒体创建的媒体集
若已经开启分区存储则会抛出
RecoverableSecurityException,捕获并通过SAF请求权限
// Apply a grayscale filter to the image at the given content URI.
try {
contentResolver.openFileDescriptor(image-content-uri, "w")?.use {
setGrayscaleFilter(it)
}
} catch (securityException: SecurityException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val recoverableSecurityException = securityException as?
RecoverableSecurityException ?:
throw RuntimeException(securityException.message, securityException)
val intentSender =
recoverableSecurityException.userAction.actionIntent.intentSender
intentSender?.let {
startIntentSenderForResult(intentSender, image-request-code,
null, 0, 0, 0, null)
}
} else {
throw RuntimeException(securityException.message, securityException)
}
}
2. 文件集 (通过 SAF)
1)创建文档
注:创建操作若重名的话不会覆盖原文档,会添加 (1) 最为后缀,如 document.pdf -> document(1).pdf
// Request code for creating a PDF document.
const val CREATE_FILE = 1
private fun createFile(pickerInitialUri: Uri) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/pdf"
putExtra(Intent.EXTRA_TITLE, "invoice.pdf")
// Optionally, specify a URI for the directory that should be opened in
// the system file picker before your app creates the document.
putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}
startActivityForResult(intent, CREATE_FILE)
}
2)打开文档
建议使用 type 设置 MIME 类型
// Request code for selecting a PDF document.
const val PICK_PDF_FILE = 2
fun openFile(pickerInitialUri: uri) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/pdf"
// Optionally, specify a URI for the file that should appear in the
// system file picker when it loads.
putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}
startActivityForResult(intent, PICK_PDF_FILE)
}
3)授予对目录内容的访问权限
用户选择目录后,可访问该目录下的所有内容
Android 11 中无法访问 Downloads
fun openDirectory(pickerInitialUri: Uri) {
// Choose a directory using the system's file picker.
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
// Provide read access to files and sub-directories in the user-selected
// directory.
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
// Optionally, specify a URI for the directory that should be opened in
// the system file picker when it loads.
putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}
startActivityForResult(intent, your-request-code)
}
4)永久获取目录访问权限
上面提到的授权是临时性的,重启后则会失效。可以通过下面的方法获取相应目录永久性的权限
val contentResolver = applicationContext.contentResolver
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
// Check for the freshest data.
contentResolver.takePersistableUriPermission(uri, takeFlags)
5)SAF API 响应
SAF API 调用后都是通过 onActivityResult来相应动作
override fun onActivityResult(
requestCode: Int, resultCode: Int, resultData: Intent?) {
if (requestCode == your-request-code
&& resultCode == Activity.RESULT_OK) {
// The result data contains a URI for the document or directory that
// the user selected.
resultData?.data?.also { uri ->
// Perform operations on the document using its URI.
}
}
}
6) 其它操作
除了上面的操作之外,对文档其它的复制、移动等操作都是通过设置不同的 FLAG 来实现,见 Document.COLUMN_FLAGS
3. 批量操作媒体集
构建一个媒体集的写入操作 createWriteRequest()
val urisToModify = /* A collection of content URIs to modify. */
val editPendingIntent = MediaStore.createWriteRequest(contentResolver,
urisToModify)
// Launch a system prompt requesting user permission for the operation.
startIntentSenderForResult(editPendingIntent.intentSender, EDIT_REQUEST_CODE,
null, 0, 0, 0)
//相应
override fun onActivityResult(requestCode: Int, resultCode: Int,
data: Intent?) {
...
when (requestCode) {
EDIT_REQUEST_CODE ->
if (resultCode == Activity.RESULT_OK) {
/* Edit request granted; proceed. */
} else {
/* Edit request not granted; explain to the user. */
}
}
}
createFavoriteRequest() createTrashRequest() createDeleteRequest() 同理
适配和兼容
在 targetSDK = 29 APP 中,在 AndroidManifes 设置
requestLegacyExternalStorage="true" 启用兼容模式,以传统分区模式运行。
<manifest ... >
<!-- This attribute is "false" by default on apps targeting
Android 10 or higher. -->
<application android:requestLegacyExternalStorage="true" ... >
...
</application>
</manifest>
注意:如果某个应用在安装时启用了传统外部存储,则该应用会保持此模式,直到卸载为止。无论设备后续是否升级为搭载 Android 10 或更高版本,或者应用后续是否更新为以 Android 10 或更高版本为目标平台,此兼容性行为均适用。
意思就是在新系统新安装的应用才会启用,覆盖安装会保持传统分区模式,例如:
- 系统通过 OTA 升级到 Android 10/11
- 应用通过更新升级到 targetSdkVersion >= 29
补充
Q:之前讨论过一些问题,APP 无需权限可以访问自己创建的媒体,那么系统如何进行判断?
A:创建媒体时系统会给媒体打上 packageName tag,应用被卸载则会清除 tag ,所以不会存在使用同样 packageName 进行欺骗的情况。
Q:我可以在媒体集文件夹下创建文档,就可以避开权限的问题了?
A:官方文档上写了只能创建相应类型的媒体/文件,具体如何限制的,没有说明。
总结
从 Android 10提出分区存储之后到现在已经一年多了,所以Google 从强制推行的态度到现在 targetSDK >=30 才强制启用分区存储来看,Google 还是渐渐地选择给开发者留更多的时间。缺点当然是不强制启用的话,国内 APP 适配进度估计得延后了。不过好消息是在查资料的时候,看到了国内大厂的相关适配文章,至少说明大厂在跟进了。
去年(19年)的文档描述是无论 targetSDK 多少,明年(20年)高版本强制启用。
今年(20)文档描述是 targetSDK >=30 才强制启用
关于适配的难度:
对绝对路径相关接口依赖比较深的 APP 适配还是改动挺多的;其次权限的划分很细,什么时候需要什么权限以及调用哪个接口,理解起来需要一定时间;MediaStore API SAF API 这类接口以前就设计好了,我也觉得也不算特别友好;最后测试也需要重新进行。
所以虽然明年才会强制执行分区存储,但还是建议尽早理解和 review 项目中需要适配的代码。
文末附上大厂学长给我的资料,内容包含:Android学习PDF+架构视频+面试文档+源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 这几块的内容
这些都是我现在闲暇还会反复翻阅的精品资料。里面对近几年的大厂面试高频知识点都有详细的讲解。相信可以有效的帮助大家掌握知识、理解原理。
分享给大家,非常适合近期有面试和想在技术道路上继续精进的朋友。也是希望可以帮助到大家提升进阶
如果你有需要的话,可以私信我【提升】我发给你
喜欢本文的话,不妨顺手给我点个赞、评论区留言或者转发支持一下呗~
相关推荐
- 这些端口关闭后,系统会更安全!系统高危端口及其关闭方法?
-
在这高速发展的网络信息时代,信息安全显得非常重要,病毒、木马、非法侵入等安全事件经常发生。在我们使用电脑过程中,为了确保系统安全,以下高危端口必须关闭,防患于未然。一.Windows系统的445端口...
- 什么是安全组_什么是安全组件
-
安全组是一种虚拟防火墙,具备状态检测和数据包过滤功能,用于在云计算环境中设置网络访问控制,保护云服务器(ECS实例)、负载均衡、云数据库等资源。核心特性:虚拟防火墙:安全组控制云资源的出入站流量,决定...
- 针对单个网站的渗透思路(精)_网站渗透步骤
-
欢迎搜索公众号:白帽子左一每天分享更多黑客技能,工具及体系化视频教程(免费领首先,当我们拿到一个网站的域名或者IP的时候。最先要做的是信息收集。下面着重介绍一下信息收集模块一、信息收集——端口扫描与分...
- 风险突出的高危端口汇总 一网打尽 !
-
高危端口一直是攻击者关注的焦点,了解这些端口的风险、攻击方式及防护策略至关重要。一、文件传输类端口1.TCP20/21:FTP服务端口FTP(文件传输协议)用于文件的上传和下载。其明文传输特性使得...
- 指定IP地址进行远程访问服务器设置方法(windows系统)
-
我们有很多服务器经常受到外界网络的干扰,入侵者们通过扫描3389端口爆破密码非法进入我们的服务器,这时,我们可以配置服务器IP安全策略来限制一些IP访问,大大提高了服务器的安全。实验环境:服务端:...
- 服务器被黑,如何查找入侵、攻击痕迹呢?
-
本文出自头条号老王谈运维,转载请说明出处。引言:随着网络的越来越普及,使用的越来越频繁,木马病毒也随之侵入进来并且肆无忌惮。如何将病毒拒之门外,已成为我们普通大众必须具备的一项技能。这样,你才能使木马...
- win10 telnet命令怎么查看端口是否打开
-
可能大家也会遇到这个问题,win10telnet命令查看端口是否打开的步骤是什么?具体方法如下:1、键盘输入快捷键WIN+R,打开运行窗口。2、输入cmd,点击确定按钮。3、弹出cmd命令行窗...
- Crysis勒索病毒针对政企服务器攻击升级 腾讯安全展开全面防御
-
近日,腾讯安全御见威胁情报中心监测发现,Crysis勒索病毒在国内传播升级,感染数量呈上升趋势,该病毒主要通过RDP弱口令爆破传播入侵政企机构,加密重要数据,由于该病毒的加密破坏暂无法解密,被攻击后将...
- Windows端口详解,这几个端口不能开!
-
一、血泪警告这7个端口开着电脑秒变公共厕所445端口:勒索病毒专用通道永恒之蓝病毒最爱突破口,文件共享功能成致命漏洞。企业内网还敢用用,个人电脑开着就是作死135-139端口:网络邻居成内鬼,Wind...
- 网络通讯笔记_网络通讯笔记怎么写
-
网络通讯一、NIC(网卡)二、CMD命令提示符三、服务、协议与端口常见的计算机服务常见的计算机端口与协议四、DOS命令1、基本DOS命令五、地址一、NIC(网卡)网络接口控制器又叫网络适配器也就是...
- 服务器远程端口是什么意思?什么是服务器远程端口?
-
什么是服务器远程端口?如图:IP冒号后面的数字这就是服务器的一个远程端口服务器远程端口是什么意思?服务器远程端口是服务器通信服务中的一个服务端窗口号码,取值范围是1-65535.一个服务器里面包含服务...
- 服务器节点到底是啥?看完这篇全明白,旧电脑也能派上大用场
-
不少朋友看了我用旧电脑改服务器节点的文章,后台都在问:“服务器节点到底能干啥?”其实这东西没那么神秘,今天用大白话讲讲,看完你就知道家里的旧设备藏着多大潜力。服务器节点:网络世界的“小工位”简单说...
- 广东通管局预警:勒索病毒威胁“关键信息基础设施”,应高度警惕
-
来源:澎湃新闻据广东省通信管理局网站消息,广东省通信管理局5月12日发布了《关于勒索病毒对关键信息基础设施威胁的预警通报》。通报称,5月7日,美国最大燃油运输管道商“科洛尼尔”(ColonialP...
- 80端口和443端口是什么?服务器端口干什么用的?
-
80和443端口是最常见的2个端口,都是提供网络WEB浏览服务所需要的端口,一台服务器通过不同的端口,提供不同的服务。80端口服务:HTTP(HyperTextTransportProtocol)...
- 从单日网络安全风险看当前网络安全状况
-
一、核心结论(从单日数据看全局风险)通过对2025年8月18日这一天的非法访问数据深度分析,可以清晰看到:网络环境中的安全威胁呈现高频次、多目标、全球化三大特征。单日4557次非法访问尝试,覆盖22、...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- 如何绘制折线图 (52)
- javaabstract (48)
- 新浪微博头像 (53)
- grub4dos (66)
- s扫描器 (51)
- httpfile dll (48)
- ps实例教程 (55)
- taskmgr (51)
- s spline (61)
- vnc远程控制 (47)
- 数据丢失 (47)
- wbem (57)
- flac文件 (72)
- 网页制作基础教程 (53)
- 镜像文件刻录 (61)
- ug5 0软件免费下载 (78)
- debian下载 (53)
- ubuntu10 04 (60)
- web qq登录 (59)
- 笔记本变成无线路由 (52)
- flash player 11 4 (50)
- 右键菜单清理 (78)
- cuteftp 注册码 (57)
- ospf协议 (53)
- ms17 010 下载 (60)