百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

Android 10(Q)/11(R) 分区存储适配

liuian 2025-03-30 18:25 23 浏览

作者:连续三届村草


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 中主要对共享目录进行了权限详细的划分,不再能通过绝对路径访问。

受影响的接口:

访问不同分区的方式:

  1. 私有目录:和以前的版本一致,可通过 File() API 访问,无需申请权限。
  2. 共享目录:需要通过MediaStore和Storage Access Framework API 访问,视具体情况申请权限,下面详细介绍。

其中,对共享目录的权限进行了细分:

  1. 无需申请权限的操作:
    通过 MediaStore API对媒体集、文件集进行媒体/文件的添加、对 自身APP 创建的 媒体/文件 进行查询、修改、删除的操作。
  2. 需要申请READ_EXTERNAL_STORAGE 权限:
    通过 MediaStore API对所有的媒体集进行查询、修改、删除的操作。
  3. 调用 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 ,除了应用专有目录都可以访问。

应用可通过执行以下操作向用户请求名为所有文件访问权限的特殊应用访问权限:

  1. 在清单中声明 MANAGE_EXTERNAL_STORAGE 权限。
  2. 使用 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

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' .getcontenturimediastore.volume_external_primary val songdetails='ContentValues().apply' putmediastore.audio.media.display_name my workout playlist.mp3 putmediastore.audio.media.is_pending 1 val songcontenturi='resolver.insert(audioCollection,' songdetails resolver.openfiledescriptorsongcontenturi 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" 启用兼容模式,以传统分区模式运行。

   
      
      
        ...
      
    

注意:如果某个应用在安装时启用了传统外部存储,则该应用会保持此模式,直到卸载为止。无论设备后续是否升级为搭载 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开发面试专题资料,高级进阶架构资料 这几块的内容

这些都是我现在闲暇还会反复翻阅的精品资料。里面对近几年的大厂面试高频知识点都有详细的讲解。相信可以有效的帮助大家掌握知识、理解原理。

分享给大家,非常适合近期有面试和想在技术道路上继续精进的朋友。也是希望可以帮助到大家提升进阶

如果你有需要的话,可以私信我【提升】我发给你


喜欢本文的话,不妨顺手给我点个赞、评论区留言或者转发支持一下呗~

相关推荐

教你把多个视频合并成一个视频的方法

一.情况介绍当你有一个m3u8文件和一个目录,目录中有连续的视频片段,这些片段可以连成一段完整的视频。m3u8文件打开后像这样:m3u8文件,可以理解为播放列表,里面是播放视频片段的顺序。视频片段像这...

零代码编程:用kimichat合并一个文件夹下的多个文件

一个文件夹里面有很多个srt字幕文件,如何借助kimichat来自动批量合并呢?在kimichat对话框中输入提示词:你是一个Python编程专家,完成如下的编程任务:这个文件夹:D:\downloa...

Java APT_java APT 生成代码

JavaAPT(AnnotationProcessingTool)是一种在Java编译阶段处理注解的工具。APT会在编译阶段扫描源代码中的注解,并根据这些注解生成代码、资源文件或其他输出,...

Unit Runtime:一键运行 AI 生成的代码,或许将成为你的复制 + 粘贴神器

在我们构建了UnitMesh架构之后,以及对应的demo之后,便着手于实现UnitMesh架构。于是,我们就继续开始UnitRuntime,以用于直接运行AI生成的代码。PS:...

挣脱臃肿的枷锁:为什么说Vert.x是Java开发者手中的一柄利剑?

如果你是一名Java开发者,那么你的职业生涯几乎无法避开Spring。它如同一位德高望重的老国王,统治着企业级应用开发的大片疆土。SpringBoot的约定大于配置、SpringCloud的微服务...

五年后,谷歌还在全力以赴发展 Kotlin

作者|FredericLardinois译者|Sambodhi策划|Tina自2017年谷歌I/O全球开发者大会上,谷歌首次宣布将Kotlin(JetBrains开发的Ja...

kotlin和java开发哪个好,优缺点对比

Kotlin和Java都是常见的编程语言,它们有各自的优缺点。Kotlin的优点:简洁:Kotlin程序相对于Java程序更简洁,可以减少代码量。安全:Kotlin在类型系统和空值安全...

移动端架构模式全景解析:从MVC到MVVM,如何选择最佳设计方案?

掌握不同架构模式的精髓,是构建可维护、可测试且高效移动应用的关键。在移动应用开发中,选择合适的软件架构模式对项目的可维护性、可测试性和团队协作效率至关重要。随着应用复杂度的增加,一个良好的架构能够帮助...

颜值非常高的XShell替代工具Termora,不一样的使用体验!

Termora是一款面向开发者和运维人员的跨平台SSH终端与文件管理工具,支持Windows、macOS及Linux系统,通过一体化界面简化远程服务器管理流程。其核心定位是解决多平台环境下远程连接、文...

预处理的底层原理和预处理编译运行异常的解决方案

若文章对您有帮助,欢迎关注程序员小迷。助您在编程路上越走越好![Mac-10.7.1LionIntel-based]Q:预处理到底干了什么事情?A:预处理,顾名思义,预先做的处理。源代码中...

为“架构”再建个模:如何用代码描述软件架构?

在架构治理平台ArchGuard中,为了实现对架构的治理,我们需要代码+模型描述所要处理的内容和数据。所以,在ArchGuard中,我们有了代码的模型、依赖的模型、变更的模型等,剩下的两个...

深度解析:Google Gemma 3n —— 移动优先的轻量多模态大模型

2025年6月,Google正式发布了Gemma3n,这是一款能够在2GB内存环境下运行的轻量级多模态大模型。它延续了Gemma家族的开源基因,同时在架构设计上大幅优化,目标是让...

比分网开发技术栈与功能详解_比分网有哪些

一、核心功能模块一个基本的比分网通常包含以下模块:首页/总览实时比分看板:滚动展示所有正在进行的比赛,包含比分、比赛时间、红黄牌等关键信息。热门赛事/焦点战:突出显示重要的、关注度高的比赛。赛事导航...

设计模式之-生成器_一键生成设计

一、【概念定义】——“分步构建复杂对象,隐藏创建细节”生成器模式(BuilderPattern):一种“分步构建型”创建型设计模式,它将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建...

构建第一个 Kotlin Android 应用_kotlin简介

第一步:安装AndroidStudio(推荐IDE)AndroidStudio是官方推荐的Android开发集成开发环境(IDE),内置对Kotlin的完整支持。1.下载And...