【安卓基础】一文搞懂Android历代版本文件访问权限变化

2024-03-21 09:59

本文主要是介绍【安卓基础】一文搞懂Android历代版本文件访问权限变化,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 前言
  • 1、几个目录
    • 1.1 私有目录
      • 1.1.1 api < 19
      • 1.1.2 18 <= api < 23
      • 1.1.3 23 <= api
    • 1.2 共享目录
  • 2、文件读写
      • 2.1 api < 19
        • 2.1.1 读文件
        • 2.1.2 写文件
      • 2.2 19 <= api < 23
        • 2.2.1 读文件
        • 2.2.2 写文件
      • 2.3 23 <= api <= 28
      • 2.4 28 < api <= 29
      • 2.5 29 < api
  • 3、总结


前言

运行三年的App为何说崩就崩?数十种设备为何表现不一?新晋安卓开发者为何频频流泪?十年安卓资深开发为何深夜惨叫?这一切的背后,是技术的薄弱,还是谷歌的坑爹

1、几个目录

1.1 私有目录

App-specific storage: Store files that are meant for your app’s use only, either in dedicated directories within an internal storage volume or different dedicated directories within external storage. Use the directories within internal storage to save sensitive information that other apps shouldn’t access.

通过上面官网的描述可知,私有目录也分为两种:内部存储和外部存储的私有目录。
下面我们分别来打印这两个目录的路径:

/*** 打印内存存储、外部存储(私有目录)及外部存储(共享目录)的路径,不会校验权限* 内存存储:Context#getFilesDir,Context#getCacheDir* 外部存储(私有目录):Context#getExternalFilesDir,Context#getExternalCacheDir* 外部存储(共享目录):* @param pathTypes 打印的路径类型* @see [PRINT_EXTERNAL_VOLUME_SHARE][PRINT_EXTERNAL_VOLUME_SPECIFIC][PRINT_EXTERNAL_VOLUME_SHARE]*/private fun printStoragePathsUnChecked(pathTypes: Int) {val appContext = thisval paths = StringBuilder().apply {if (PRINT_INTERNAL_VOLUME and pathTypes != 0) {append("\n${getString(R.string.internal_storage_volume)}: ${appContext.filesDir}").append("\t${appContext.cacheDir}")}if (PRINT_EXTERNAL_VOLUME_SPECIFIC and pathTypes != 0) {append("\n${getString(R.string.external_storage_volume_specific)}: ${appContext.getExternalFilesDir(Environment.DIRECTORY_MUSIC)}").append("\t${appContext.externalCacheDir}")}if (PRINT_EXTERNAL_VOLUME_SHARE and pathTypes != 0) {append("\n${getString(R.string.external_storage_volume_share)}:${Environment.getExternalStorageDirectory()}")}}Log.d(TAG, paths.toString())}

正常情况输出如下:

内部存储: /data/data/com.codersth.android.foundation/files	/data/data/com.codersth.android.foundation/cache
外部存储(私有目录,以Music为例): /storage/sdcard/Android/data/com.codersth.android.foundation/files/Music

下面我们对不同的系统版本,再来看看上面的输出。

1.1.1 api < 19

我们创建api18的虚拟机,在不加任何存储权限的情况下,再次运行上面的代码。

内部存储: /data/data/com.codersth.android.foundation/files	/data/data/com.codersth.android.foundation/cache
外部存储(私有目录): null	null

可以看到,虽然没有提示权限问题,但外部存储(私有目录)拿不到具体路径。
尝试加上读取权限:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

结果仍然为null。
只有加上写入权限,才是正常的。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

关于这一点,我们可以在官网上得到验证。

Also starting in API level 19, this permission is not required to read/write files in your application-specific directories returned by Context.getExternalFilesDir(String) and Context.getExternalCacheDir().

也就是说,从api19开始,打印外部存储(私有目录)路径才不需要WRITE_EXTERNAL_STORAGE这个权限。

1.1.2 18 <= api < 23

接下面我们再建一个api为19的虚拟机,同样跑上述代码。
在不加任何存储权限的情况下,可以正常输出,大家不妨自己试下。

1.1.3 23 <= api

