Android 的 ExternalStorage

原文(台湾码农)

在 Android 上要儲存檔案經常會用到 Internal 或 External Storage,對部分的應用程式來說,常見的需求是「把檔案存到 SD card」。

從結論來說,Android 並沒有 SD card 這樣的概念,External Storage 也不一定是外接式儲存裝置。先有這樣的認知,接著就可以問以下幾個問題

  • 有哪些 Storage 可以用?
  • Context 與 Environment 有哪些 Storage 的 API 可以用?
  • 有怎樣的權限問題?

有哪些 Storage 可以用

打開 API 文件就可以看到幾個名詞

  1. Internal Storage
  2. Primary External Storage
  3. Secondary External Storage

Internal Storage

User 不能直接存取,只有 app 可以。路徑通常就是 data/data/。就是手機內部的儲存空間(internal flash),app 的設定檔或是 SQLite 資料庫就是寫在這裡面。属于各個應用程式的私密空間,寫在這裡面的東西,在系統安全無虞的前提下,只有該 App 可以存取。

雖說不能直接存取,但如果是你自己正在開發的 app,所以會安裝 debuggable 的 app,就能透過 adb 的 run-as 去存取該目錄

$ adb shell
$ run-as <app_packageName>

Primary External Storage

不管系統有沒有 SD card,你一定有 Primary External Storage 可以用。所以將 Primary external storage 當成 SD card 一定會被搞得很混亂。正如前述,External Storage 不一定是外接式儲存裝置。把 External 換成 Shared 就會變得容易理解:Primary Shared Storage

相較於 Internal storage 專門儲存一些 app 私密的資料,有時我們會想存下一些可供分享的檔案,好比程式產出的圖片、照片,或是錄下的聲音,透過網路抓下來的檔案…等等。這些檔案想要被其他程式使用,系統提供了一個非 Internal 的 Stroage,這就是 External Storage。

Primary External Storage 可能是 sd card,也可能是系統自己從 internal flash 分割一塊出來的空間。

Secondary External Storage

Primary 以外,剩下的就是 Secondary External Storage,SD-card 或是外接式 USB 硬碟,往往都是這一類。早期的 API 並沒有提供存取 Secondary External Storage 的方法,後來漸漸提供了稍微受限的存取方式,後面再講。

Traditional Storage and Adoptable Storage

API 上面看不見,但可以從文件裡面看到這個名詞。Traditional Storage 就是前述那些可以拔插置換的傳統儲存裝置,相對的名詞則是 Adoptable Storage。

買了手機卻擔心 internal flash 不夠大?在 Android 6.0(M, API 23) 之後提供了這個功能。有些手機的 SD 卡插槽設計得難以拔插,實務上可以視為半永久的儲存裝置。在這種插槽裝上 SD 卡,系統就會詢問是否要將其「收養」,變成 Adoptable Storage

adopt 之後系統便將此 storage 重新格式化、加密,接著把 Primary shared storage 的東西搬移到上面,開始把這顆裝置當成 Priamry shared storage 來使用。也因為會被加密,所以沒辦法拔下來給其他裝置使用。

API 使用

Context 跟 Environment 都有 storage 相關的 API,乍看會有點混亂,表列之後就清晰許多。Context 是跟 App 相關的東西,Environment 是跟系統相關的東西。在 Android Froyo 之前,app 只能拿到 internal 的目錄來放檔案,想要分享檔案,就要取得系統的 primary external storage 的最上層目錄,自己手動建立目錄把檔案放進去。

後來才透過統一的 API 來給各個 App 放置自己的 cache 或是 files;所以 Context 傳回來的都是「專屬於此 App 的相關目錄」

相較之下,Environment 傳回來的就是 external storage 的最上層目錄,或是共用的 Public directory

  • Context.getCacheDir() - 取得 /data/data/
  • Context.getFilesDir() - 取得 /data/data/
  • ContextCompat.getExternalFilesDirs(Context context, String type)
    • 回傳 application-specific 的目錄,專屬於這個 app,app 移除的時候會一併砍掉這個目錄
    • 存取不需要 Permission
    • 回傳的 File[],第一筆是位在 Primary external storage 上面的目錄
  • ContextCompat.getExternalCacheDirs(Context context)
  • Environment.getExternalStorageDirectory()
    • 回傳 primary external storage top path
    • 若有複數使用者 (UserManager),每個 user 看到的 external storage 不一樣
    • 砍 App 的時候不會把這裡面的一起砍掉
  • Environment.getExternalStoragePublicDirectory(String type)
    • 回傳 primary external storage 相對應 type 的 目錄
    • 目錄可能不存在,要自己呼叫 File.mkdirs()

