基于亿级设备梳理下Android存储目录结构体系,及特例机型_同时声明write_external_storage和read_external_storage-程序员宅基地

技术标签: android  

Android存储

所有Android设备都有两个存储区域:“内部”存储和“外部”存储,这些名词是Android早期产生。
一些设备会把内置存储介质(通常为Flash)划分为系统分区和扩展分区,即使没有扩展sd卡,也会有“内部”存储和“外部”存储之分。
因此,内部存储通常是指系统分区(/data所在分区),外部存储通常指扩展分区,可以在内置Flash上,也可以是移动存储介质;
image.png

本文名词解释

1、内部存储:指系统分区(/data),以区分内置卡;
2、外部存储:指系统分区之外的扩展分区;
3、内置卡:指内部存储介质(通常为Flash),不可移除;
4、外置卡:指扩展sd卡,可以随意拔插;
5、主存储(Primary存储)分区:Google定义的默认外部存储分区,即Environment.getExternalStorageDirectory()返回路径所在分区;
而这个API的返回结果是由OEM厂商决定,可以随意更改,不一定在内置卡上。
主存储分区主要是针对支持多个外部存储的设备而言;

Android应用可用存储目录

1、内部存储应用私有目录

通常为/data/data/{package_name}/路径,App私有,普通应用通常没有权限访问;sharepreference和database等数据也存储在该目录下。
###Android SDK提供的API
Context.getFilesDir(): 返回/data/data/{package_name}/files目录,存储app相关私有数据
Context.getCacheDir(): 返回/data/data/{package_name}/cache目录,存储app相关缓存数据,在存储空间不足时,可能会被系统自动清除
读写权限相关:所有Android版本都不需要申请读写权限,app可以直接访问。
image.png

2、外部存储应用私有目录

通常是指外部存储分区上Android/data/{package_name}/路径下的files或cache目录;
Android SDK提供的API
Context.getExternalFilesDir(String): 返回外部Primary存储Android/data/{package_name}/files目录,存储app相关数据;
Context.getExternalCacheDir(): 返回外部Primary存储Android/data/{package_name}/cache目录,存储app相关缓存数据,在存储空间不足时,可能会被系统自动清除

Android 4.4新增
Context.getExternalFilesDirs(String): 返回所有外部存储Android/data/{package_name}/files目录,数组形式

Context.getExternalCacheDirs(): 返回所有外部存储Android/data/{package_name}/cache目录,数组形式
Android 5.0新增
Context.getExternalMediaDirs(): 返回所有外部存储上的媒体文件存储目录,这些文件可以被MediaStore扫描到
读写权限相关:Android 4.4以下需要申请权限,4.4以上不再需要申请权限
image.png

Note:应用被卸载时,该目录下的所有数据都会被清除。

其他应用申请了WRITE_EXTERNAL_STORAGE权限,也能够读写该目录。

特例华为某些机型(荣耀X2、荣耀7)修改默认存储为外置卡时,如果应用没有申请WRITE_EXTERNAL_STORAGE权限,此时外置卡的该目录将不可写

3、外部存储公用路径

指外部存储上非应用私有目录的普通目录,需要申请权限;
Android SDK提供的API
Environment.getExternalStorageDirectory(): 返回外部Primary存储的根目录
Environment.getExternalStoragePublicDirectory(String): 返回外部Primary存储上的Picture、Music、DCIM等公用路径
读写权限相关:所有Android版本都要申请权限
image.png
Note: Android 4.4权限WRITE_EXTERNAL_STORAGE变更
在Android 4.4以下,WRITE_EXTERNAL_STORAGE权限对所有的外部存储分区(内置卡和外置卡)都有效;
Android 4.4之后,WRITE_EXTERNAL_STORAGE权限只对内置卡有效,对外置sd卡无效,应用将没有权限写外置卡的普通目录;
但是某些厂商可以通过定制ROM或修改默认存储开放外置卡的写权限。

Android外部存储权限

1、READ_EXTERNAL_STORAGE 读外部存储权限,Android 4.1新增
2、WRITE_EXTERNAL_STORAGE 写外部存储权限,一直都有;
3、WRITE_MEDIA_STORAGE 写外置sd卡权限,4.0-4.3存在,4.4该权限被回收,只授予系统级app,需要系统签名。
Note: WRITE_EXTERNAL_STORAGE权限隐式包含了READ_EXTERNAL_STORAGE权限。

