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 采用了沙箱模型,应用只能使用自己独立的存储空间,不能直接像过去一样通过公共目录方式,存储文件。

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

发布了15 篇原创文章 · 获赞 6 · 访问量 1万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章