自从Android6.0(api23)有个重大变化就是增加了动态权限,而且READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE属于Dangerous。
那么读取上面两个目录要不要这些权限呢?
我们再拿api=23的虚拟机跑下。

内部存储: /data/user/0/com.codersth.android.foundation/files	/data/user/0/com.codersth.android.foundation/cache
外部存储(私有目录): /storage/emulated/0/Android/data/com.codersth.android.foundation/files/Music	/storage/emulated/0/Android/data/com.codersth.android.foundation/cache

结果输出正常。从而说明内部存储和外部存储(私有目录)尽管在6.0系统上仍然不用存储权限说明,即使是清单文件中的权限声明都不需要。
最后我们在最新的api30的虚拟机上跑下,仍然正常

总结一下,关于打印路径:

1、内部存储任何版本都不需要存储权限,
2、外部存储(私有目录)仅在api19以下需要声明权限WRITE_EXTERNAL_STORAGE

1.2 共享目录

下面我们看下外部存储的共有目录,也就是所谓的共享目录,即不同应用都可访问的位置,比如相册、下载、音乐等。
我们稍微改下代码,仅输出共享目录:

printStoragePaths(PRINT_EXTERNAL_VOLUME_SHARE)

输出结果为:

外部存储(共享目录):/storage/emulated/0

那么这个目录在不同的系统版本中的访问是否有差异呢?
结论是:无差异,即任何版本都可以在不声明存储权限的情况下打印出共享目录的路径。

大家可以参考上面的过程自行尝试。

2、文件读写

下面我们分别在上述三个目录中创建文件,来看看不同系统对文件访问的差异。

2.1 api < 19

2.1.1 读文件

测试代码如下:

/*** 在不同的目录下读取文件, 注意:先放入对应文件。* @param pathTypes 打印的路径类型* @see [PRINT_EXTERNAL_VOLUME_SHARE][PRINT_EXTERNAL_VOLUME_SPECIFIC][PRINT_EXTERNAL_VOLUME_SHARE]*/private fun readStorageUnChecked(pathTypes: Int) {// 分别在指定目录放一个此名称的文件val fileName = "testread.txt"if (PRINT_INTERNAL_VOLUME and pathTypes != 0) {val file = File(filesDir, fileName)Log.d(TAG, "${getString(R.string.internal_storage_volume)}: read file content: ${readFileContent(file)}")}if (PRINT_EXTERNAL_VOLUME_SPECIFIC and pathTypes != 0) {val file = File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), fileName)Log.d(TAG, "${getString(R.string.external_storage_volume_specific)}: read file content: ${readFileContent(file)}")}if (PRINT_EXTERNAL_VOLUME_SHARE and pathTypes != 0) {val file = File(Environment.getExternalStorageDirectory(), fileName)Log.d(TAG, "${getString(R.string.external_storage_volume_share)}: read file content: ${readFileContent(file)}")}}

我们事先分别在三个目录中上传文件。
在这里插入图片描述
然后执行:

readStorageUnChecked(PRINT_INTERNAL_VOLUME or PRINT_EXTERNAL_VOLUME_SPECIFIC or PRINT_EXTERNAL_VOLUME_SHARE)

结果正常,都输出了文件中的内容:

2022-02-10 06:16:03.422 7725-7725/com.codersth.android.foundation D/FSMainActivity: 内部存储: read file content: hello, android developer
2022-02-10 06:16:03.422 7725-7725/com.codersth.android.foundation D/FSMainActivity: 外部存储(私有目录): read file content: hello, android developer
2022-02-10 06:16:03.422 7725-7725/com.codersth.android.foundation D/FSMainActivity: 外部存储(共享目录): read file content: hello, android developer

所以说在api < 19的情况下,不用任何权限均可以正常读取文件。

2.1.2 写文件

测试代码如下:

/*** 在不同的目录下创建文件* @param pathTypes 打印的路径类型* @see [PRINT_EXTERNAL_VOLUME_SHARE][PRINT_EXTERNAL_VOLUME_SPECIFIC][PRINT_EXTERNAL_VOLUME_SHARE]*/private fun writeStorageUnChecked(pathTypes: Int) {if (PRINT_INTERNAL_VOLUME and pathTypes != 0) {val file = File(filesDir, "${System.currentTimeMillis()}.txt")Log.d(TAG, "${getString(R.string.internal_storage_volume)}: create file ${file.createNewFile()}, file path: ${file.absolutePath}")}if (PRINT_EXTERNAL_VOLUME_SPECIFIC and pathTypes != 0) {val file = File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "${System.currentTimeMillis()}.txt")Log.d(TAG, "${getString(R.string.external_storage_volume_specific)}: create file ${file.createNewFile()}, file path: ${file.absolutePath}")}if (PRINT_EXTERNAL_VOLUME_SHARE and pathTypes != 0) {val file = File(Environment.getExternalStorageDirectory(), "${System.currentTimeMillis()}.txt")Log.d(TAG, "${getString(R.string.external_storage_volume_share)}: create file ${file.createNewFile()}, file path: ${file.absolutePath}")}}

