一般应用开发者不需要了解具体的细节,只需要知道,Android操作系统向我们保证了

  1. 使用正确的API,就可以获取到合适的内部或者外部存储目录;
  2. 并且在多用户场景下,每个应用在不同用户使用时,都能看到一个完全隔离的内部和外部存储空间。

下面的内容完全是出于好奇,并且了解了其内部细节后,也要注意不要依赖内部实现细节实现业务逻辑,毕竟Google从来没有保证过内部实现是稳定的,只保证对外暴露的应用层API是稳定的。

发现了一个reddit,看起来很靠谱,但还没时间细看。。。https://www.reddit.com/r/Android/comments/496sn3/lets_clear_up_the_confusion_regarding_storage_in/

linux文件系统基础

文件系统的类型

如果我们在一个已经root的Android设备上,运行mount命令,可以看到当前所有的挂载点信息:
image.png
上图中,出现了很多种文件系统,包括看起来不像是存储设备的rootfs、tmpfs、proc、sysfs、cgroup、debugfs,以及我们耳熟能详的ext4,还有Android系统上才能见到的sdcardfs。
上面的这些文件系统,个人认为可以大体分为三类:

  • kernel出于特殊目的提供的虚拟文件系统,sysfs、proc等没有块设备与之对应的即属于这一类;
    • 分类依据:这类文件系统背后没有真实存在的文件,而是对应着kernel中的对象,是用户空间和内核空间交互的接口,因为linux内核的设计原则是一切都是文件,因此linux内核的一些内部状态信息需要通过文件系统来提供给用户空间,用户空间也需要使用文件系统来动态配置内核;
  • 可挂载文件系统,除了rootfs以及上面的虚拟文件系统之外的其他文件系统都可以归到这一类;
    • 分类依据:这类文件系统是一个格式化好的文件系统image(或者说映像?总感觉有的英文翻译过来就没那味儿了,这里还是直接说image吧),是存储设备中的一整块经过格式化的存储空间,内核只要安装了对应的文件格式驱动,就可以直接将存储空间挂载到文件系统树上;
    • 厂商通常会将系统配置文件实现做成一个image,然后在系统启动时以只读的方式挂载,这些映像往往不会预留任何多余的空间,因为它们永远不会被写入;
    • 除去上述只读image之外的存储设备空间会被格式化为一个或多个可读写的空间,Android设备的内部存储空间和外部存储空间都是可读写的,它们都由这类可读写image承载;
  • rootfs,这个文件系统比较特殊,需要单独列为一类,本质上,它也是一个只读的文件系统,内核启动时,会将rootfs作为第一个文件系统挂载;
    • https://blog.csdn.net/LEON1741/article/details/78159754,总结来说,rootfs的特殊之处在于,这个文件系统中必须提供0号进程init进程,shell应用程序和基本的shell命令对应的应用程序,kernel加上rootfs,就组成了一个最小可用的Linux操作系统,并且rootfs中的init进程还负责完成系统启动的剩余工作。

作为Android应用层开发者,上面的/data/media是我们可能需要关注的,这个目录就是我们平时使用的/sdcard/目录,可以看到它被挂载到了另外四个挂载点,且挂载的类型都是sdcardfs。在Android O之前,/sdcard/目录还是使用内核提供的fuse来模拟的,但如今已经被性能更好的sdcardfs替代了。
关于Android的/sdcard/目录的内容,下面会有更详细的讨论。

/sdcard/究竟是什么?

在使用Android设备时,有时会发现Android的文件系统有点奇怪,具体来说,就是Android的/sdcard/、/mnt/sdcard/、/storage/emulated/0/、/data/media/这些目录都指向我们通常所说的sd卡根目录,为什么会这样?为什么Android没有在/data和/sdcard分别挂载两个文件系统分别表示内部存储和外部存储?sdcardfs是什么,为什么会把/data/media挂载到
要弄明白这个问题,需要了解Android系统的历史。

从真实到虚拟

最早的Android系统中,/sdcard/存储目录对应的是一个独立的image,这个image通常对应一张独立的可插拔的sd卡,这个sd卡被挂载到/mnt/sdcard目录,/sdcard/是/mnt/sdcard/目录的软链接。
可惜好景不长,随着Google nexus设备的推出,Google自家的Android机器开始逐渐不支持外置sd卡,而是只允许使用内置sd卡了,这里面固然有商业的考量,因为内置sd卡容量可以卖的更贵,但其中也有技术和产品方面的考虑:

  1. 为程序员减负,应用很难处理sd卡一会存在一会不存在的情况,更别说多张sd卡切换时,外部存储的内容变来变去的情况了,不支持外部sd卡后应用逻辑变得更加简单更加健壮;
  2. 手机的内置存储越来越大,外置sd卡需求变得没那么重要;
  3. 简化硬件设计,设备更加一体化,拥有更好的防水性能、更轻薄的机身。

