Android外部存储

WeTest 导读

外部存储作为开发中经常接触的一个重要系统组成,在Android历代版本中,有过许许多多重要的变更。我也曾疑惑过,为什么一个简简单单外部存储,会存在存在这么多奇奇怪怪的路径:/sdcard、/mnt/sdacrd、/storage/extSdCard、/mnt/shell/emulated/0、/storage/emulated/0、/mnt/shell/runtime/default/emulated/0...其实,这背后代表了一项项技术的成熟与发布:模拟外部存储、多用户、运行时权限...



一、各版本外部存储特性

1、Android 4.0

● 支持模拟外部存储(通过FUSE实现)

● 出现了主外部存储,以及二级外部存储(没有接口对外暴露)

● 支持MTP(Media Transfer Protocol)、PTP协议(Picture Transfer Protocol)

2、Android 4.1

● 开发者选项出现”强制应用声明读权限才可以进行读操作”的开关

3、Android 4.2

● 支持多用户,每个用户拥有独立的外部存储

4、Android 4.4

● 读操作需要声明READ_EXTERNAL_STORAGE权限

● 应用读写在外部存储的应用目录(/sdcard/Android/<pkg>/)不需要声明权限

● 增加了Context.getExternalFilesDirs() 接口,可以获取应用在主外部存储和其他二级外部存储下的files路径

● 引入存储访问框架(SAF,Storage Access Framework)

5、Android 6.0

● 外部存储支持动态权限管理

● Adoptable Storage特性

6、Android 7.0

● 引入作用域目录访问

补充一个点:

如果应用的minSdkVersion和targetSdkVersion设置成<=3,系统会默认授予READ_EXTERNAL_STORAGE权限。

二、部分特性讲解

1.模拟外部存储

a. 必要性

● FAT32 属于微软专利,可能存在许可和法律问题(相关文章);

● 可以定制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进程能够访问该目录。

● sdcard守护进程启动后,打开/dev/fuse设备。

● 在/mnt/sdcard目录挂载fuse文件系统。

● 开线程,在线程中处理文件系统事件,并将结果写回。

经过上面一系列步骤,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核心子系统之一,做为文件系统的附加功能,它可监控文件系统并将异动通知应用程序。 —— 维基百科(https://zh.wikipedia.org/wiki/Inotify

2、多用户

a. 支持版本

● Android 4.2开始支持多用户,但仅限平板;

● Android 5.0开始,设备制造商可以在编译时候开启多用户模块;

b. 背景知识

● 绑定挂载——mount —bind

MS_BIND (Linux 2.4 onward)

Perform a bind mount, making a file or a directory subtree visible at another point within a file system. Bind mounts may cross file system boundaries and span chroot(2) jails. The filesystemtype and dataarguments are ignored. Up until Linux 2.6.26, mountflagswas also ignored (the bind mount has the same mount options as the underlying mount point). —— mount(2) - Linux man page(https://linux.die.net/man/2/mount)

图例(来自https://xionchen.github.io/2016/08/25/linux-bind-mount)

1) 将/home目录树bind到/mnt/backup:

2) bind完成之后,对/mnt/backup的访问将等同于对/home的访问,原/mnt/backup变为不可见。

● 挂载命名空间

Mount namespaces provide isolation of the list of mount points seen by the processes in each namespace instance. Thus, the processes in each of the mount namespace instances will see distinct single-directory hierarchies. ——mount_namespaces(7) - Linux manual page - man7.org

(http://man7.org/linux/man-pages/man7/mount_namespaces.7.html)

通俗的讲,挂载命名空间实现了挂载点的隔离,在不同挂载命名空间的进程,看到的目录层次不同。

● 挂载传播之共享挂载、从属挂载、私有挂载

挂载命名空间实现了完全的隔离,但对于有些情况并不适用。例如在Linux系统上,进程A在命名空间1挂载了一张CD-ROM,这时候命名空间2因为隔离无法看到这张CD-ROM。

为了解决这个问题,引入了挂载传播(mount propagation)。传播挂载定义了挂载点的传播类型:

1)共享挂载,此类型的挂载点会加入一个peer group,并会在group内传播和接收挂载事件;

2)从属挂载,此类型的挂载点会加入一个peer group,并会接收group内的挂载事件,但不传播;

3)共享/从属挂载,上面两种类型的共存体。可以从一个peer group(此时类型为从属挂载)接收挂载事件,再传播到另一个peer group;

4)私有挂载,此类型的挂载点没有peer group,既不传播也不接收挂载事件;

5)不可绑定挂载,不展开讲;

peer group的形成条件为,一个挂载点被设置成共享挂载,并满足以下任意一种情况:

1)挂载点在创建新的命名空间时被复制

2)从该挂载点创建了一个绑定挂载

另外再补充下传播类型的转换:

