本文来自于腾讯Bugly公众号(weixinBugly), 作者:chivyzhuang,未经作者同意,请勿转载,原文地址:https://mp.weixin.qq.com/s/qT5waBvKVbqhuqXYHC-3cA
| 导语 外部存储作为开发中经常接触的一个重要系统组成,在Android历代版本中,有过许许多多重要的变更。我也曾疑惑过,为什么一个简简单单外部存储,会存在存在这么多奇奇怪怪的路径
:/sdcard、/mnt/sdacrd、/storage/extSdCard、/mnt/shell/emulated/0、/storage/emulated/0、/mnt/shell/runtime/default/emulated/0…其实,这背后代表了一项项技术的成熟与发布:模拟外部存储、多用户、运行时权限…
一、各版本外部存储特性
Android 4.0
支持模拟外部存储(通过FUSE实现)
出现了主外部存储,以及二级外部存储(没有接口对外暴露)
支持MTP(Media Transfer Protocol)、PTP协议(Picture Transfer Protocol)
Android 4.1
开发者选项出现”强制应用声明读权限才可以进行读操作”的开关
Android 4.2
支持多用户,每个用户拥有独立的外部存储
Android 4.4
- 读操作需要声明READ_EXTERNAL_STORAGE权限
- 应用读写在外部存储的应用目录(/sdcard/Android//)不需要声明权限
- 增加了
Context.getExternalFilesDirs()
接口,可以获取应用在主外部存储和其他二级外部存储下的files路径 - 引入存储访问框架(SAF,Storage Access Framework)
Android 6.0
外部存储支持动态权限管理
Adoptable Storage特性
Android 7.0
- 引入作用域目录访问
补充一个点:如果应用的minSdkVersion和targetSdkVersion设置成<=3,系统会默认授予READ_EXTERNAL_STORAGE权限
二、部分特性讲解
1. 模拟外部存储
a. 必要性
- FAT32 属于微软专利,可能存在许可和法律问题(相关文章(https://www.howtogeek.com/183766/why-microsoft-makes-5-to-15-from-every-android-device-sold/));
- 可以定制Android自己的外部存储访问规则;
- 为多用户做铺垫;
b. 实现原理
系统/system/bin/sdcard守护进程,使用FUSE实现类FAT格式SD卡文件系统的模拟,也就是我们经常说的内置SD卡。(详细代码可以参考:/xref/system/core/sdcard/sdcard.c)
用户空间文件系统(Filesystem in Userspace,简称FUSE)是一个面向类Unix计算机操作系统的软件接口,它使无特权的用户能够无需编辑内核代码而创建自己的文件系统。目前Linux通过内核模块对此进行支持。
sdcard守护进程模拟外部存储大致流程(Android 4.0为例):
- 首先,指定/data/media目录用于模拟外部存储。该路径的owner和group一般为media_rw,这样保证只有sdcard程序或root进程能够访问该目录。
# create virtual SD card at /mnt/sdcard, based on the /data/media directory
# daemon will drop to user/group system/media_rw after initializing
# underlying files in /data/media will be created with user and group media_rw (1023)
service sdcard /system/bin/sdcard /data/media 1023 1023
class late_start
- sdcard守护进程启动后,打开/dev/fuse设备。
fd = open("/dev/fuse", O_RDWR);
if (fd < 0) {
ERROR("cannot open fuse device (%d)\n", errno);
return -1;
}
- 在/mnt/sdcard目录挂载fuse文件系统。
#define MOUNT_POINT "/mnt/sdcard"
...
sprintf(opts, "fd=%i,rootmode=40000,default_permissions,allow_other,"
"user_id=%d,group_id=%d", fd, uid, gid);
res = mount("/dev/fuse", MOUNT_POINT, "fuse", MS_NOSUID | MS_NODEV, opts);
if (res < 0) {
ERROR("cannot mount fuse filesystem (%d)\n", errno);
return -1;
}
*开线程,在线程中处理文件系统事件,并将结果写回。
void handle_fuse_requests(struct fuse *fuse)
{
unsigned char req[256 * 1024 + 128];
int len;
for (;;) {
len = read(fuse->fd, req, 8192);
if (len < 0) {
if (errno == EINTR)
continue;
ERROR("handle_fuse_requests: errno=%d\n", errno);
return;
}
handle_fuse_request(fuse, (void*) req, (void*) (req + sizeof(struct fuse_in_header)), len);
}
}
经过上面一系列步骤,sdcard进程在/mnt/sdcard路径上创建了一个FUSE文件系统,所有对/mnt/sdcard将转为事件由sdcard守护进程处理,并对应到/data/media目录。
例如,应用创建/mnt/sdcard/a文件,实际是创建/data/media/a文件。
c. 优点
模拟外部存储容量和/data分区是共享的,用户数据在内外存储的分配更加自由;
模拟外部存储本身不可卸载,不会因为卸载导致应用访问出现问题,也减少了外部因素导致被破坏的情况;
所有的访问都经过sdcard守护进程,Android可以定制访问规则;
d. 劣势
性能上存在一定损失
e. 影响
Android 6.0以后,由于动态权限管理的需要,会存在多个fuse挂载点,这导致inotify/FileObserver对外部存储进行文件事件监控时,会丢失事件。
inotify是Linux核心子系统之一,做为文件系统的附加功能,它可监控文件系统并将异动通知应用程序。 —— 维基百科(