Android 外部存儲

做者:莊燦傑,騰訊移動客戶端開發 工程師
商業轉載請聯繫騰訊WeTest得到受權,非商業轉載請註明出處。
原文連接:wetest.qq.com/lab/view/36…html


WeTest 導讀

外部存儲做爲開發中常常接觸的一個重要系統組成,在Android歷代版本中,有過許許多多重要的變動。我也曾疑惑過,爲何一個簡簡單單外部存儲,會存在存在這麼多奇奇怪怪的路徑:/sdcard、/mnt/sdacrd、/storage/extSdCard、/mnt/shell/emulated/0、/storage/emulated/0、/mnt/shell/runtime/default/emulated/0...其實,這背後表明了一項項技術的成熟與發佈:模擬外部存儲、多用戶、運行時權限...linux


1、各版本外部存儲特性

一、Android 4.0android

● 支持模擬外部存儲(經過FUSE實現)git

● 出現了主外部存儲,以及二級外部存儲(沒有接口對外暴露)github

● 支持MTP(Media Transfer Protocol)、PTP協議(Picture Transfer Protocol)shell


二、Android 4.1框架

● 開發者選項出現」強制應用聲明讀權限才能夠進行讀操做」的開關ide


三、Android 4.2函數

● 支持多用戶,每一個用戶擁有獨立的外部存儲性能


四、Android 4.4

● 讀操做須要聲明READ_EXTERNAL_STORAGE權限

● 應用讀寫在外部存儲的應用目錄(/sdcard/Android/<pkg>/)不須要聲明權限

● 增長了Context.getExternalFilesDirs() 接口,能夠獲取應用在主外部存儲和其餘二級外部存儲下的files路徑

● 引入存儲訪問框架(SAF,Storage Access Framework)



五、Android 6.0

● 外部存儲支持動態權限管理

● Adoptable Storage特性


六、Android 7.0

● 引入做用域目錄訪問



補充一個點:
若是應用的minSdkVersion和targetSdkVersion設置成<=3,系統會默認授予READ_EXTERNAL_STORAGE權限。若是應用的minSdkVersion和targetSdkVersion設置成<=3,系統會默認授予READ_EXTERNAL_STORAGE權限。


2、部分特性講解

1.模擬外部存儲

a. 必要性

● FAT32 屬於微軟專利,可能存在許可和法律問題(相關文章);

● 能夠定製Android本身的外部存儲訪問規則;

● 爲多用戶作鋪墊;


b. 實現原理

系統/system/bin/sdcard守護進程,使用FUSE實現類FAT格式SD卡文件系統的模擬,也就是咱們常常說的內置SD卡。(詳細代碼能夠參考:/xref/system/core/sdcard/sdcard.c)

用戶空間文件系統(Filesystem in Userspace,簡稱FUSE)是一個面向類Unix計算機操做系統的軟件接口,它使無特權的用戶可以無需編輯內核代碼而建立本身的文件系統。目前Linux經過內核模塊對此進行支持。用戶空間文件系統(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核心子系統之一,作爲文件系統的附加功能,它可監控文件系統並將異動通知應用程序。 —— 維基百科( zh.wikipedia.org/wiki/Inotif…

二、多用戶

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

圖例(來自xionchen.github.io/2016/08/25/…

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


通俗的講,掛載命名空間實現了掛載點的隔離,在不一樣掛載命名空間的進程,看到的目錄層次不一樣。


● 掛載傳播之共享掛載、從屬掛載、私有掛載

掛載命名空間實現了徹底的隔離,但對於有些狀況並不適用。例如在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)進行學習:man7.org/linux/man-p…


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和掛載命名空間這兩個技術配合實現。
經過下面這個提交記錄(android.googlesource.com/platform/sy…),咱們能夠很清楚的瞭解整個實現。



爲了達到不殺死進程,就可以賦予進程讀/寫外置存儲的目的,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年的手遊測試經驗,旨在經過制定針對性的測試方案,精準選取目標機型,執行專業、完整的測試用例,來提早發現遊戲版本的兼容性問題,針對性地作出修正和優化,來保障手遊產品的質量。目前該團隊已經支持全部騰訊在研和運營的手遊項目


歡迎進入:wetest.qq.com/product/clo… 體驗安卓真機

歡迎進入:wetest.qq.com/product/exp… 使用專家兼容測試服務。
WeTest兼容性測試團隊期待與您交流!You Create,We Test!

相關文章
相關標籤/搜索