歷史共業

SD card 的歷史從蠻荒時代的 1.5 就有了,看完一些文章的整理如下

  • 最早開始 external storage 是一個能夠 expose 給 PC 的 Volume

    • 可能是 internal flash 上面的一塊空間,也可能是 sdcard。
    • 唯一的規則是,它若沒有被 PC 拿去用,就要被預設掛載起來
    • 單一的 permission 就可以對整顆 external storage 的任何位置讀寫
  • Froyo 2.2 (Api level 8) 之後

    • permission 跟之前一樣
    • 加入了 application specific directory 的概念 - /Android/data/[Package name]
    • 但是移除 app 的時候,會順便連這個目錄一起移除
    • 若檔案被拿到這個目錄之外,則不會被移除
  • 在早期的時候,系統廠可能自己弄一個 primary external storage 及數個 secondary external storage,但此時的 API 都沒有開放存取 secondary。Samsung galaxy 手機就是這樣,而 sdcard 都是 secondary,所以 API 都無法存取。只有系統的 app 能夠把資料存在上面

  • Kitkat 4.0(Api level 19) 之後

    • 開始能夠存取 secondary external storage。此時 primary external storage 的權限跟以前一樣,secondary 的權限規則就不一樣
    • 還是沒有 api 能夠存取到 secondary 上,application-specific 以外的目錄,好比 secondary 的根目錄
    • 所以 secondary 上面的 /DCIM /Picture 都拿不到囉
  • Android N 7.0(Api level 24) 之後

存取權限

Android 上面的 storage 讀取權限,主要是靠 Linux 上面常見的檔案系統與 UID/GID 來控管,以 FUSE 提供檔案存取的介面。以前是在 AndroidManifest.xml 裡面宣告了權限,安裝 App 的時候就批准所有權限。後來在 6.0 之後則是執行到需要的時候,才會向使用者要權限

取得了 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 權限之後,便把該 app 加入 sdcard_rsdcard_w 的群組。所以系統只要設定好各個 storage 上面各個目錄的權限與群組,就可以控管該 app 能不能碰檔案了。

  • Froyo 2.2 (Api level 8) 之後

    • /storage/sdcard 底下的檔案,所屬的 group 是 sdcard_rw owner 是 root
    • Android/data/[Package Name] 也是一樣的權限
    • WRITE_EXTERNAL_STORAGE 會讓 app 加入 sdcard_rw 這個 group,所以就拿到完整的讀寫權限
    • READ_EXTERNAL_STORAGE 會拿到 sdcard_r 的權限,但是在 4.3 之前都沒用
  • Kitkat 4.0(Api level 19) 之後

    • /storage/sdcard 底下的檔案所屬的 group 是sdcard_r
    • 取得 READ_EXTERNAL_STORAGE 就能加入 sdcard_r group,能讀取 volume 上所有的檔案
    • 取得 WRITE_EXTERNAL_STORAGE 則會加入 sdcard_rsdcard_rw
    • 非 owner 非 group member 者,在這 volume 上的 read 權限被拿掉了
    • app-specific-dir 底下的 group 也屬於 sdcard_r,但是檔案的 owner 換成 app-id,所以 app 存取自己的目錄時不需要任何權限

Primary 與 Secondary 的目錄權限設定上也略有不同,所以就會有讀寫能力不完全相同的情況,表格整理之後就是這樣

Action Primary Secondary
讀最上層目錄 R R
寫最上層目錄 W N
讀自己的 Data Directory Y Y
寫自己的 Data Directory Y Y
讀他人的 Data Directory R R
寫他人的 Data Directory W N

R = 需要 Read Permission, W = 需要 Write Permission, Y = Always, N = Never

Reference

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容