原来的两张卡简化成了一张卡之后,要解决的问题就是,原本的/mnt/sdcard/,或者说/sdcard/目录应该怎么办?
Google的解决方法是,用内置存储卡模拟一个/sdcard/出来。从Android 4.0开始,使用fuse将/data/media目录挂载到了/sdcard/目录,并且为了兼容性考虑,把/data/media/也挂载到了/mnt/sdcard/目录,这样之前的应用不论访问的是哪个目录,都可以读取到外部存储了。当应用访问/sdcard/时,实际上文件操作会被fuse拦截,fuse会做一些权限检查操作,然后将文件操作在/data/media/目录重放,因此应用访问/sdcard/,实际上访问的就是/data/media/,由于/data/media/只有root有权限访问,因此应用程序无法直接访问。
并且,由于使用了fuse,实际上这两个挂载点是共享同一个磁盘上的image的,因此/data/和/sdcard/可以共享存储空间。此外Android4.0能够切换到fuse还和Android在3.0就支持了MTP有关:过去为了让/sdcard/目录挂载到PC(Windows)上,必须要将/sdcard/对应的image格式化成FAT32这个微软持有专利的格式,并将sd卡直接挂载到PC上,在使用fuse支持/sdcard/之后,Android无法支持并且也不准备再支持直接将sd卡设备挂载到PC了,而是使用MTP作为唯一文件传输协议。
这里有一篇mtp的介绍:https://cloud.tencent.com/developer/article/1029989
另外,这里有一片对mtp的吐槽:https://www.v2ex.com/t/550572

多用户引入的问题

为了说明为什么多用户导致了存储目录的变更,首先需要看看原本的存储目录有什么问题。多用户场景下,一个应用可以被多个用户使用,但每个用户使用应用时,应用的存储目录都应该是分离的,这意味着,系统需要为每个用户维护一份应用的私有存储和公共存储目录。这应该如何实现呢?最直接的方案,就是为每个用户维护各自的/sdcard/和/data/存储目录,让这些存储目录之间相互隔离。
Android系统正是这么干的,事实上,/storage/emulated/0就是这么来的。下面,来看看/sdcard/目录在这个阶段都发生了些什么?
为了支持多用户,需要为每个用户创建对应的外部存储空间,因此需要在创建新用户时,为用户创建新的/sdcard/存储空间,并在用户切换时,将对应的存储空间挂载到/sdcard/目录。为此,Android定义主用户的存储空间为/storage/emulated/0/,而当前用户的存储空间为/storage/emulated/self。回头看一眼上面的mount输出的信息,/data/media/被作为sdcardfs挂载到了/storage/emulated/,因此/data/media/中的文件和/storage/emulated,以及另外几个挂载点,都是完全一致的。

运行时权限带来了什么

从6.0开始,Android系统开始支持运行时权限。回头看一眼mount命令的输出,可以看到/data/media被挂载到了/mount/runtime/[read | write | default]/emulated/目录,这就是为了支持运行时权限而出现的,可以看到,这三个挂载点的mask值不同,由于这里使用的是sdcardfs,所以需要查看sdcardfs文档来确认mask是什么意思,这三个挂载点的权限控制就是用这里的mask来区分的。
在应用程序启动时,系统会检查应用的运行时权限,并为应用进程bind mount对应的/mnt/runtime/xxx目录作为/storage/目录(可以单独为一个应用程序挂载目录吗?当然可以,Linux对于namespace/cgroup的支持已经很完善了,内核层面支持应用的mount namespace隔离。),通过这种方式来控制应用程序对sdcard目录的访问权限。当应用程序访问/sdcard/目录时,由于/sdcard/是一个指向/storage/emulated/0的软链接,最终其实会访问到/mnt/runtime/[read | write | default]/emulated/0这个目录。
当用户为应用程序授予权限时,系统会为应用进程重新做一次mount,将/mnt/runtime/write/目录挂载到/storage/目录。

今天

Android O已经将fuse改成了sdcardfs,替换的主要收益是sdcardfs效率比fuse更高。能力上,倒是没有什么区别,毕竟fuse已经是最灵活的文件系统实现方案了。