作者:莊燦傑,騰訊移動客戶端開發 工程師
商業轉載請聯絡騰訊WeTest獲得授權,非商業轉載請註明出處。
原文連結:wetest.qq.com/lab/view/36…
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許可權。如果應用的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通過核心模組對此進行支援。使用者空間檔案系統(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…)
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
圖例(來自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!