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


前言

运行三年的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 = this val 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: main Process: com.codersth.android.foundation, PID: 7337 java.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.txt Caused 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 denied at 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 permitted at 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开发规范,合理存放自己的数据。

仓库地址:android-foundation-samples/FSMainActivity.kt at master · codersth/android-foundation-samples · GitHub

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CAN(Controller Area Network,控制器局域网)总线协议是一种广泛应用于工业自动化、汽车电子等领域的串行通讯协议。其帧格式如下: <img src="https://img-blog.csdnimg.cn/20200925125252655.png" width="400"> CAN总线协议的帧分为标准帧和扩展帧两种,其中标准帧包含11位标识符,扩展帧包含29位标识符。在CAN总线上,所有节点都可以同时发送和接收数据,因此需要在帧中包含发送方和接收方的信息。 帧格式的具体解释如下: 1. 帧起始符(SOF):一个固定的位模式,表示帧的起始。 2. 报文控制(CTRL):包含几个控制位,如IDE、RTR等。其中IDE表示标识符的类型,0表示标准帧,1表示扩展帧;RTR表示远程请求帧,0表示数据帧,1表示远程请求帧。 3. 标识符(ID):11位或29位的标识符,用于区分不同的CAN消息。 4. 控制域(CTL):包含几个控制位,如DLC、EDL等。其中DLC表示数据长度,即数据域的字节数;EDL表示数据长度是否扩展,0表示标准数据帧,1表示扩展数据帧。 5. 数据域(DATA):0~8字节的数据。 6. CRC:用于校验数据是否正确。 7. 确认位(ACK):由接收方发送的确认信息,表示数据是否正确接收。 8. 结束符(EOF):一个固定的位模式,表示帧的结束。 以上就是CAN总线协议的帧格式。在实际应用中,节点之间通过CAN总线进行数据交换,通过解析帧中的各个字段,可以判断消息的发送方、接收方、数据内容等信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值