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

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

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

作者:连续三届村草


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开发面试专题资料,高级进阶架构资料 这几块的内容

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

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

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


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

相关推荐

迅猛兔加速器(迅猛兔加速器官网)

要下载迅猛兔加速器,首先需要在官网或其他可信的下载平台上搜索并找到该软件。一般情况下,官网提供的下载链接是最稳定和安全的选择。在下载之前,确保您的电脑或手机系统能够支持使用此软件,并检查下载链接的文件...

台式电脑怎么重做系统(台式电脑怎么重装系统)

你好,电脑系统重装的步骤如下:1.备份数据:在重装系统之前,需要备份电脑中的重要数据,以免数据丢失。2.准备安装介质:需要准备一个安装介质,可以是光盘、U盘或者硬盘分区镜像等。3.设置启动顺序:将电脑...

微软office2007安装包(office2007安装包怎么安装)
  • 微软office2007安装包(office2007安装包怎么安装)
  • 微软office2007安装包(office2007安装包怎么安装)
  • 微软office2007安装包(office2007安装包怎么安装)
  • 微软office2007安装包(office2007安装包怎么安装)
电脑无法从u盘启动怎么办(电脑无法从u盘启动解决方法)
电脑无法从u盘启动怎么办(电脑无法从u盘启动解决方法)

电脑的进入不了u盘启动的解决方法:一、我们第一步需要确定的是你的u盘在别的电脑上检查一下U盘是否可读,如果可读的话是否成功制作了u盘启动盘了,因为想要启动进入pe的话需要u盘具备启动的功能。  二、如果你检查好自己的u盘已经成功制作了启动盘...

2026-01-13 10:05 liuian

cpu频率越高越好吗(cpu频率越高速度越快吗)

高好。CPU的频率是影响CPU的一个重要因素,直观上来说,频率的高低影响了CPU的性能。频率越高,CPU性能越好;不过需要注意的是,CPU的主频表示在CPU内数字脉冲信号震荡的速度,与CPU实际的运算...

注册表清理软件(注册表清理软件残留软件)

你好!关于注册表清理工具的推荐,以下是几个值得推荐的工具:1.CCleaner:这是一款功能强大的免费清理工具,可以有效地清理注册表、垃圾文件等,使用简单方便。2.WiseRegistryCl...

显卡驱动升级有好处吗(显卡驱动升级有什么坏处)

显卡的新版本驱动能修改一些游戏,图形显示的BUG,所以新版本的显卡驱动能有效的利用显卡的资源,提高游戏性能。不仅可以修正旧版本中的BUG,而且可以进一步挖掘显卡硬件的功能,使得部分硬件功能得以充分发挥...

w7旗舰版系统安装无线网卡(win7系统安装无线网卡)

要在Windows7中安装无线网卡,请按照以下步骤进行操作:1.检查您的计算机是否已安装无线网卡。您可以通过右键单击“我的电脑”并选择“属性”来查看计算机的硬件设置。如果计算机没有内置无线网卡,则...

腾达路由器管理员密码是什么

1、旧版本的腾达路由器,默认的用户名和密码都是:admin。?旧版腾达路由器的初始密码是:admin2、目前腾达新推出的无线路由器,在出厂状态下,是没有初始管理员密码的。?新版腾达路由器没有初始密码新...

电脑开机只有一个鼠标箭头黑屏

解决方法如下:1、同时按“ctrl+shlft+exc”键,调出任务管理器。2、点击任务管理器左下角的“详细信息”。3、然后点击左上角“文件”里的“运行新任务”。4、弹出新窗口,输入“explorer...

把vx好友删了想找回聊天记录

没有啦,联系人列表里没有了,聊天记录就没有了,无法进行恢复,收不到好友消息微信删除好友时会同时删除与该联系人的聊天记录,不过对方还是有双方的微信聊天记录的,删除好友后将无法发送消息给对方,所以伙伴们在...

163邮箱密码正确就是登不上(163邮箱密码一直错误)

邮箱不能登录或登录异常的原因有很多种哦,如您浏览器“隐私”或“安全”级别设置过高,或用户名、密码输入不正确、较长时间未登录被冻结等都会导致不能登录或登录异常。请您先检查一下哦。解决无法登录的方法有:...

移动硬盘维修费用大概是多少钱

芯片不需要多少钱,但数据恢复就另当别论了。。。如果认识人就帮你换个芯片板,要不了多少钱,如果是硬盘盒的芯片板坏了你就乾脆换个盒子,80左右。如果是硬盘芯片坏了,那就不好办了,没人愿意给你换阿。。。但如...

windows资源管理器停止工作是什么原因

1.在进行重装系统之前,可以先检测一下windows资源管理器停止工作的原因是什么。如果是因为电脑的文件太多了,垃圾堆积导致的停止工作,我们就不需要进行重装系统。我们只需要下载一个360卫士或者其他可...

联想电脑24小时维修热线电话

   1.打开Think.lenovo.com.cn网页,点击登陆。  2.输入用户名密码,点击登陆。  3.点击右上角的:返回个性化首页。  4.点击“咨询与报修”中的“网上报修”。 ...