2018年7月16日 星期一

Android Storage System 和 SD Card 存取

在 Android 平台開發中對 SD Card 的存取一直讓我很困惑,Android 提出的 internal storage 跟 external storage 是甚麼意思?Android 在 Context 跟 Environment class 都提供了 method 來對 external storage 的存取,這些 method 有甚麼差別,從 Android Kitkat 提出來的 secondary external storage 又是甚麼?

對 SD Card 存取的諸多混沌不清之處,讓人在開發程式時舉步維艱,因此在這邊整理一下自己的心得與了解,希望可以釐清這些問題,從而對 Android storage system 有一個比較清楚的認識

Android 想的跟你不一樣

首先最容易讓人困惑的大概就是 Android 提出的 internal/external storage 的概念了,我想大部分的人在第一次看到這個 internal/external storage 詞彙時,應該會認為所謂的 internal storage 就是在手機中內建的在主板上的 flash 區域,因為你可以看到,連系統 UI 上都是這麼顯示的

https://commonsware.com/blog/images/2014-04-07-storage-situation-internal-storage-1.png

而 external storage 理所當然的就是外接的 SD Card 了
我一開始也是這樣想的,但很可惜,Android 想的跟你不一樣,來看看 Android 是怎麼定義 internal storage 的



可以看到 Android 認為的 internal storage 跟 UI 上顯示的 internal storage 根本不是同樣的東西,官方文件是說,internal storage 是 app 的私有儲存區域,其他的 app 無法存取這個區域,而這個區域的內的資料會在 app 被移除時一併被刪除,文件裡從頭到尾沒有提到這個區域到底是在主板內建的 flash 還是在 SD Card 上,反之是用一個抽象化的概念來描述

再來看看 external storage 的定義吧



同樣的,external storage 跟 SD Card 也沒有任何關係,external storage 是使用者將裝置插入到 PC 上時被 mount 成 external storage 的區域,並且這個區域有可能是 SD Card,但也有可能不是

看完了 Android 對 internal 和 external 的定義,應該會有一種感覺,那就是 Android 根本不想讓 app 知道現在在用的儲存區域是內建的 flash 還是 SD Card,所以才用 internal/external storage 這種抽象概念把儲存區域包裝起來,實際上 internal 可以是在 SD Card 上,而 external 也可以在內建 flash 上

不清楚 Android 為什麼要這樣做,將儲存區域抽象化感覺對開發並沒有甚麼好處,絕大部分 app 應該都想知道現在實際在用的區域到底是不是 SD Card,另外雖然說 internal 也可以在 SD Card 上,但實際上應該沒有哪家製造商會做這種事情,所以可以預設 internal 都在內建 flash 上

瞭解了 internal/external storage 的觀念之後,接下來就是要講實際開發程式了,雖然 Android 想把 storage 抽象化,但還是有方法可以讓開發者判斷使用的 storage 到底是不是在 SD Card 上面的,尤其是被廣大開發者抗議之後,開始新增了對 SD Card 支援的 API...

在 Android 不同版本中對 storage 有不同的存取設計,所以以下會依照不同 Android 版本對 SD Card 的存取方法來做說明

Before Kitkat...

事實上,在 Kitkat 之前,Android 官方是不支援 SD Card 的,所以你連 SD Card 的路徑在哪都不知道,但是上有政策下有對策,可以用隱藏 API getVolumePaths 來取得系統上所有的 storage 路徑,假如製造商沒有故意修改這個 API,那 SD Card 的路徑應該會包含在其中

只要將取得的路徑跟官方 API 取得的 external storage 路徑比對,那不是 external storage 的路徑十有八九就是 SD Card 了

而因為在這個時期 Android 還沒有正式支援 SD Card,對 SD Card 的存取也就沒有甚麼限制,只要 app 持有 WRITE_EXTERNAL_STORAGE 權限而且你找的到 SD Card 的路徑,那 app 就可以對 SD Card 無限制的存取

Kitkat

Kitkat 對 SD Card 存取相關的 app 來說是一個重要的版本,在 Kitkat 中 Android 第一次對 SD Card 有了正式的支援,並且引入了 SAF 這個新的 storage 存取 API,但是 SAF 在 Kitkat 上還是相當難用的,等到了 Lolipop 才開始能用起來

Kitkat 對 SD Card 的支援主要是加入了 secondary external storage 的概念,而相對應的 primary external storage 其實跟 Kitkat 之前版本中的 external storage 講的是一樣的東西

Kitkat 還將下列這些 API 都加入了複數的版本
  • Context.getExternalCacheDir
  • Context.getExternalFilesDir
  • Context.getObbDirs

這些 API 原本在 Kitkat 之前都只會返回一個 String,也就是 primary external storage,但新加入的複數版的 API 會將 secondary external storage 一起返回,當然前提是你的系統上有 secondary external storage

就實務上來說,這個 secondary external storage 應該就是 SD Card 沒錯了,但實際上是不是 SD Card,在這時期也沒有任何官方 API 能給你答案,別忘了 Android 其實是不想讓你知道你是不是在用 SD Card 的

既然已經正式支援 SD Card 了,Android 也開始對 SD Card 的存取權限作控制,對於用 Context.getExternalFilesDirs API 返回的 secondary external storage 路徑,app 可以自由的寫入,但是這個路徑是專屬於 app 的,通常路徑會長得像這樣 /mnt/sdcard/Android/com.your.app/,而裡面的檔案在 app 移除時也會一併刪除

而 SD Card 中除了這個專屬於你 app 的路徑之外,其他路徑基本上 Android 是限制 app 直接去寫入的,除非使用 SAF 這個新的 storage 存取 API,不過 SAF雖然是可以讓你在 SD Card 的任意位置存取檔案,但是每次存取一個不同的檔案,系統就會跳出一次視窗通知使用者,使用者需同意之後,app 才有權限存取該檔案,這種可以說是擾民的使用方式,應該是沒有開發者會接受的

雖然在 XDA 有人提供了方法讓你不需經過使用者同意就可以在其他路徑寫檔和刪檔,但是這方法未經大量驗證,而且最重要的是它不能建立新資料夾,所以實用性不高

也有人說 ES explorer 可以在 Kitkat 上建立新資料夾,不過它用了甚麼神奇的魔法就不得而知了,網路上對這時期的 Android SD Card 存取絕大部分還是會推薦用一千零一招,root

Lolipop

因為在 Kitkat 上對 SD Card 的存取受到了這麼大的限制,Google 應該是收到了廣大開發者的抗議,所以在 Lolipop 上,Android 改善了對 SD Card 存取的機制

首先是 SAF,新增了一個 Intent 來讓 app 獲得 SD Card 上某個目錄以及其下所有子目錄的存取權限,app 可以透過這個 intent 詢問使用者是否要給予權限,一旦使用者同意,app 就可以對該目錄以及所有子目錄內的檔案做存取而不用重複詢問使用者

前面提到過在 Kitkat 時期沒有官方的 API 能告訴你 secondary external storage 是不是在 SD Card 上,不過 Lolipop 在 Environment class 中新增了 API isExternalStorageRemovable 可詢問任意位置的 storage 是否是 removable,若是 removable 那就是在 SD Card 上面了

基本上到了 Lolipop 時期,對 SD Card 的存取應該可以說沒有甚麼大問題了,不過後來 Android 還在持續修改對 SD Card 存取權限的機制以及又有了新的 storage API,這就之後有空再來分析了

Reference:

沒有留言:

張貼留言