我们先在内部存储中创建文件:

accessStorageUnChecked(PRINT_INTERNAL_VOLUME)

再看下目录里,文件确实存在。
在这里插入图片描述
接在我们在外部存储(私有目录)中创建文件,可以看到报错了。

libcore.io.ErrnoException: open failed: EROFS (Read-only file system)

我们加入WRITE_EXTERNAL_STORAGE权限再试下。

2022-02-10 05:31:36.302 5737-5737/com.codersth.android.foundation D/FSMainActivity: 外部存储(私有目录): create file true, file path: /storage/sdcard/Android/data/com.codersth.android.foundation/files/Download/1644471096304.txt

外部存储共享目录同样会报错,大家不妨自己试下。
所以说在api < 19的情况下写文件,内部存储不用权限,外部存储需要WRITE_EXTERNAL_STORAGE。

2.2 19 <= api < 23

2.2.1 读文件


在api=19的虚拟机上测试,测试方法与2.1节相同,这里不再赘述。

2022-02-10 06:44:45.766 7337-7337/com.codersth.android.foundation D/FSMainActivity: 内部存储: read file content: hello, android developer
2022-02-10 06:44:45.766 7337-7337/com.codersth.android.foundation D/FSMainActivity: 外部存储(私有目录): read file content: hello, android developer
2022-02-10 06:44:45.766 7337-7337/com.codersth.android.foundation E/AndroidRuntime: FATAL EXCEPTION: mainProcess: com.codersth.android.foundation, PID: 7337java.lang.RuntimeException: Unable to start activity ComponentInfo{com.codersth.android.foundation/com.codersth.android.foundation.filesystem.FSMainActivity}: java.io.FileNotFoundException: /storage/sdcard/testread.txt: open failed: at dalvik.system.NativeStart.main(Native Method) Caused by: libcore.io.ErrnoException: open failed: EACCES (Permission denied)at libcore.io.Posix.open(Native Method)at libcore.io.BlockGuardOs.open(BlockGuardOs.java:110)at libcore.io.IoBridge.open(IoBridge.java:393)

结论:私有目录不用任何权限即可读文件,共享目录需要READ_EXTERNAL_STORAGE权限。

2.2.2 写文件


在api=19的虚拟机上测试,测试方法与2.1节相同,这里不再赘述。
日志如下:

