本文解释了KitKat(Android4.4)以及更早的Android框架中,外部存储访问权限是如何工作的。实际上,从KitKat直到AndroidL,外部存储访问权限暂时还没有变化。
TL;DR(选读)
KitKat之前的Android版本,仅提供了一个共享的外部存储区给应用。这个存储区可能位于一张SD卡上,也可能是内部flash的某个位置。系统仅使用权限WRITE_EXTERNAL_STORAGE来保护某个应用是否对其是否有任意的完全的读写权限。如果仅仅是读访问该存储区的话,应用无需申请任何权限。
KitKat的改动主要分两点:读访问需要读权限READ_EXTERNAL_STORAGE(但如果应用申请了写权限,也就隐含的包含了读权限);应用可以自由访问外部存储区中的专有目录(/Android/data/[PACKAGE_NAME])而无需申请任何权限。
在KitKat中,外部存储区的API被分隔以便支持多存储区:一个主存储区,以及更多的次存储区。主存储区 与之前的单存储区完全一致。KitKat之前所定义的相关API 都在主外部存储区上操作。而次外部存储区 稍微修改了写权限:所有的次外部存储区是全局可读的,只要应用有READ_EXTERNAL_STORAGE权限;但是,除该应用的外部存储区的专有目录外,该次外部存储区的其他位置是不可写的。
Google并不要求厂商采用这些SD上的额外权限。不过,存在一些限制,这些限制要求设备厂商将这些权限增加到次存储区。
如何使用外部存储区请参考:https://developer.android.com/guide/topics/data/data-storage.html
外部存储权限管理
Android 使用FUSE来管理外部存储区。sdcard 是一个守护服务,用来与fuse内核驱动交互,以便推算新文件的权限;sdcard也基于请求来源的uid/gid信息来动态接受或者拒绝各个访问请求。
基于FUSE机制,android可以在那些可移动存储区上实施基于owner/group/everyone的DAC访问控制,即使这些存储区使用FAT文件系统;甚至在DAC之上进一步实现多级访问控制。
step by step
早期设备中,外部存储区是唯一可以共享给PC的存储区,因此也被称为 共享存储区;其物理位置无关紧要,CTS要求的唯一规则是:只要不以其他方式使用时(例如,共享给PC),该存储区必须被默认挂载。对该存储区上的所有文件的写访问,仅有唯一的全局写权限WRITE_EXTERNAL_STORAGE所保护。
从Android 2.2开始(FroYo,API level 8),在外部存储区上出现了应用专有目录( 存储区上的/Android/data/[PACKAGE_NAME] )的概念。应用专有目录由系统负责管理,但其访问权限并没有什么butong (任何有全局写权限的应用都可以读写其他应用的专有目录)。当应用卸载时,包管理器将自动清理该应用的专有目录。
技术细节
在Android4.3上,外部存储区目录列表如下:
root@generic:/storage/sdcard # lld---rwxr-x system sdcard_rw 2014-03-14 01:20 Alarmsd---rwxr-x system sdcard_rw 2014-03-14 01:21 Androidd---rwxr-x system sdcard_rw 2014-03-14 01:20 DCIMd---rwxr-x system sdcard_rw 2014-03-14 01:20 Downloadd---rwxr-x system sdcard_rw 2014-03-14 01:18 LOST.DIRd---rwxr-x system sdcard_rw 2014-03-14 01:20 Moviesd---rwxr-x system sdcard_rw 2014-03-14 01:20 Musicd---rwxr-x system sdcard_rw 2014-03-14 01:20 Notificationsd---rwxr-x system sdcard_rw 2014-03-14 01:20 Picturesd---rwxr-x system sdcard_rw 2014-03-14 01:20 Podcastsd---rwxr-x system sdcard_rw 2014-03-14 01:20 Ringtones
注意,应用专有数据所在的子目录(/Android/data) 的权限与其他目录时相同的:
root@generic_x86:/storage/sdcard # ll Android/data/drwxrwx--- system sdcard_rw 2014-03-14 01:21 com.google.android.apps.maps
如果应用申请了android.permission.WRITE_EXTERNAL_STORAGE 权限,那么该应用就属于sdcard_rw组,因此可以读写该卷上的所有文件。此外,从外部存储区目录列表可以看出,所有子目录目录对everyone都是可读的,这意味着应用无需申请读权限。
Android4.1增加了读权限android.permission.READ_EXTERNAL_STORAGE 权限,授予应用sdcard_r 组的资格。但是到4.3为止,系统并没有实施该权限。
Behind the Scenes
在官方Framewaork API支持多外部存储区之前,有些厂商已经支持了多存储区。StorageManager和MountService系统服务可以管理厂商创建的一个主存储区以及多个次存储区。这里的主存储区相当于之前的我们熟悉的那个外部存储区。
现在的很多设备都支持SD卡,但是并没有将其用作外部存储区(从Android API的角度来看),而是作为一种“辅助”的存储区域。三星的Galaxy就是这样的例子。从权限的角度来看,SD卡的权限控制与外部存储区一样,但是不存在framework API能访问该辅助存储区。
下面是AOSP 设备存储区配置的一个片段:
on init
mkdir /mnt/shell/emulated 0700 shell shell
mkdir /storage/emulated 0555 root root
mkdir /mnt/media_rw/sdcard1 0700 media_rw media_rwmkdir /storage/sdcard1 0700 root rootexport EXTERNAL_STORAGE /storage/emulated/legacyexport EMULATED_STORAGE_SOURCE /mnt/shell/emulatedexport EMULATED_STORAGE_TARGET /storage/emulatedexport SECONDARY_STORAGE /storage/sdcard1
内部系统应用或者第三方应用可以使用的辅助存储区,其路径由环境变量SECONDARY_STORAGE来引用(该方法并不保证在任意设备中可用,这仅仅是出于习惯)。这就是将要修改的部分。。。
我们继续吧
从KitKat开始,主外部存储区和次外部存储区的概念最终出现在了框架API中:
Context.getExternalCacheDirs()
Context.getExternalFilesDirs()
Context.getObbDirs()
Environment.getStorageState()
上述的每个方法,先前都对应于一个API,返回位于主外部存储区上的指向应用专有数据目录的一个路径。新的版本返回所有可用存储区上的一个可用路径。基于此,主外部存储区以及我们先前提到的单一外部存储区实际上是一回事,不管是实际的挂载位置以及读写权限控制。次外部存储区作为一个有着不同的访问控制权限规则(更复杂了)的新区域来出现。
TIP: 这些新API返回结果的第一项,总是位于主存储区。这一点文档中没有明说,但是CTS对此作了检查。
为了兼容,现存的方法仍然返回主外部存储区:
getExternalStorageDirectory()
getExternalStoragePublicDirectory()
getExternalStorageState()
在KitKat中,仍未提供公开的API去获取任何次外部存储区上的顶级路径,而只能获取到次外部存储区上的该应用的专属目录。换句话说,次存储区上/DCIM、/Pictures目录可能已经存在,但是无法通过官方API来获取。
更多的技术细节
下面是典型的Android4.4 设备上的外部存储区的目录列表:
root@generic_x86:/storage/sdcard # lldrwxrwx--- root sdcard_r 2013-11-27 23:35 Alarmsdrwxrwx--x root sdcard_r 2013-11-27 23:36 Androiddrwxrwx--- root sdcard_r 2014-03-14 01:33 DCIMdrwxrwx--- root sdcard_r 2013-11-27 23:35 Downloaddrwxrwx--- root sdcard_r 2013-11-28 04:33 LOST.DIRdrwxrwx--- root sdcard_r 2013-11-27 23:35 Moviesdrwxrwx--- root sdcard_r 2013-11-27 23:35 Musicdrwxrwx--- root sdcard_r 2013-11-27 23:35 Notificationsdrwxrwx--- root sdcard_r 2013-11-27 23:35 Picturesdrwxrwx--- root sdcard_r 2013-11-27 23:35 Podcastsdrwxrwx--- root sdcard_r 2013-11-27 23:35 Ringtones
权限android.permission.READ_EXTERNAL_STORAGE仍然用来授予应用sdcard_r的成员资格。注意:everyone的读权限已经被移除。现在,该权限的实施方式与之前版本中的sdcard_rw类似。
如果我们看看应用专有目录,会发现有些不同:
root@generic_x86:/storage/sdcard # ll Android/data/drwxrwx--- u0_a33 sdcard_r 2013-11-27 23:36 com.google.android.apps.mapsroot@generic_x86:/storage/sdcard # ll Android/data/com.google.android.apps.maps/drwxrwx--- u0_a33 sdcard_r 2013-11-27 23:36 cachedrwxrwx--- u0_a33 sdcard_r 2013-11-27 23:36 testdata
在KitKat中,应用专有目录的owner为该应用的uid,有完全的访问能力。这意味着,应用不必申请任何权限就可以读写自己的在外部存储区上的专有数据目录。
注意:上面的两个列表中,sdcard_r 组也有着完整的DAC rwx权限,这显然不合适。实际上,FUSE守护进程会在运行时参与这些权限的动态合成。然而,这些DAC 权限位 对文件API(如canRead(), canWrite(), and canExecute() )的行为有非常重要的影响。由于这些API的非返回值仅依赖于内核文件系统,因此,对于外部存储区上的文件,即使这些API返回成功,实际上POSIX open调用仍可能失败。
android.permission.WRITE_EXTERNAL_STORAGE 将同时授予 应用 sdcard_r、sdcard_rw的成员资格。在KitKat中,验证写权限将要求更多的动态检查,因此FUSE守护用于补充文件系统权限。FUSE 授予应用对其专有数据目录的完整访问权限,对于其他进程,则验证他们是否为sdcard_rw的成员。
FUSE用于对非owner进程强制实施写保护访问的group,默认为sdcard_rw,但是可以通过-w标识进行配置。请看下面的例子:
service sdcard /system/bin/sdcard -u 1023 -g 1023 -l /data/media /mnt/shell/emulated class late_startservice fuse_sdcard1 /system/bin/sdcard -u 1023 -g 1023 -w 1023 -d /mnt/media_rw/sdcard1 /storage/sdcard1 class late_start disabled
我们可以看到,守护进程控制SD 卡采用GID 1023(media_rw, 只有系统应用可以作为其成员)作为group ,以便让某个进程可以写其并不拥有的目录。
从应用层看起来如何?
让我们分析下运行在一台有多个外部存储区的设备商的应用,能做什么,不能做什么。下表描述了应用开发者试图进行的访问以及KitKat的回应:
Action | Primary | Secondary |
---|---|---|
Read Top-Level Directories | R | R |
Write Top-Level Directories | W | N |
Read My Package’s Android Data Directory | Y | Y |
Write My Package’s Android Data Directory | Y | Y |
Read Another Package’s Android Data Directory | R | R |
Write Another Package’s Android Data Directory | W | N |
R = With Read Permission
W = With Write Permission
Y = Always
N = Never
我们可以注意到:
对于某应用在任意外部存储区上的专有数据文件,应用无需申请权限
除上一条规则外,主外部存储区其他位置的权限控制没有改变,在Android4.4以及之前的版本中保持一致。
次外部存储区是可读的,只要应用有全局读权限READ_EXTERNAL_STORAGE。如果用户将一堆文件拷贝到SD上的任意位置,然后插入设备,所有应用都可以读到这些文件。第三方应用只是不能在专有目录外增加修改文件。
Why Now?
简短的说,原因是CTS。如果你对此不熟,请点击此官方页面。
最新的CDD对外部存储区没有任何新的东西。没有规定要求可移除存储区不能作为主外部存储区。实际上,google仍然提供了AOSP配置样例,让厂商将物理SD卡作为唯一的外部存储区。此外,关于应用不能写次外部存储区的规则,从Android4.2开始就加到了CDD文档中。粗略看起来,的确没什么变化。
然后,CTS for Android4.4增加了新的测试,以便验证此外部存储区是否为非应用专用目录正确的配置了读权限。既然CTS包含了这些规则,OEM必须提供支持以便在设备中包含GMS(如Google Play)。
What About Sharing Files?
如果应用需要共享它在次外部存储区创建的文件呢?Google的回答是,这些积极避免将文件写到主外部存储区的应用,应该提供某种安全的文件共享方式,如使用content provider,或者使用新的SAF(存储访问框架)。
Samsung: A Case Study
总结
最重要的事情是:总所周知的主外部存储区在KitKat中没有消失,该存储区上的访问权限模型也没有变化。使用公开的框架API应用,不会因为KitKat中权限变化而受到任何不良影响。看起来,Google仅仅将次存储区用作存储应用专有目录。不过,究竟是将SD卡用作主外部存储区还是次外部存储区,最终还是完全取决于厂商。
事实上,因为权限的改变而受到影响的应用,没有直接使用系统API,而是采用了其他迂回的方式。我不是说它们这么做不好,这种迂回的方式至少说明了它们有些办法。不过,API是系统和开发者之间的契约。除非某个行为变化明显违背了这个契约,我很难支持任何人对这个系统指指点点。
名词
主外部存储区 primary external storage
次外部存储区 secondary external storage