存储权限的版本演变

1、Android 4.1以下版本没有READ_EXTERNAL_STORAGE权限,外部存储是全局可读的;4.1-4.3版本,用户可以通过开发者选项开启对SD卡的读保护,最好声明读权限;
从Android 4.4版本开始,系统强制对外部存储的读权限进行校验;

2、WRITE_EXTERNAL_STORAGE权限一直都有,Android 4.4以下版本对外部存储所有目录都有效,Android 4.4以后,该权限只对内置卡有效,对外置sd卡无效;
3、Android 6.0的动态权限,READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE都隶属于STORAGE权限组;
当动态申请权限时,用户授权之后,在Manifest中声明过的同组的另一个权限自动获取。

(1) Manifest中只声明READ_EXTERNAL_STORAGE权限,动态申请权限用户授权之后,只能获取READ_EXTERNAL_STORAGE权限;(因为WRITE_EXTERNAL_STORAGE权限没有在Manifest中声明)

(2) Manifest中只声明WRITE_EXTERNAL_STORAGE权限,动态申请权限用户授权之后,同时获取READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限;(因为WRITE_EXTERNAL_STORAGE隐式包含READ_EXTERNAL_STORAGE权限的声明)

(3) Manifest中同时声明READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限,结果同(2)。
因此,在适配Android 6.0时,需要存储数据到外部存储根目录时,一定记得检查权限和申请权限;
Android 4.4(Kitkat)之后,外置sd卡根目录的写权限被收紧,应用只能写自己的私有目录。
同时Google引入了存储访问框架SAF,让用户能够在首选文档存储程序中方便地浏览并打开文件、图像以及其他文件。
Android 5.0(Lollipop),google引入了Intent.ACTION_CREATE_DOCMENT_TREE可以向SAF框架申请目录的临时读写权限。
SAF框架的使用和集成请参考链接:https://developer.android.com/guide/topics/providers/document-provider.html

Android SDK API的局限性

1、使用Context.getExternalFilesDir()或Environment.getExternalStorageDiretory()通常只能获取一张卡的存储路径,两者返回的是同一个存储分区;

2、在某些机型上,这两个系统API可能拿不到路径,返回值为null,出现空指针异常现象,如ZTEU817、ZTEU950,需要做空指针判断或保护;

3、这两个API在不同的机型上返回的路径可能是内置卡,也可能是外置卡;

4、这两个API在某些机型上插拔卡或修改默认存储时返回的路径会发生动态变化,比如未插扩展sd卡时,返回内置卡的路径,插上扩展sd卡后,返回外置卡的路径,如红米1,、华为X2。

5、Android 4.4上新增的Context.getExternalFilesDirs()方法可以获取多张卡的路径,但是在某些机型上会出现获取路径不全的现象,如华为畅享5。

6、Environment.getExternalStorageDiretory()和Environment.geteExternalStoragePublicDirectory(String)返回SD卡根路径下相关目录,需要申请读写权限,特别是Android 6.0以上需要动态申请权限;
如果直接读写SD卡根目录,而没有检查并确保获取相应权限,应用会因权限问题抛出异常。
对SD卡进行文件IO操作时,应确保获取了相应的权限,特别注意Android 6.0上的动态权限申请。

SD卡扫描方式

1、反射StorageManager和StorageVolume的隐藏API
2、读取/proc/mounts或执行linux mount命令
反射方式原理

StorageManager类是一项系统服务,有3个隐藏API可以获取存储分区的路径和状态, 以下3个API在4.0上可用

  • StorageVolume[] getVolumeList(): 获取所有的存储分区

  • String[] getVolumePaths(): 获取所有存储分区的路径

  • String getVolumeState(String mountPoint): 获取存储分区的状态
    Note:getVolumePaths()和getVolumeState(String)在高版本中标记为deprecated
    StorageVolume类是系统的一个隐藏类,标识了一个存储分区,它提供了一系列相关API获取该分区的信息,该类于4.0引入

  • long getMaxFileSize(): 获取文件大小

  • String getPath(): 获取分区路径

  • File getPathFile(): 4.2+, 返回File类型路径

  • String getState(): 4.4+, 返回分区的状态(挂载还是卸载等)

  • boolean isEmulated(): 是否模拟出来的分区

  • boolean isRemovable(): 该分区是否可移除

  • boolean isPrimary(): 4.2+, 该分区是否是主分区