1)如果一个共享挂载是peer group中仅存的挂载点,那么对它应用从属挂载将会导致它变为私有挂载。
2)对一个非共享挂载类型的挂载点,应用从属挂载是无效的。

背景知识讲到这里,其中挂载点的传播类型比较不好理解,但很重要,可以参考上面mount namespace的Linux Programmer’s Manual里面的例子(搜索MS_XXX example)进行学习:http://man7.org/linux/man-pages/man7/mount_namespaces.7.html

c. 实现原理

概括多用户的外部存储隔离实现:应用进程在创建时,创建了新的挂载命名空间,然后通过绑定挂载对应用暴露当前用户的外部存储空间。

以Android 4.2代码为例【mountEmulatedStorage(dalvik_system_Zygote.cpp)】:

● 首先获取用户id。在多用户下,用户id为应用uid/100000。

● 通过unshare方法创建新的挂载命名空间。

● 获取外部存储相关的环境变量。EXTERNAL_STORAGE环境变量是从旧版本沿袭下来的环境变量,记录了外部存储的传统路径。EMULATED_STORAGE_SOURCE环境变量,记录绑定挂载的源路径,注意应用是没有权限进入这个目录的。EMULATED_STORAGE_TARGET记录绑定挂载的目标路径,应用获取的外部存储路径就在这个目录下。

● 准备挂载路径并进行绑定挂载。这里看mountMode为MOUNT_EXTERNAL_MULTIUSER时的执行分支,/mnt/shell/emulated/0将被绑定到/storage/emulated/0。如果是第二个用户,则是/mnt/shell/emulated/1绑定到/storage/emulated/1,数字就是用户id。注意这里是新的挂载命名空间,所以只有该应用看得到/storage/emulated/0下的绑定挂载,从adb shell下是看到的只能是个空目录。

● 为了兼容以前的版本,将用户的外部存储路径绑定到EXTERNAL_STORAGE环境变量指定的路径。

3. 动态权限管理

a.背景

Android 6.0引入了运行时权限,允许用户对危险权限进行动态授权,这部分权限包含外部存储访问权限。

b.实现原理

外部存储访问权限的动态授权,是利用FUSE和挂载命名空间这两个技术配合实现。
通过下面这个提交记录(https://android.googlesource.com/platform/system/core/+/f38f29c87d97cea45d04b783bddbd969234b1030%5E%21/#F1),我们可以很清楚的了解整个实现。

为了达到不杀死进程,就能够赋予进程读/写外置存储的目的,Android利用FUSE对/data/media模拟了三种访问视图,分别是default、read、write。

当应用被授予读/写权限时,vold子进程会切换到应用的挂载命名空间,将对应的视图重新绑定到应用的外部存储路径上。

切换进程的挂载命名空间,需要内核版本在3.8及以上,切换函数为setns,ndk貌似没有对开发者暴露,但可以在源码里找到arm的实现,有需要直接编入就可以了,也就一个sys call。

c. 代码分析

● 源码版本:Android 6.0.0_r1

● 首先从/xref/system/core/sdcard/sdcard.c开始分析,仅摘取部分代码,并加了些注释:

● 应用进程创建时,大致流程如下(/xref/frameworks/base/core/jni/com_android_internal_os_Zygote.cpp):

1)创建新的挂载命名空间;

2)将之前的挂载命名空间在/storage下的挂载全部去除,排除影响;

3)根据mount_mode,选择一个路径;

4)将选择的路径绑定到/storage下。

● 进程在运行时,当外部存储的访问许可发生改变(用户授权)时,基本流程如下(/xref/system/vold/VolumeManager.cpp):

1)获取init的挂载命名空间,为了对之后进程的挂载命2)名空间进行对比,如果一致,不重新绑定;

3)遍历/proc下各个进程目录,根据uid进行筛选;

找到对应的pid后,fork子进程进行重新挂载,这里用到setns进行挂载命名空间的切换;

重新挂载部分的逻辑和应用进程创建时基本一致,不难理解。

腾讯WeTest提供上千台真实手机,随时随地进行测试,保障应用/手游品质。节省百万硬件费用,加速敏捷研发流程。

同时腾讯WeTest兼容性测试团队积累了10年的手游测试经验,旨在通过制定针对性的测试方案,精准选取目标机型,执行专业、完整的测试用例,来提前发现游戏版本的兼容性问题,针对性地做出修正和优化,来保障手游产品的质量。目前该团队已经支持所有腾讯在研和运营的手游项目

欢迎进入:http://wetest.qq.com/product/cloudphone?体验安卓真机

欢迎进入:http://wetest.qq.com/product/expert-compatibility-testing?使用专家兼容测试服务。WeTest兼容性测试团队期待与您交流!You Create,We Test!

 

如果对使用当中有任何疑问,欢迎联系腾讯WeTest企业QQ:800024531

原文地址:https://www.cnblogs.com/wetest/p/8536868.html

时间: 2024-10-11 11:00:11

Android外部存储的相关文章