2022-02-10 06:29:44.226 6793-6793/com.codersth.android.foundation D/FSMainActivity: 内部存储: create file true, file path: /data/data/com.codersth.android.foundation/files/1644474584242.txt
2022-02-10 06:29:44.256 6793-6793/com.codersth.android.foundation D/FSMainActivity: 外部存储(私有目录): create file true, file path: /storage/sdcard/Android/data/com.codersth.android.foundation/files/Download/1644471096304.txtCaused by: libcore.io.ErrnoException: open failed: EACCES (Permission 

结论:私有目录不用任何权限即可写入文件,共享目录需要WRITE_EXTERNAL_STORAGE权限。

2.3 23 <= api <= 28

这个与api = 19时一样,只不过在读写的时候要动态赋予权限。值得一提的是,android >= 23加入多用户,所以内部存储路径与之前版本不同,具体可看第1节中打印的地址。
*:部分版本中无法查看/data/user目录(其实是存在的),可先调用写入方法写入文件再测试读取。
在这里插入图片描述
结论:私有目录不用任何权限即可读写文件,共享目录读写时需要分别赋予READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限。

2.4 28 < api <= 29

我们用上面的流程在api = 29上跑了下,写文件在私有目录上是成功的,但是在共享目录尽管动态给了权限,但仍然说提示没权限。

     Caused by: java.io.IOException: Permission deniedat java.io.UnixFileSystem.createFileExclusively0(Native Method)at java.io.UnixFileSystem.createFileExclusively(UnixFileSystem.java:317)at java.io.File.createNewFile(File.java:1008)at com.codersth.android.foundation.filesystem.FSMainActivity.writeStorageUnChecked(FSMainActivity.kt:174)

查阅官网有这样的描述:
在这里插入图片描述
可以看到,api28引用分区存储概念,同时对共享目录存储做了限制,即只对在功能上需要使用共享目录的应用开放这个权限,比如文件管理应用、垃圾清理软件等。
然而,在api29及以下版本中,我们仍然可以通过在清单文件中声明
requestLegacyExternalStorage = true来选择停用分区存储。
在这里插入图片描述
修改清单文件后,可以看到,写入正常:

2022-02-10 16:19:29.290 2018-2036/system_process I/ActivityTaskManager: START u0 {act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.codersth.android.foundation/.filesystem.FSMainActivity} from uid 2000
2022-02-10 16:19:29.636 8543-8543/com.codersth.android.foundation D/FSMainActivity: 内部存储: create file true, file path: /data/user/0/com.codersth.android.foundation/files/1644481169636.txt
2022-02-10 16:19:29.638 8543-8543/com.codersth.android.foundation D/FSMainActivity: 外部存储(私有目录): create file true, file path: /storage/emulated/0/Android/data/com.codersth.android.foundation/files/Download/1644481169638.txt
2022-02-10 16:19:29.639 8543-8543/com.codersth.android.foundation D/FSMainActivity: 外部存储(共享目录): create file true, file path: /storage/emulated/0/1644481169639.txt

值得一提的是,在实际的开发过程中,我们仍然以官网建议为准备,即尽量从私有目录中存储,除非应用实在需要访问共享目录。

结论:私有目录不用任何权限即可读写文件,共享目录读写时分别赋予READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE仍没卵用,可通过requestLegacyExternalStorage = true来暴力解决这个问题。
自此,WRITE_EXTERNAL_STORAGE在api >= 29上将寿终正寝。

2.5 29 < api

如果说api29及以下可以通过requestLegacyExternalStorage = true来解决共享目录存储问题的话,api = 30开始,google则彻底堵死了这个口子。
在这里插入图片描述
我们看到,尽管上面该加的都加了,仍然会报错:

Caused by: java.io.IOException: Operation not permittedat java.io.UnixFileSystem.createFileExclusively0(Native Method)at java.io.UnixFileSystem.createFileExclusively(UnixFileSystem.java:317)

也就是说api29(Android 11)开始,google是逼着你用分区存储模型了。
那么api > 29该如何使用共享目录呢?
首先,在清单文件中声明权限:

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>

然后,增加对Android R的权限处理。

// 判断当前应用是否管理外部存储的权限if(Environment.isExternalStorageManager()) {writeStorageUnChecked(pathTypes)} else {// 赋予权限val forResultLauncher = registerForActivityResult(ManagerStorageContract()) {if (Environment.isExternalStorageManager()) {writeStorageUnChecked(pathTypes)} else {Toast.makeText(this, "Permission not granted.", Toast.LENGTH_SHORT).show();}}forResultLauncher.launch(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)}

这样,在访问文件前,会进入这样的页面,用户开启则表示该应用有管理共享目录的权限。
在这里插入图片描述
这样便成功写入文件了。

2022-02-10 17:21:00.103 8165-8165/com.codersth.android.foundation D/FSMainActivity: 内部存储: create file true, file path: /data/user/0/com.codersth.android.foundation/files/1644484860103.txt
2022-02-10 17:21:00.122 8165-8165/com.codersth.android.foundation D/FSMainActivity: 外部存储(私有目录): create file true, file path: /storage/emulated/0/Android/data/com.codersth.android.foundation/files/Download/1644484860121.txt
2022-02-10 17:21:00.126 8165-8165/com.codersth.android.foundation D/FSMainActivity: 外部存储(共享目录): create file true, file path: /storage/emulated/0/1644484860124.txt

读文件同理,不再赘述。
结论:对于api 30及以上版本,私有目录可正常访问,但是共享目录需要用户在明确自己应用用途的情况下设置中开启“访问外部存储”的开关。

3、总结

以上便介绍完了Android历代版本中文访问权限变化,可以看到,单纯访问文件这件事,在个别版本中都千差万别。
在实际开发过程中,强烈建议大家遵守google开发规范,合理存放自己的数据。

仓库地址:https://github.com/codersth/android-foundation-samples/blob/master/app/src/main/java/com/codersth/android/foundation/filesystem/FSMainActivity.kt

这篇关于【安卓基础】一文搞懂Android历代版本文件访问权限变化的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/832383

相关文章

Android开发中gradle下载缓慢的问题级解决方法

《Android开发中gradle下载缓慢的问题级解决方法》本文介绍了解决Android开发中Gradle下载缓慢问题的几种方法,本文给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧... 目录一、网络环境优化二、Gradle版本与配置优化三、其他优化措施针对android开发中Gradle下载缓慢的问

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeek R1模型的操作流程

《0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeekR1模型的操作流程》DeepSeekR1模型凭借其强大的自然语言处理能力,在未来具有广阔的应用前景,有望在多个领域发... 目录0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeek R1模型,3步搞定一个应

Android里面的Service种类以及启动方式

《Android里面的Service种类以及启动方式》Android中的Service分为前台服务和后台服务,前台服务需要亮身份牌并显示通知,后台服务则有启动方式选择,包括startService和b... 目录一句话总结:一、Service 的两种类型:1. 前台服务(必须亮身份牌)2. 后台服务(偷偷干

浅析如何使用Swagger生成带权限控制的API文档

《浅析如何使用Swagger生成带权限控制的API文档》当涉及到权限控制时,如何生成既安全又详细的API文档就成了一个关键问题,所以这篇文章小编就来和大家好好聊聊如何用Swagger来生成带有... 目录准备工作配置 Swagger权限控制给 API 加上权限注解查看文档注意事项在咱们的开发工作里,API

本地搭建DeepSeek-R1、WebUI的完整过程及访问

《本地搭建DeepSeek-R1、WebUI的完整过程及访问》:本文主要介绍本地搭建DeepSeek-R1、WebUI的完整过程及访问的相关资料,DeepSeek-R1是一个开源的人工智能平台,主... 目录背景       搭建准备基础概念搭建过程访问对话测试总结背景       最近几年,人工智能技术

Ollama整合open-webui的步骤及访问

《Ollama整合open-webui的步骤及访问》:本文主要介绍如何通过源码方式安装OpenWebUI,并详细说明了安装步骤、环境要求以及第一次使用时的账号注册和模型选择过程,需要的朋友可以参考... 目录安装环境要求步骤访问选择PjrIUE模型开始对话总结 安装官方安装地址:https://docs.

java中不同版本JSONObject区别小结

《java中不同版本JSONObject区别小结》本文主要介绍了java中不同版本JSONObject区别小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们... 目录1. FastjsON2. Jackson3. Gson4. org.json6. 总结在Jav

一文详解Java Condition的await和signal等待通知机制

《一文详解JavaCondition的await和signal等待通知机制》这篇文章主要为大家详细介绍了JavaCondition的await和signal等待通知机制的相关知识,文中的示例代码讲... 目录1. Condition的核心方法2. 使用场景与优势3. 使用流程与规范基本模板生产者-消费者示例

Android kotlin语言实现删除文件的解决方案

《Androidkotlin语言实现删除文件的解决方案》:本文主要介绍Androidkotlin语言实现删除文件的解决方案,在项目开发过程中,尤其是需要跨平台协作的项目,那么删除用户指定的文件的... 目录一、前言二、适用环境三、模板内容1.权限申请2.Activity中的模板一、前言在项目开发过程中,尤