通过反射这两个类的相关方法,基本上可以正常获取到所有sd卡的存储路径。
Note:目前通过StorageVolume类的**isPrimary()**区分内置卡还是外置卡;机型测试结果表示,准确性较高,暂时发现只在机型ZTEV987上出现兼容性问题,该机型将外置SD卡分区的isPrimary()方法标记为true。而isPrimary()方法由于是隐藏的,因此与系统API Environment.getExternalStorageDiretory()结果并不保持一致;

机型测试结果表示,isRemovable()能够区分内置和外置卡,由于没有线上大面积测试验证过,因此没有以此作为区分内置卡和外置卡的依据。

读取mount表原理

mount是linux的一个shell命令,可以返回所有挂载分区的信息,需要从一堆的挂载点中筛选出可能的sd卡存储路径。

mount表格式:
<设备名> <挂载点> <文件系统> <可读写,所属用户,所属组等信息> <整数1> <整数2>

Android外部存储挂载点特点:
第一张sd卡(通常称为主卡)的挂载点演变:

  • Android 2.3, 挂载到/mnt/sdcard,/sdcard软连接指向它
  • Android 4.1, 挂载到/storage/sdcard0, /sdcard, /mnt/sdcard软连接指向它
  • Android 4.2, 开始支持多用户,挂载到/storage/emulated/0,同时挂载到/storage/emulated/legacy向下兼容,并建立三个软连接/storage/sdcard0, /sdcard, /mnt/sdcard指向/storage/emulated/legacy
  • Android 4.4, 挂载到/mnt/shell/emulated/0, 建立软连接/storage/emualted/legacy指向它

第二张sd卡的挂载点没有标准,各厂商自行定义
可能挂载点:/mnt/external, /mnt/extSdCard, /mnt/sdcard/ext_sd等

问题:

  1. 返回的挂载信息很多,需要进行过滤筛选出有效的sd卡路径;
  2. 很多挂载路径可能是软连接,需要标准化,如/mnt/sdcard—>/storage/emulated/0;
  3. 同一个设备可能会挂载到多个目录下,需要过滤。

解决方案:

  1. 通过关键词和可读写性过滤,多机型测试整理常见的关键词;
  2. 通过File.geteCanonicalPath可以对路径进行标准化,将软连接转换为真实的路径;
  3. 通过创建隐藏文件(指纹)对同一个设备的多个目录进行过滤;

常见关键词(逆向分析和测试经验值)
设备名:/dev/block/vold, /dev/block/sd, /dev/sd, /dev/fuse, /dev/lfuse等
挂载点:emmc, storage, sdcard, external, ext_sd, extSdCard等
缺点:内置卡和外置卡的挂载顺序是不确定的,无法准确区分内置卡和外置卡。

总结
  1. 反射StorageManager和StorageVolume的方法最为靠谱,兼容性较好;
  2. 读取mount表经过过滤和筛选也能正确的获取所有存储路径,但无法区分内置卡和外置卡;
  3. 目前SD卡扫描方式采取两种方式相互补充的形式,在反射调用出现异常的时候,再选用读取mount表的方式。
android Q 以上新幺蛾子

image.png
Android 采用了沙箱模型,应用只能使用自己独立的存储空间,不能直接像过去一样通过公共目录方式,存储文件。

开发者需要做相应的适配动作。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/jjbheda/article/details/103749840

智能推荐

c# 调用c++ lib静态库_c#调用lib-程序员宅基地

文章浏览阅读2w次,点赞7次,收藏51次。四个步骤1.创建C++ Win32项目动态库dll 2.在Win32项目动态库中添加 外部依赖项 lib头文件和lib库3.导出C接口4.c#调用c++动态库开始你的表演...①创建一个空白的解决方案,在解决方案中添加 Visual C++ , Win32 项目空白解决方案的创建:添加Visual C++ , Win32 项目这......_c#调用lib

deepin/ubuntu安装苹方字体-程序员宅基地