Android 外部存储权限分析

不知道你有么有发现,来自菜鸟的成长史:http://blog.csdn.net/zjbpku/article/details/25161131, KitKat之后的版本不再支持用户对外置SDcard(Secondary Storage)的写入等操作.如果用户想要将文件等copy到手机中,则只能 存储到内部存储器中,而无法存储到外置sdcard中,而且无法创建新的文件夹,这样一来给用户和开发者都带来了一定的不便.之所 以在KitKat之后版本中无法操作外置Sdcard,是因为Google更改了此模

Android外部存储设备状态

MEDIA_UNKNOWN:不能识别SD卡 MEDIA_REMOVED:SD卡被移除,没有SD卡 MEDIA_UNMOUNTED:SD卡存在,但是没有挂载,没有被使用(老版本中存在,现在已经不用) MEDIA_CHECKING:SD卡正在准备 MEDIA_MOUNTED:SD卡已经挂载,表明SD卡可以被使用

狂刷Android范例之3:读写外部存储设备

狂刷Android范例之3:读写外部存储设备 说明 狂刷Android范例系列文章开张了.每篇学习一个Android范例,将一个范例单独生成一个可运行的app,并对重点源代码进行简要分析.然后提供打包好的源代码下载. 功能 提供一个经典范例,监控Android外部存储设备状态,对公用目录,app私有目录进行读写操作,并展示在app界面上. 代码包在此,无需下载分: http://download.csdn.net/detail/logicteamleader/8790109 来源 ReadAss

Android Environment.getExternalStorageDirectory() 获取的是内部存储还是外部存储?

这几天在做Android应用的远程更新功能,将下载的更新包放在移动设备上指定的文件夹. 用的是  Environment.getExternalStorageDirectory() 这种方法.然后在获取的文件夹中新建一个hkapp文件夹,用来存放下载的apk文件. 那么,这个hkapp文件究竟是在那块存储区域呢? 一開始,看看网上的API,已经这种方法的字面意思.想当然地以为它就是获取SD卡上的文件夹,而不是手机的内部存储. 当然.除了望文生义之外,似乎还有确凿的证据支持我的观点.那就是在执行的

Android 在外部存储读写文件

本文主要介绍android中如何在外部存储读写数据 sd卡的路径 sdcard:2.3之前的sd卡路径 mnt/sdcard:4.3之前的sd卡路径 storage/sdcard:4.3之后的sd卡路径 打开file explorer 可以看到sdcard是个空的文件夹,因为这个文件夹是个快捷方式,指向/storag文件夹,接着打开storag文件夹 读写sd卡 最简单的打开sd卡的方式 File file = new File("sdcard/info.txt"); * 写sd卡需要

Android简易实战教程--第十五话《在外部存储中读写文件》

第七话里面介绍了在内部存储读写文件 点击打开链接. 这样有一个比较打的问题,假设系统内存不够用,杀本应用无法执行,或者本应用被用户卸载重新安装后.以前保存的用户名和密码都不会得到回显.所以,有必要注意这个问题 因此把文件保存到sd卡中.即今天所写的  第十五话<在外部存储中读写文件> 首先布局文件和第七话一样: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:

彻底理解android中的内部存储与外部存储

我们先来考虑这样一个问题: 打开手机设置,选择应用管理,选择任意一个App,然后你会看到两个按钮,一个是清除缓存,另一个是清除数据,那么当我们点击清除缓存的时候清除的是哪里的数据?当我们点击清除数据的时候又是清除的哪里的数据?读完本文相信你会有答案. 在android开发中我们常常听到这样几个概念,内存,内部存储,外部存储,很多人常常将这三个东西搞混,那么我们今天就先来详细说说这三个东西是怎么回事? 内存,我们在英文中称作memory,内部存储,我们称为InternalStorage,外部存储我

【转】 android中的文件操作详解以及内部存储和外部存储

摘要 其实安卓文件的操作和Java在pc环境下的操作并无二致,之所以需要单独讲解是因为安卓系统提供了不同于pc的访问文件系统根路径的api,同时对一个应用的私有文件做了统一的管理.根据我的经验,初学者在这部分感到很容易混淆内部存储和外部存储两个概念. 相对 其实安卓文件的操作和java在pc环境下的操作并无二致,之所以需要单独讲解是因为安卓系统提供了不同于pc的访问文件系统根路径的api,同时对一个应用的私有文件做了统一的管理.根据我的经验,初学者在这部分感到很容易混淆内部存储和外部存储两个概念

android内部存储与外部存储

存储在内部还是外部 所有的Android设备均有两个文件存储区域:"internal" 与 "external" . 这两个名称来自于早先的Android系统,当时大多设备都内置了不可变的(internal storage)及一个类似于SD card这样的可卸载的存储部件(external storage).之后有一些设备将"internal" 与 "external" 都做成了不可卸载的内置存储,虽然如此,但是这一整块还是从