文章浏览阅读4.6k次。苹方字体是苹果系统上的黑体,挺好看的。注重颜值的网站都会使用,例如知乎:font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, W..._ubuntu pingfang

html表单常见操作汇总_html表单的处理程序有那些-程序员宅基地

文章浏览阅读159次。表单表单概述表单标签表单域按钮控件demo表单标签表单标签基本语法结构<form action="处理数据程序的url地址“ method=”get|post“ name="表单名称”></form><!--action,当提交表单时,向何处发送表单中的数据,地址可以是相对地址也可以是绝对地址--><!--method将表单中的数据传送给服务器处理,get方式直接显示在url地址中,数据可以被缓存,且长度有限制;而post方式数据隐藏传输,_html表单的处理程序有那些

PHP设置谷歌验证器(Google Authenticator)实现操作二步验证_php otp 验证器-程序员宅基地

文章浏览阅读1.2k次。使用说明:开启Google的登陆二步验证(即Google Authenticator服务)后用户登陆时需要输入额外由手机客户端生成的一次性密码。实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。下载谷歌验证类库文件放到项目合适位置(我这边放在项目Vender下面)https://github.com/PHPGangsta/GoogleAuthenticatorPHP代码示例://引入谷_php otp 验证器

【Python】matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距-程序员宅基地

文章浏览阅读4.3k次,点赞5次,收藏11次。matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距

docker — 容器存储_docker 保存容器-程序员宅基地

文章浏览阅读2.2k次。①Storage driver 处理各镜像层及容器层的处理细节,实现了多层数据的堆叠,为用户 提供了多层数据合并后的统一视图②所有 Storage driver 都使用可堆叠图像层和写时复制(CoW)策略③docker info 命令可查看当系统上的 storage driver主要用于测试目的,不建议用于生成环境。_docker 保存容器

随便推点

网络拓扑结构_网络拓扑csdn-程序员宅基地

文章浏览阅读834次,点赞27次,收藏13次。网络拓扑结构是指计算机网络中各组件(如计算机、服务器、打印机、路由器、交换机等设备)及其连接线路在物理布局或逻辑构型上的排列形式。这种布局不仅描述了设备间的实际物理连接方式,也决定了数据在网络中流动的路径和方式。不同的网络拓扑结构影响着网络的性能、可靠性、可扩展性及管理维护的难易程度。_网络拓扑csdn

JS重写Date函数,兼容IOS系统_date.prototype 将所有 ios-程序员宅基地

文章浏览阅读1.8k次,点赞5次,收藏8次。IOS系统Date的坑要创建一个指定时间的new Date对象时,通常的做法是:new Date("2020-09-21 11:11:00")这行代码在 PC 端和安卓端都是正常的,而在 iOS 端则会提示 Invalid Date 无效日期。在IOS年月日中间的横岗许换成斜杠,也就是new Date("2020/09/21 11:11:00")通常为了兼容IOS的这个坑,需要做一些额外的特殊处理,笔者在开发的时候经常会忘了兼容IOS系统。所以就想试着重写Date函数,一劳永逸,避免每次ne_date.prototype 将所有 ios

如何将EXCEL表导入plsql数据库中-程序员宅基地

文章浏览阅读5.3k次。方法一:用PLSQL Developer工具。 1 在PLSQL Developer的sql window里输入select * from test for update; 2 按F8执行 3 打开锁, 再按一下加号. 鼠标点到第一列的列头,使全列成选中状态,然后粘贴,最后commit提交即可。(前提..._excel导入pl/sql

Git常用命令速查手册-程序员宅基地

文章浏览阅读83次。Git常用命令速查手册1、初始化仓库git init2、将文件添加到仓库git add 文件名 # 将工作区的某个文件添加到暂存区 git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件...

分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120-程序员宅基地

文章浏览阅读202次。分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120

【C++缺省函数】 空类默认产生的6个类成员函数_空类默认产生哪些类成员函数-程序员宅基地

文章浏览阅读1.8k次。版权声明:转载请注明出处 http://blog.csdn.net/irean_lau。目录(?)[+]1、缺省构造函数。2、缺省拷贝构造函数。3、 缺省析构函数。4、缺省赋值运算符。5、缺省取址运算符。6、 缺省取址运算符 const。[cpp] view plain copy_空类默认产生哪些类成员函数

推荐文章

热门文章

相关标签